diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d97236eab..1e0fa7533 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,26 +5,22 @@ There are no real formal contribution guidelines right now, they will maybe come ## Repository Structure -- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation. - This mainly concerns API classes not a lot of implementation. -- [beacon](beacon) - The XPipe beacon component is responsible for handling all communications between the XPipe - daemon and the client applications, for example APIs and the CLI - [app](app) - Contains the XPipe daemon implementation and the XPipe desktop application - [dist](dist) - Tools to create a distributable package of XPipe - [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension ## Development Setup -You need to have JDK for Java 25 installed to compile the project. +You need to have JDK for Java 26 installed to compile the project. If you are on Linux or macOS, you can easily accomplish that by using [SDKMAN](https://sdkman.io/) and running ```bash curl -s "https://get.sdkman.io" | bash . "$HOME/.sdkman/bin/sdkman-init.sh" -sdk install java 25.0.2-graalce -sdk default java 25.0.2-graalce +sdk install java 26.0.2-zulu +sdk default java 26.0.2-zulu ``` -On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=25). +On Windows, you have to manually install a JDK, e.g. from [Azul](https://www.azul.com/downloads/?version=java-26-sts&package=jdk#zulu). You can configure a few development options in the file `app/dev.properties` which will be automatically generated when gradle is first run. @@ -46,22 +42,21 @@ So for example if you currently have XPipe `21.0` installed, you should run `git You can use the gradle wrapper to build and run the project: - `gradlew app:run` will run the desktop application. You can set various useful properties in `app/build.gradle` - `gradlew clean dist` will create a distributable production version in `dist/build/dist/base`. -- `gradlew :test` will run the tests of the specified project. You are also able to properly debug the built production application: - The `dist/build/dist/base/app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it ## Modularity and IDEs -All XPipe components target [Java 25](https://openjdk.java.net/projects/jdk/25/) and make full use of the Java Module System (JPMS). +All XPipe components target [Java 26](https://openjdk.java.net/projects/jdk/26/) 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, 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. +For example, it might be difficult to 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 25) +When setting up the project in IntelliJ, make sure that the correct JDK (Java 26) is selected both for the project and for gradle itself. ## Contributing guide @@ -70,7 +65,7 @@ Especially when starting out, it might be a good idea to start with easy tasks f ### Interacting via the HTTP API -You can create clients that communicate with the XPipe daemon via its HTTP API. +You can create external clients and tools that communicate with the XPipe daemon via its HTTP API. To get started, see the [OpenAPI spec](https://docs.xpipe.io/api). ### Implementing support for a new editor @@ -90,10 +85,6 @@ Once you created your custom classes, you have to register them in your module i All actions that you can perform for certain connections in the connection overview tab are implemented using an [Action API](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/ext/ActionProvider.java). You can find a sample implementation [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java) and many common action implementations [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/action). -### Adding more predefined scripts - -You can add custom script definitions [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java) and [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts). - ### Adding more file icons for specific types You can register file types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/resources/io/xpipe/app/resources/file_list.txt) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/img/browser). diff --git a/README.md b/README.md index d37ceaa5a..a044fcdfe 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ It currently supports: - [Proxmox PVE](https://docs.xpipe.io/guide/proxmox), [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), and [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines - [Tailscale](https://docs.xpipe.io/guide/tailscale), [Netbird](https://docs.xpipe.io/guide/netbird), and [Teleport](https://docs.xpipe.io/guide/teleport) connections - [AWS](https://docs.xpipe.io/guide/aws) and [Hetzner Cloud](https://docs.xpipe.io/guide/hcloud) servers -- [RDP](https://docs.xpipe.io/guide/rdp) and [VNC](https://docs.xpipe.io/guide/vnc) connections +- [RDP](https://docs.xpipe.io/guide/rdp) + gateways and [VNC](https://docs.xpipe.io/guide/vnc) connections - Windows Subsystem for Linux, Cygwin, and MSYS2 environments - [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers - [Powershell Remote Sessions](https://docs.xpipe.io/guide/pssession) @@ -28,55 +28,55 @@ It currently supports: - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/app/build.gradle b/app/build.gradle index efa32b060..9797276d1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -19,12 +19,9 @@ configurations { } dependencies { - api project(':core') - api project(':beacon') - compileOnly 'org.hamcrest:hamcrest:3.0' - compileOnly 'org.junit.jupiter:junit-jupiter-api:5.14.2' - compileOnly 'org.junit.jupiter:junit-jupiter-params:5.14.2' + compileOnly 'org.junit.jupiter:junit-jupiter-api:6.1.0' + compileOnly 'org.junit.jupiter:junit-jupiter-params:6.1.0' api 'com.vladsch.flexmark:flexmark:0.64.8' api 'com.vladsch.flexmark:flexmark-util:0.64.8' @@ -48,43 +45,47 @@ 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-core:1.0.0') { + api ('io.modelcontextprotocol.sdk:mcp-core:1.1.3') { exclude group: "com.ethlo.time", module: "itu" } - api ('io.modelcontextprotocol.sdk:mcp-json-jackson2:1.0.0') { + api ('io.modelcontextprotocol.sdk:mcp-json-jackson3:1.1.3') { exclude group: "com.ethlo.time", module: "itu" - exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml" + exclude group: "tools.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") { + api ("com.networknt:json-schema-validator:3.0.6") { exclude group: "com.ethlo.time", module: "itu" - exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml" + exclude group: "tools.jackson.dataformat", module: "jackson-dataformat-yaml" } - api "com.github.weisj:jsvg:1.7.2" + api "com.github.weisj:jsvg:2.1.0" api 'io.xpipe:vernacular:1.19' - api 'org.bouncycastle:bcprov-jdk18on:1.83' + api 'org.bouncycastle:bcprov-jdk18on:1.84' api 'info.picocli:picocli:4.7.7' api 'org.apache.commons:commons-lang3:3.20.0' api 'io.sentry:sentry:8.41.0' - api 'commons-io:commons-io:2.21.0' - api "com.fasterxml.jackson.core:jackson-databind:2.21.1" - api "com.fasterxml.jackson.core:jackson-annotations:2.21" - api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.1" + api 'commons-io:commons-io:2.22.0' + api "tools.jackson.core:jackson-databind:3.2.0" + api "com.fasterxml.jackson.core:jackson-annotations:2.22" 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 "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.8' + api "org.slf4j:slf4j-api:2.0.18" + api "org.slf4j:slf4j-jdk-platform-logging:2.0.18" + api 'io.xpipe:modulefs:0.1.9' api 'net.synedra:validatorfx:0.4.2' - api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar") + api 'io.xpipe:atlantafx-base:2.1.0' + api ("com.dlsc.atlantafx:themes:1.9.0") { + exclude group: "io.github.mkpaz", module: "atlantafx-base" + } - api("org.int4.fx:fx-values:0.4") + api "org.int4.fx:fx-values:0.4" api files("$rootDir/gradle/gradle_scripts/fx-builders-1.0.0-SNAPSHOT.jar") + + api "com.google.zxing:core:3.5.4" } apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle" @@ -97,10 +98,12 @@ jar { application { mainModule = groupName + '.app' mainClass = groupName + '.app.Main' - applicationDefaultJvmArgs = jvmRunArgs + applicationDefaultJvmArgs = daemonJvmRunArgs } run { + dependsOn extensionJarDepList + systemProperty propertyName('useVirtualThreads'), 'false' systemProperty propertyName('mode'), 'gui' systemProperty propertyName('writeLogs'), "true" @@ -123,19 +126,26 @@ run { def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList()) classpath += exts - dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList()) + if (fullVersion) { + def cli = project(':cli').getTasksByName('jar', true)[0].outputs.files.singleFile + classpath += files(cli) + + dependsOn(project(':cli').getTasksByName('jar', true)[0]) + dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList()) + } } tasks.register('runAttachedDebugger', JavaExec) { + dependsOn extensionJarDepList + workingDir = rootDir classpath = run.classpath mainModule = groupName + '.app' mainClass = groupName + '.app.Main' modularity.inferModulePath = true - jvmArgs += jvmRunArgs + jvmArgs += daemonJvmRunArgs jvmArgs += List.of( - "-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.9.jar=port:7857,host:localhost".toString(), - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0" + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0.0.0.0:7857" ) jvmArgs += ['-XX:+EnableDynamicAgentLoading'] systemProperties run.systemProperties @@ -173,6 +183,26 @@ processResources { into resourcesDir } } + + doLast { + def resourcesDir = new File(sourceSets.main.output.resourcesDir, "io/xpipe/app/resources/third-party") + resourcesDir.mkdirs() + + def home = System.getProperty("java.home") + def legalDir = file("$home/legal") + + def l = new ArrayList() + fileTree(legalDir).matching { + include "**/*.md" + }.visit { f -> + if (f.getFile().isFile()) { + l.add(f.getFile().text) + } + } + + def text = String.join("\n\n", l) + file("$resourcesDir/openjdk-deps.license").text = text + } } distTar { diff --git a/app/src/main/java/io/xpipe/app/Main.java b/app/src/main/java/io/xpipe/app/Main.java index 5368cec57..748e9bf8a 100644 --- a/app/src/main/java/io/xpipe/app/Main.java +++ b/app/src/main/java/io/xpipe/app/Main.java @@ -1,6 +1,5 @@ package io.xpipe.app; -import io.xpipe.app.core.AppNames; import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.mode.AppOperationMode; @@ -13,14 +12,9 @@ public class Main { return; } - // Since this is not marked as a console application, it will not print anything when you run it in a console on - // Windows - if (args.length == 1 && args[0].equals("--help")) { - System.out.printf(""" - 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. - %n""", AppNames.ofCurrent().getExecutableName()); + if (args.length == 1 && (args[0].equals("--help") || args[0].equals("help"))) { + System.out.println( + "For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli"); return; } 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 b3519aeab..ef40e7c55 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java @@ -3,15 +3,15 @@ package io.xpipe.app.action; import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.comp.base.*; -import io.xpipe.app.ext.DataStore; import io.xpipe.app.hub.action.BatchStoreAction; import io.xpipe.app.hub.action.MultiStoreAction; 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.hub.creation.StoreChoiceComp; +import io.xpipe.app.hub.list.StoreListChoiceComp; +import io.xpipe.app.hub.list.StoreViewState; import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.DataStore; import javafx.beans.property.*; import javafx.collections.FXCollections; @@ -56,7 +56,7 @@ public class ActionConfigComp extends SimpleRegionBuilder { }); var choice = new StoreListChoiceComp<>( - listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null); + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null); choice.hide(listProp.emptyProperty()); choice.maxHeight(450); return choice; 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 795efcfc7..afdc58a9c 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java @@ -4,14 +4,14 @@ import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.comp.base.ScrollComp; -import io.xpipe.app.ext.DataStore; import io.xpipe.app.hub.action.BatchStoreAction; 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.hub.list.StoreListChoiceComp; +import io.xpipe.app.hub.list.StoreViewState; import io.xpipe.app.platform.OptionsBuilder; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.DataStore; import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; @@ -54,7 +54,7 @@ public class ActionConfirmComp extends SimpleRegionBuilder { } var choice = new StoreListChoiceComp<>( - listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null); + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null); choice.maxHeight(450); choice.setEditable(false); choice.hide(listProp.emptyProperty()); diff --git a/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java b/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java index 04ff87008..21b4a131f 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java +++ b/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java @@ -1,35 +1,34 @@ package io.xpipe.app.action; -import io.xpipe.app.ext.DataStore; import io.xpipe.app.hub.action.*; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.storage.DataStorage; -import io.xpipe.core.JacksonMapper; -import io.xpipe.core.UuidHelper; +import io.xpipe.app.store.DataStore; +import io.xpipe.app.util.JacksonMapper; +import io.xpipe.app.util.UuidHelper; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; import java.util.ArrayList; public class ActionJacksonMapper { @SuppressWarnings("unchecked") - public static T parse(JsonNode tree) throws JsonProcessingException { + public static T parse(JsonNode tree) { if (!tree.isObject()) { return null; } var id = tree.get("id"); - if (id == null || !id.isTextual()) { + if (id == null || !id.isString()) { return null; } var provider = ActionProvider.ALL.stream() - .filter(actionProvider -> id.textValue().equals(actionProvider.getId())) + .filter(actionProvider -> id.stringValue().equals(actionProvider.getId())) .findFirst(); if (provider.isEmpty()) { return null; @@ -43,10 +42,13 @@ public class ActionJacksonMapper { var object = (ObjectNode) tree; var ref = tree.get("ref"); - var mapper = JacksonMapper.newMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + var mapper = JacksonMapper.getDefault() + .rebuild() + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + .build(); if (ref != null && !ref.isArray() && StoreAction.class.isAssignableFrom(clazz.get())) { - validateRef(provider.get(), ref.asText()); + validateRef(provider.get(), ref.asString()); var action = mapper.treeToValue(tree, clazz.get()); return (T) action; } @@ -60,7 +62,7 @@ public class ActionJacksonMapper { var batchActions = new ArrayList>(); object.remove("ref"); for (JsonNode batchRef : ref) { - validateRef(provider.get(), batchRef.asText()); + validateRef(provider.get(), batchRef.asString()); object.set("ref", batchRef); var action = mapper.treeToValue(object, clazz.get()); batchActions.add((StoreAction) action); @@ -70,7 +72,7 @@ public class ActionJacksonMapper { var makeMulti = ref != null && ref.isArray() && MultiStoreAction.class.isAssignableFrom(clazz.get()); if (makeMulti) { - validateRef(provider.get(), ref.asText()); + validateRef(provider.get(), ref.asString()); object.remove("ref"); object.set("refs", ref); var action = mapper.treeToValue(object, clazz.get()); 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 bcf0195d9..ed0988502 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionPickComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionPickComp.java @@ -19,9 +19,7 @@ public class ActionPickComp extends ModalOverlayContentComp { protected Region createSimple() { var prop = new SimpleObjectProperty<>(action); var top = new ActionConfigComp(prop); - var bottom = new ActionShortcutComp(prop, () -> { - getModalOverlay().close(); - }); + var bottom = new ActionShortcutComp(prop); var options = new OptionsBuilder().addComp(top).addComp(bottom); var scroll = new ScrollComp(options.buildComp()); return scroll.build(); diff --git a/app/src/main/java/io/xpipe/app/action/ActionProvider.java b/app/src/main/java/io/xpipe/app/action/ActionProvider.java index 3abeb1f44..4fcd5d33b 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionProvider.java +++ b/app/src/main/java/io/xpipe/app/action/ActionProvider.java @@ -1,8 +1,8 @@ package io.xpipe.app.action; +import io.xpipe.app.ext.ModuleLayerLoader; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.issue.TrackEvent; -import io.xpipe.core.ModuleLayerLoader; import java.util.*; @@ -10,6 +10,13 @@ public interface ActionProvider { List ALL = new ArrayList<>(); + static ActionProvider byId(String id) { + return ALL.stream() + .filter(p -> p.getId() != null && p.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No action provider found for ID " + id)); + } + static void initProviders() { TrackEvent.trace("Starting action provider initialization"); for (ActionProvider actionProvider : ALL) { @@ -59,5 +66,10 @@ public interface ActionProvider { .map(p -> p.get()) .toList()); } + + @Override + public boolean initForCli() { + return false; + } } } 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 66622c518..6d7909672 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java @@ -7,14 +7,17 @@ import io.xpipe.app.comp.base.ButtonComp; 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.core.AppProperties; 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.process.LocalShell; import io.xpipe.app.update.AppDistributionType; import io.xpipe.app.util.*; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; @@ -27,17 +30,16 @@ import java.util.List; public class ActionShortcutComp extends SimpleRegionBuilder { private final Property action; - private final Runnable onCreateMacro; - public ActionShortcutComp(Property action, Runnable onCreateMacro) { + public ActionShortcutComp(Property action) { this.action = action; - this.onCreateMacro = onCreateMacro; } @Override protected Region createSimple() { var options = new OptionsBuilder(); options.nameAndDescription("actionDesktopShortcut").addComp(createDesktopComp()); + options.nameAndDescription("actionCommand").addComp(createCommandComp()); options.name(AppDistributionType.get().isSupportsUrls() ? "actionUrlShortcut" : "actionUrlShortcutDisabled"); options.description( AppDistributionType.get().isSupportsUrls() @@ -71,6 +73,37 @@ public class ActionShortcutComp extends SimpleRegionBuilder { return group; } + private BaseRegionBuilder createCommandComp() { + var command = new SimpleStringProperty(); + action.subscribe((v) -> { + var s = ActionUrls.toUrl(v); + ThreadHelper.runFailableAsync(() -> { + var exec = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; + var inPath = LocalShell.getShell().view().findProgram(exec).isPresent(); + var defaultDataDir = AppProperties.get() + .getDefaultDataDir() + .equals(AppProperties.get().getDataDir()); + var c = (inPath ? exec : "\"" + AppInstallation.ofCurrent().getCliExecutablePath() + "\"") + " open \"" + + s + "\"" + + (!defaultDataDir ? " -d \"" + AppProperties.get().getDataDir() + "\"" : ""); + Platform.runLater(() -> { + command.set(c); + }); + }); + }); + + var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> { + ClipboardHelper.copyUrl(command.getValue()); + }) + .describe(d -> d.nameKey("copy")); + var field = new TextFieldComp(command); + field.apply(struc -> struc.setEditable(false)); + var group = new InputGroupComp(List.of(field, copyButton)); + group.setMainReference(field); + group.hide(Bindings.isNull(command)); + return group; + } + private BaseRegionBuilder createDesktopComp() { var url = BindingsHelper.map(action, abstractAction -> ActionUrls.toUrl(abstractAction)); var name = new SimpleStringProperty(); @@ -117,11 +150,4 @@ public class ActionShortcutComp extends SimpleRegionBuilder { group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction))); return group; } - - @SuppressWarnings("unused") - private BaseRegionBuilder createMacroComp() { - var button = new ButtonComp( - AppI18n.observable("createMacro"), new FontIcon("mdi2c-clipboard-multiple-outline"), onCreateMacro); - return button; - } } diff --git a/app/src/main/java/io/xpipe/app/action/ActionUrls.java b/app/src/main/java/io/xpipe/app/action/ActionUrls.java index 57b985542..985b1f3a3 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionUrls.java +++ b/app/src/main/java/io/xpipe/app/action/ActionUrls.java @@ -1,12 +1,12 @@ package io.xpipe.app.action; -import io.xpipe.core.JacksonMapper; -import io.xpipe.core.SecretValue; +import io.xpipe.app.util.Base64Helper; +import io.xpipe.app.util.JacksonMapper; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.SneakyThrows; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; import java.net.URLDecoder; import java.net.URLEncoder; @@ -21,8 +21,8 @@ public class ActionUrls { } private static List nodeToString(JsonNode node) { - if (node.isTextual()) { - return List.of(encodeValue(node.asText())); + if (node.isString()) { + return List.of(encodeValue(node.asString())); } if (node.isArray()) { @@ -36,7 +36,7 @@ public class ActionUrls { return list; } - var enc = SecretValue.toBase64e(node.toPrettyString().getBytes(StandardCharsets.UTF_8)); + var enc = Base64Helper.toBase64Url(node.toPrettyString()); return List.of("~" + enc); } @@ -65,7 +65,7 @@ public class ActionUrls { return encodedURL; } - public static Optional parse(String queryString) throws Exception { + public static Optional parse(String queryString) { var query = splitQuery(queryString); var id = query.get("id"); @@ -99,7 +99,7 @@ public class ActionUrls { var list = new ArrayList<>(); for (String s : entry.getValue()) { if (s.startsWith("~")) { - var json = SecretValue.fromBase64e(s.substring(1)); + var json = Base64Helper.fromBase64UrlString(s.substring(1)); var node = JacksonMapper.getDefault().readTree(json); list.add(node); } else { diff --git a/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java b/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java index 000c60808..cf6fc9dd0 100644 --- a/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java +++ b/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java @@ -1,19 +1,10 @@ package io.xpipe.app.action; import java.net.URI; -import java.util.List; -import java.util.Objects; import java.util.Optional; public interface LauncherUrlProvider extends ActionProvider { - static List getAll() { - return ActionProvider.ALL.stream() - .map(actionProvider -> actionProvider instanceof LauncherUrlProvider lup ? lup : null) - .filter(Objects::nonNull) - .toList(); - } - static Optional find(String url) { return ActionProvider.ALL.stream() .filter(actionProvider -> actionProvider instanceof LauncherUrlProvider lup diff --git a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java index da8b99cb1..da1045bdd 100644 --- a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java +++ b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java @@ -1,8 +1,8 @@ package io.xpipe.app.action; -import io.xpipe.app.ext.DataStore; import io.xpipe.app.hub.action.impl.OpenHubMenuLeafProvider; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.store.DataStore; import java.util.List; import java.util.Objects; @@ -36,6 +36,8 @@ public interface QuickConnectProvider extends ActionProvider { String getPlaceholder(); + String getTemplate(); + boolean skipDialogIfPossible(); default void open(DataStoreEntry e) { diff --git a/app/src/main/java/io/xpipe/app/action/SerializableAction.java b/app/src/main/java/io/xpipe/app/action/SerializableAction.java index 96b5441ab..b47216211 100644 --- a/app/src/main/java/io/xpipe/app/action/SerializableAction.java +++ b/app/src/main/java/io/xpipe/app/action/SerializableAction.java @@ -2,12 +2,12 @@ package io.xpipe.app.action; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.DataStoreFormatter; -import io.xpipe.core.JacksonMapper; -import io.xpipe.core.UuidHelper; +import io.xpipe.app.util.JacksonMapper; +import io.xpipe.app.util.UuidHelper; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.experimental.SuperBuilder; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; import java.util.*; import java.util.stream.Collectors; @@ -58,8 +58,8 @@ public abstract class SerializableAction extends AbstractAction { .filter(s -> !s.equals("Store")) .collect(Collectors.joining(" ")); - if (property.getValue().isTextual()) { - var value = property.getValue().textValue(); + if (property.getValue().isString()) { + var value = property.getValue().stringValue(); var uuid = UuidHelper.parse(value); if (uuid.isPresent()) { var refName = DataStorage.get() @@ -77,7 +77,7 @@ public abstract class SerializableAction extends AbstractAction { } else if (property.getValue().isArray()) { var list = new ArrayList(); for (JsonNode jsonNode : property.getValue()) { - var s = jsonNode.asText(); + var s = jsonNode.asString(); if (!s.isEmpty()) { list.add(s); } @@ -89,7 +89,7 @@ public abstract class SerializableAction extends AbstractAction { } else if (property.getValue().isBoolean()) { map.put(name, property.getValue().booleanValue() ? "Yes" : "No"); } else { - var value = property.getValue().asText(); + var value = property.getValue().asString(); map.put(name, value); } } diff --git a/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java b/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java index 75f3f917f..c21b8fb11 100644 --- a/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java +++ b/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java @@ -1,6 +1,13 @@ package io.xpipe.app.action; +import io.xpipe.app.core.AppRestart; +import io.xpipe.app.core.window.AppDialog; +import io.xpipe.app.ext.ProcModuleProvider; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.Base64Helper; + import java.net.URI; +import java.nio.charset.StandardCharsets; public class XPipeUrlProvider implements LauncherUrlProvider { @@ -12,12 +19,32 @@ public class XPipeUrlProvider implements LauncherUrlProvider { @Override public AbstractAction createAction(URI uri) throws Exception { var a = uri.getHost(); - if (!"action".equals(a)) { + + if (a.equals("webtop")) { + ProcModuleProvider.get().openWebtopUrl(uri); return null; } - var query = uri.getQuery(); - var action = ActionUrls.parse(query); - return action.orElse(null); + if ("action".equals(a)) { + var query = uri.getQuery(); + var action = ActionUrls.parse(query); + return action.orElse(null); + } + + if ("sync".equals(a)) { + var repo = new String(Base64Helper.fromBase64UrlString(uri.getPath()), StandardCharsets.UTF_8); + var alreadySynced = AppPrefs.get().storageGitRemote().getValue() != null; + if (alreadySynced && !repo.equals(AppPrefs.get().storageGitRemote().getValue())) { + AppDialog.information("syncUrlAlreadySynced"); + return null; + } + + AppPrefs.get().setFromExternal(AppPrefs.get().storageGitRemote(), repo); + AppPrefs.get().save(); + AppRestart.restart(); + return null; + } + + return null; } } diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java index 025c4e1e1..b34ad22aa 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java @@ -1,8 +1,7 @@ package io.xpipe.app.beacon; -import io.xpipe.app.ext.ShellStore; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.beacon.BeaconClientException; +import io.xpipe.app.store.ShellStore; import lombok.Value; @@ -15,7 +14,7 @@ public class AppBeaconCache { Set shellSessions = new HashSet<>(); - public BeaconShellSession getShellSession(UUID uuid) throws BeaconClientException { + public BeaconShellSession getShellSession(UUID uuid) throws Exception { var found = shellSessions.stream() .filter(beaconShellSession -> beaconShellSession.getEntry().getUuid().equals(uuid)) @@ -23,6 +22,12 @@ public class AppBeaconCache { if (found.isEmpty()) { throw new BeaconClientException("No active shell session known for id " + uuid); } + + var sc = found.get().getControl(); + if (!sc.isRunning(true) || sc.isAnyStreamClosed()) { + sc.restart(); + } + return found.get(); } 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 c05ca6c40..4887fdcd5 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -2,15 +2,15 @@ package io.xpipe.app.beacon; import io.xpipe.app.beacon.mcp.AppMcpServer; import io.xpipe.app.core.AppLocalTemp; +import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.DocumentationLink; -import io.xpipe.beacon.BeaconConfig; -import io.xpipe.beacon.BeaconInterface; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import lombok.Getter; @@ -47,6 +47,7 @@ public class AppBeaconServer { @Getter private String localAuthSecret; + private FileChannel localLockFileChannel; private FileLock localLockFileLock; @@ -56,7 +57,9 @@ public class AppBeaconServer { public static void init() { try { - INSTANCE = new AppBeaconServer(BeaconConfig.getUsedPort()); + // We already queried the beacon port at this point, so this will always work + INSTANCE = new AppBeaconServer( + AppProperties.get().queryEffectiveBeaconPort(false).orElseThrow()); INSTANCE.initAuthSecret(); INSTANCE.start(); TrackEvent.withInfo("Started http server") @@ -111,24 +114,25 @@ public class AppBeaconServer { } private void initAuthSecret() throws IOException { - var file = BeaconConfig.getLocalBeaconAuthFile(); + var file = AppProperties.get().getBeaconAuthFile(); // Create and set temp dir permissions for Linux AppLocalTemp.getLocalTempDataDirectory(); var id = UUID.randomUUID().toString(); - Files.writeString(file, id); + Files.createFile(file); if (OsType.ofLocal() != OsType.WINDOWS) { Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----")); } + Files.writeString(file, id); localAuthSecret = id; - var lockFile = BeaconConfig.getLocalBeaconLockFile(); + var lockFile = AppProperties.get().getBeaconLockFile(); localLockFileChannel = new RandomAccessFile(lockFile.toFile(), "rw").getChannel(); localLockFileLock = localLockFileChannel.tryLock(); } private void deleteAuthSecret() { - var file = BeaconConfig.getLocalBeaconAuthFile(); + var file = AppProperties.get().getBeaconAuthFile(); try { Files.deleteIfExists(file); if (localLockFileLock != null) { @@ -137,11 +141,12 @@ public class AppBeaconServer { if (localLockFileChannel != null) { localLockFileChannel.close(); } - } catch (IOException ignored) {} + } catch (IOException ignored) { + } } private void start() throws IOException { - executor = Executors.newFixedThreadPool(5, r -> { + executor = Executors.newFixedThreadPool(3, r -> { Thread t = Executors.defaultThreadFactory().newThread(r); t.setDaemon(true); t.setName("http handler"); @@ -157,11 +162,15 @@ public class AppBeaconServer { server = HttpServer.create(new InetSocketAddress(addr, port), 10); BeaconInterface.getAll().forEach(beaconInterface -> { var handler = new BeaconRequestHandler<>(beaconInterface); - server.createContext(beaconInterface.getPath(), exchange -> { + HttpHandler httpHandler = exchange -> { if (!handleCorsHeaders(exchange)) { handler.handle(exchange); } - }); + }; + server.createContext(beaconInterface.getPath(), httpHandler); + for (String pathAlias : beaconInterface.getPathAliases()) { + server.createContext(pathAlias, httpHandler); + } }); server.setExecutor(executor); @@ -175,7 +184,7 @@ public class AppBeaconServer { if (!handleCorsHeaders(exchange)) { var mcpServer = AppMcpServer.get(); if (mcpServer != null) { - mcpServer.createHttpHandler().handle(exchange); + mcpServer.getHttpHandler().handle(exchange); } } }); @@ -186,8 +195,7 @@ public class AppBeaconServer { private boolean handleCorsHeaders(HttpExchange exchange) throws IOException { if (AppPrefs.get().enableHttpApi().get()) { - exchange.getResponseHeaders() - .add("Origin", "http://localhost:" + getPort()); + exchange.getResponseHeaders().add("Origin", "http://localhost:" + getPort()); exchange.getResponseHeaders().add("Vary", "Origin"); exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true"); diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java b/app/src/main/java/io/xpipe/app/beacon/BeaconAuthMethod.java similarity index 87% rename from beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconAuthMethod.java index 7df93e070..c05bcac5e 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconAuthMethod.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; @@ -10,7 +10,7 @@ import lombok.extern.jackson.Jacksonized; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface BeaconAuthMethod { - @JsonTypeName("Local") + @JsonTypeName("local") @Value @Builder @Jacksonized @@ -20,7 +20,7 @@ public interface BeaconAuthMethod { String authFileContent; } - @JsonTypeName("ApiKey") + @JsonTypeName("apiKey") @Value @Builder @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/app/src/main/java/io/xpipe/app/beacon/BeaconClient.java similarity index 86% rename from beacon/src/main/java/io/xpipe/beacon/BeaconClient.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconClient.java index 99c587f9b..874a21564 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconClient.java @@ -1,17 +1,18 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; -import io.xpipe.beacon.api.HandshakeExchange; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.beacon.api.HandshakeExchange; +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.util.JacksonMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.SneakyThrows; +import tools.jackson.databind.node.ObjectNode; -import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; +import java.nio.file.Path; import java.util.Optional; public class BeaconClient { @@ -23,9 +24,10 @@ public class BeaconClient { this.port = port; } - public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception { + public static BeaconClient establishConnection(int port, BeaconClientInformation information, Path authFile) + throws Exception { var client = new BeaconClient(port); - var auth = Files.readString(BeaconConfig.getLocalBeaconAuthFile()); + var auth = Files.readString(authFile); HandshakeExchange.Response response = client.performRequest(HandshakeExchange.Request.builder() .client(information) .auth(BeaconAuthMethod.Local.builder().authFileContent(auth).build()) @@ -34,19 +36,11 @@ public class BeaconClient { return client; } - public static Optional tryEstablishConnection(int port, BeaconClientInformation information) { - try { - return Optional.of(establishConnection(port, information)); - } catch (Exception ex) { - return Optional.empty(); - } - } - @SuppressWarnings("unchecked") public RES performRequest(BeaconInterface prov, String rawNode) throws BeaconConnectorException, BeaconClientException, BeaconServerException { var content = rawNode; - if (BeaconConfig.printMessages()) { + if (AppProperties.get().isPrintBeaconMessages()) { System.out.println("Sending raw request:"); System.out.println(content); } @@ -70,7 +64,7 @@ public class BeaconClient { throw new BeaconConnectorException("Couldn't send request", ex); } - if (BeaconConfig.printMessages()) { + if (AppProperties.get().isPrintBeaconMessages()) { System.out.println("Received raw response:"); System.out.println(response.body()); } @@ -94,7 +88,7 @@ public class BeaconClient { } var v = (RES) reader.readValue(body); return v; - } catch (IOException ex) { + } catch (Exception ex) { throw new BeaconConnectorException("Couldn't parse response", ex); } } @@ -117,7 +111,7 @@ public class BeaconClient { if (prov.isEmpty()) { throw new IllegalArgumentException("Unknown request class " + req.getClass()); } - if (BeaconConfig.printMessages()) { + if (AppProperties.get().isPrintBeaconMessages()) { System.out.println( "Sending request to server of type " + req.getClass().getName()); } @@ -134,7 +128,7 @@ public class BeaconClient { try { var v = JacksonMapper.getDefault().readValue(response.body(), BeaconClientErrorResponse.class); return Optional.of(v); - } catch (IOException ex) { + } catch (Exception ex) { throw new BeaconConnectorException("Couldn't parse client error message", ex); } } @@ -148,7 +142,7 @@ public class BeaconClient { try { var v = JacksonMapper.getDefault().readValue(response.body(), BeaconServerErrorResponse.class); return Optional.of(v); - } catch (IOException ex) { + } catch (Exception ex) { throw new BeaconConnectorException("Couldn't parse client error message", ex); } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java b/app/src/main/java/io/xpipe/app/beacon/BeaconClientErrorResponse.java similarity index 92% rename from beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconClientErrorResponse.java index bd2c196a2..fcc5128a8 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconClientErrorResponse.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java b/app/src/main/java/io/xpipe/app/beacon/BeaconClientException.java similarity index 87% rename from beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconClientException.java index 031edbb51..06c76d430 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconClientException.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; /** * Indicates that a client request was invalid. diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java b/app/src/main/java/io/xpipe/app/beacon/BeaconClientInformation.java similarity index 75% rename from beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconClientInformation.java index 605f5e495..0c199e80d 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconClientInformation.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; @@ -13,7 +13,7 @@ public abstract class BeaconClientInformation { public abstract String toDisplayString(); - @JsonTypeName("Cli") + @JsonTypeName("cli") @Value @Builder @Jacksonized @@ -26,7 +26,7 @@ public abstract class BeaconClientInformation { } } - @JsonTypeName("Daemon") + @JsonTypeName("daemon") @Value @Builder @Jacksonized @@ -39,7 +39,7 @@ public abstract class BeaconClientInformation { } } - @JsonTypeName("Api") + @JsonTypeName("api") @Value @Builder @Jacksonized @@ -54,4 +54,17 @@ public abstract class BeaconClientInformation { return name; } } + + @JsonTypeName("mcp") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class Mcp extends BeaconClientInformation { + + @Override + public String toDisplayString() { + return "XPipe MCP"; + } + } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java b/app/src/main/java/io/xpipe/app/beacon/BeaconConnectorException.java similarity index 92% rename from beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconConnectorException.java index 4511d0b7b..29d31cc05 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconConnectorException.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; /** * Indicates that a connection error occurred. diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java b/app/src/main/java/io/xpipe/app/beacon/BeaconInterface.java similarity index 71% rename from beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconInterface.java index 8ab336566..1b42487e4 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconInterface.java @@ -1,6 +1,6 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; -import io.xpipe.core.ModuleLayerLoader; +import io.xpipe.app.ext.ModuleLayerLoader; import com.sun.net.httpserver.HttpExchange; import lombok.SneakyThrows; @@ -31,16 +31,14 @@ public abstract class BeaconInterface { @SuppressWarnings("unchecked") @SneakyThrows public Class getRequestClass() { - var c = getClass().getSuperclass(); - var name = (c.getSuperclass().equals(BeaconInterface.class) ? c : getClass()).getName() + "$Request"; + var name = getClass().getName() + "$Request"; return (Class) Class.forName(name); } @SuppressWarnings("unchecked") @SneakyThrows public Class getResponseClass() { - var c = getClass().getSuperclass(); - var name = (c.getSuperclass().equals(BeaconInterface.class) ? c : getClass()).getName() + "$Response"; + var name = getClass().getName() + "$Response"; return (Class) Class.forName(name); } @@ -58,6 +56,10 @@ public abstract class BeaconInterface { public abstract String getPath(); + public List getPathAliases() { + return List.of(); + } + public Object handle(HttpExchange exchange, T body) throws Throwable { throw new UnsupportedOperationException(); } @@ -66,6 +68,10 @@ public abstract class BeaconInterface { return false; } + public boolean requiresBody() { + return true; + } + public boolean requiresEnabledApi() { return true; } @@ -78,17 +84,16 @@ public abstract class BeaconInterface { @Override public void init(ModuleLayer layer) { - var services = layer != null - ? ServiceLoader.load(layer, BeaconInterface.class) - : ServiceLoader.load(BeaconInterface.class); + var services = ServiceLoader.load(layer, BeaconInterface.class); ALL = services.stream() .map(ServiceLoader.Provider::get) .map(beaconInterface -> (BeaconInterface) beaconInterface) .collect(Collectors.toList()); - // Remove parent classes - ALL.removeIf(beaconInterface -> ALL.stream() - .anyMatch(other -> !other.equals(beaconInterface) - && beaconInterface.getClass().isAssignableFrom(other.getClass()))); + } + + @Override + public boolean initForCli() { + return true; } } } 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 14c885644..106e7f370 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -1,12 +1,12 @@ package io.xpipe.app.beacon; +import io.xpipe.app.core.AppProperties; 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; +import io.xpipe.app.util.JacksonMapper; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.beacon.*; -import io.xpipe.core.JacksonMapper; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -86,24 +86,38 @@ public class BeaconRequestHandler implements HttpHandler { object = createRawDataRequest(beaconInterface, read); } else { var tree = JacksonMapper.getDefault().readTree(read); - TrackEvent.trace("Parsed raw request:\n" + tree.toPrettyString()); + if (AppProperties.get().isPrintBeaconMessages()) { + TrackEvent.trace("Parsed raw request:\n" + tree.toPrettyString()); + } var emptyRequestClass = tree.isEmpty() && beaconInterface.getRequestClass().getDeclaredFields().length == 0; object = emptyRequestClass ? createDefaultRequest(beaconInterface) : JacksonMapper.getDefault().treeToValue(tree, beaconInterface.getRequestClass()); - TrackEvent.trace("Parsed request object:\n" + object); + if (AppProperties.get().isPrintBeaconMessages()) { + TrackEvent.trace("Parsed request object:\n" + object); + } } } } - var sync = beaconInterface.getSynchronizationObject(); - if (sync != null) { - synchronized (sync) { + if (beaconInterface.requiresBody() && object == null) { + writeError(exchange, new BeaconClientErrorResponse("Request body must not be empty"), 400); + return; + } + + try { + var sync = beaconInterface.getSynchronizationObject(); + if (sync != null) { + synchronized (sync) { + response = beaconInterface.handle(exchange, object); + } + } else { response = beaconInterface.handle(exchange, object); } - } else { - response = beaconInterface.handle(exchange, object); + } catch (IOException ioe) { + // Prevent IO exception from being interpreted as beacon connection issue + throw new BeaconServerException(ioe); } } catch (BeaconClientException clientException) { ErrorEventFactory.fromThrowable(clientException).omit().expected().handle(); @@ -146,9 +160,11 @@ public class BeaconRequestHandler implements HttpHandler { || !AppPrefs.get().developerMode().getValue() || !AppPrefs.get().developerShowSensitiveCommands().get(); var mapper = redact ? JacksonMapper.getRedactedSecretMapper() : JacksonMapper.getUnredactSecretMapper(); - TrackEvent.trace("Sending response:\n" + response); - TrackEvent.trace( - "Sending raw response:\n" + mapper.valueToTree(response).toPrettyString()); + if (AppProperties.get().isPrintBeaconMessages()) { + TrackEvent.trace("Sending response:\n" + response); + TrackEvent.trace("Sending raw response:\n" + + mapper.valueToTree(response).toPrettyString()); + } var bytes = JacksonMapper.getDefault() .valueToTree(response) .toPrettyString() diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/BeaconServer.java similarity index 94% rename from beacon/src/main/java/io/xpipe/beacon/BeaconServer.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconServer.java index 4bf21a641..e6d0784a7 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconServer.java @@ -1,6 +1,6 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; -import io.xpipe.beacon.api.DaemonStopExchange; +import io.xpipe.app.beacon.api.DaemonStopExchange; import lombok.SneakyThrows; diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java b/app/src/main/java/io/xpipe/app/beacon/BeaconServerErrorResponse.java similarity index 95% rename from beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconServerErrorResponse.java index 7d61954a5..c9917bc05 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconServerErrorResponse.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java b/app/src/main/java/io/xpipe/app/beacon/BeaconServerException.java similarity index 93% rename from beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java rename to app/src/main/java/io/xpipe/app/beacon/BeaconServerException.java index 227b74899..959fe7d91 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconServerException.java @@ -1,4 +1,4 @@ -package io.xpipe.beacon; +package io.xpipe.app.beacon; /** * Indicates that an internal server error occurred. diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java b/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java index 69c549147..907da05dd 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java @@ -1,7 +1,5 @@ package io.xpipe.app.beacon; -import io.xpipe.beacon.BeaconClientInformation; - import lombok.Value; @Value 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 e5c2acd1c..ddc3e318e 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java +++ b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java @@ -2,7 +2,6 @@ package io.xpipe.app.beacon; import io.xpipe.app.core.AppLocalTemp; import io.xpipe.app.issue.ErrorEventFactory; -import io.xpipe.beacon.BeaconClientException; import org.apache.commons.io.FileUtils; @@ -54,6 +53,14 @@ public class BlobManager { return file; } + public long getSize(UUID id) throws IOException { + if (memoryBlobs.containsKey(id)) { + return memoryBlobs.get(id).length; + } else { + return Files.size(fileBlobs.get(id)); + } + } + public void store(UUID uuid, byte[] blob) { memoryBlobs.put(uuid, blob); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ActionExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/ActionExchange.java similarity index 60% rename from app/src/main/java/io/xpipe/app/beacon/impl/ActionExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/ActionExchange.java index 913ace5d9..64cb6d942 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ActionExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/ActionExchange.java @@ -1,14 +1,24 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; import io.xpipe.app.action.ActionJacksonMapper; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.ActionExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import tools.jackson.databind.JsonNode; -public class ActionExchangeImpl extends ActionExchange { +public class ActionExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/action"; + } @Override public Object handle(HttpExchange exchange, Request msg) throws Exception { @@ -37,4 +47,19 @@ public class ActionExchangeImpl extends ActionExchange { } return r; } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + JsonNode action; + + boolean confirm; + } + + @Jacksonized + @Builder + @Value + public static class Response {} } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/AskpassExchange.java similarity index 80% rename from app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/AskpassExchange.java index d2b55e143..30d210a21 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/AskpassExchange.java @@ -1,25 +1,42 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.secret.InPlaceSecretValue; 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; -import io.xpipe.beacon.api.AskpassExchange; -import io.xpipe.core.InPlaceSecretValue; +import io.xpipe.app.util.AskpassAlert; +import io.xpipe.app.util.GlobalTimer; +import io.xpipe.app.util.SecretValue; import javafx.beans.property.SimpleStringProperty; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.time.Duration; +import java.util.UUID; -public class AskpassExchangeImpl extends AskpassExchange { +public class AskpassExchange extends BeaconInterface { + + @Override + public boolean acceptInShutdown() { + return true; + } + + @Override + public String getPath() { + return "/askpass"; + } @Override public boolean requiresCompletedStartup() { @@ -106,4 +123,25 @@ public class AskpassExchangeImpl extends AskpassExchange { } TerminalView.focus(term.get()); } + + @Jacksonized + @Builder + @Value + public static class Request { + long pid; + + UUID secretId; + + UUID request; + + String prompt; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + SecretValue value; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/CategoryAddExchange.java similarity index 61% rename from app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/CategoryAddExchange.java index 7437ea490..b89596098 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/CategoryAddExchange.java @@ -1,13 +1,24 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.CategoryAddExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public class CategoryAddExchangeImpl extends CategoryAddExchange { +import java.util.UUID; + +public class CategoryAddExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/category/add"; + } @Override public Object handle(HttpExchange exchange, Request msg) throws Throwable { @@ -32,4 +43,23 @@ public class CategoryAddExchangeImpl extends CategoryAddExchange { public Object getSynchronizationObject() { return DataStorage.get(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String name; + + @NonNull + UUID parent; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + UUID category; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryInfoExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/CategoryInfoExchange.java similarity index 50% rename from app/src/main/java/io/xpipe/app/beacon/impl/CategoryInfoExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/CategoryInfoExchange.java index b9ca1e79e..6c2a1a6ed 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryInfoExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/CategoryInfoExchange.java @@ -1,16 +1,29 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.CategoryInfoExchange; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.util.JacksonMapper; +import io.xpipe.app.util.StorePath; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import tools.jackson.databind.JsonNode; +import java.time.Instant; import java.util.ArrayList; +import java.util.List; import java.util.UUID; -public class CategoryInfoExchangeImpl extends CategoryInfoExchange { +public class CategoryInfoExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/category/info"; + } @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { @@ -39,4 +52,42 @@ public class CategoryInfoExchangeImpl extends CategoryInfoExchange { public Object getSynchronizationObject() { return DataStorage.get(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + List categories; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List<@NonNull InfoResponse> infos; + } + + @Jacksonized + @Builder + @Value + public static class InfoResponse { + @NonNull + UUID category; + + UUID parentCategory; + + @NonNull + StorePath name; + + @NonNull + Instant lastUsed; + + @NonNull + Instant lastModified; + + @NonNull + JsonNode config; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/CategoryQueryExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/CategoryQueryExchange.java new file mode 100644 index 000000000..6da752982 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/CategoryQueryExchange.java @@ -0,0 +1,51 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStorageQuery; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.UUID; + +public class CategoryQueryExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/category/query"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) { + var found = DataStorageQuery.queryCategory(msg.getFilter()); + return Response.builder() + .found(found.stream().map(entry -> entry.getUuid()).toList()) + .build(); + } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String filter; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List<@NonNull UUID> found; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryRemoveExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/CategoryRemoveExchange.java similarity index 61% rename from app/src/main/java/io/xpipe/app/beacon/impl/CategoryRemoveExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/CategoryRemoveExchange.java index 3085fad4f..f4d0325d6 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryRemoveExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/CategoryRemoveExchange.java @@ -1,16 +1,26 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.CategoryRemoveExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.util.ArrayList; +import java.util.List; import java.util.UUID; -public class CategoryRemoveExchangeImpl extends CategoryRemoveExchange { +public class CategoryRemoveExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/category/remove"; + } @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { @@ -38,4 +48,21 @@ public class CategoryRemoveExchangeImpl extends CategoryRemoveExchange { public Object getSynchronizationObject() { return DataStorage.get(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + List categories; + + boolean removeChildrenCategories; + + boolean removeContents; + } + + @Jacksonized + @Builder + @Value + public static class Response {} } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonFocusExchange.java similarity index 58% rename from app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/DaemonFocusExchange.java index bb454b355..842239034 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonFocusExchange.java @@ -1,12 +1,25 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.core.mode.AppOperationMode; import io.xpipe.app.core.window.AppMainWindow; -import io.xpipe.beacon.api.DaemonFocusExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public class DaemonFocusExchangeImpl extends DaemonFocusExchange { +public class DaemonFocusExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/focus"; + } + + @Override + public boolean requiresBody() { + return false; + } @Override public Object handle(HttpExchange exchange, Request msg) throws Throwable { @@ -34,4 +47,14 @@ public class DaemonFocusExchangeImpl extends DaemonFocusExchange { public boolean requiresCompletedStartup() { return false; } + + @Jacksonized + @Builder + @Value + public static class Request {} + + @Jacksonized + @Builder + @Value + public static class Response {} } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonModeExchange.java similarity index 53% rename from app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/DaemonModeExchange.java index 341354f54..0ec2cad14 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonModeExchange.java @@ -1,12 +1,23 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.core.mode.AppOperationMode; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.DaemonModeExchange; +import io.xpipe.app.util.XPipeDaemonMode; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class DaemonModeExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/mode"; + } -public class DaemonModeExchangeImpl extends DaemonModeExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { var mode = AppOperationMode.map(msg.getMode()); @@ -22,11 +33,27 @@ public class DaemonModeExchangeImpl extends DaemonModeExchange { } AppOperationMode.switchToSyncIfPossible(mode); - return DaemonModeExchange.Response.builder().usedMode(msg.getMode()).build(); + return Response.builder().usedMode(msg.getMode()).build(); } @Override public boolean requiresEnabledApi() { return false; } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + XPipeDaemonMode mode; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + XPipeDaemonMode usedMode; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java similarity index 52% rename from app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java index 207421e52..75bb6d8c9 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java @@ -1,18 +1,32 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.beacon.BeaconServerException; import io.xpipe.app.core.AppOpenArguments; +import io.xpipe.app.core.AppProperties; 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; +import io.xpipe.app.util.OsType; +import io.xpipe.app.util.ThreadHelper; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public class DaemonOpenExchangeImpl extends DaemonOpenExchange { +import java.nio.file.Path; +import java.util.List; + +public class DaemonOpenExchange extends BeaconInterface { private int openCounter = 0; + @Override + public String getPath() { + return "/daemon/open"; + } + @Override public boolean requiresCompletedStartup() { return false; @@ -20,6 +34,14 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange { @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException { + if (msg.getDirectory() != null && !AppProperties.get().getDataDir().equals(msg.getDirectory())) { + ThreadHelper.runAsync(() -> { + ThreadHelper.sleep(1000); + AppOperationMode.shutdown(false); + }); + return Response.builder().restartRequired(true).build(); + } + if (msg.getArguments().isEmpty()) { try { // At this point we are already loading this on another thread @@ -46,4 +68,22 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange { public boolean requiresEnabledApi() { return false; } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + List arguments; + + Path directory; + } + + @Jacksonized + @Builder + @Value + public static class Response { + + boolean restartRequired; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/DaemonStatusExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonStatusExchange.java new file mode 100644 index 000000000..f4f6d9253 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonStatusExchange.java @@ -0,0 +1,56 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.core.mode.AppOperationMode; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class DaemonStatusExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/status"; + } + + @Override + public boolean requiresCompletedStartup() { + return false; + } + + @Override + public boolean requiresBody() { + return false; + } + + @Override + public Object handle(HttpExchange exchange, Request body) { + String mode; + if (AppOperationMode.get() == null) { + mode = "none"; + } else { + mode = AppOperationMode.get().getId(); + } + + return Response.builder().mode(mode).build(); + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Value + @Jacksonized + @Builder + public static class Request {} + + @Jacksonized + @Builder + @Value + public static class Response { + String mode; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/DaemonStopExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonStopExchange.java new file mode 100644 index 000000000..e0f456550 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonStopExchange.java @@ -0,0 +1,57 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.core.mode.AppOperationMode; +import io.xpipe.app.util.ThreadHelper; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * Requests the daemon to stop. + */ +public class DaemonStopExchange extends BeaconInterface { + + @Override + public boolean requiresBody() { + return false; + } + + @Override + public String getPath() { + return "/daemon/stop"; + } + + @Override + public boolean requiresCompletedStartup() { + return false; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) { + ThreadHelper.runAsync(() -> { + ThreadHelper.sleep(1000); + AppOperationMode.shutdown(false); + }); + return Response.builder().success(true).build(); + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request {} + + @Jacksonized + @Builder + @Value + public static class Response { + boolean success; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonVersionExchange.java similarity index 56% rename from app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/DaemonVersionExchange.java index b1c6764bd..e04e594dd 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonVersionExchange.java @@ -1,19 +1,33 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppVersion; import io.xpipe.app.util.LicenseProvider; -import io.xpipe.beacon.api.DaemonVersionExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public class DaemonVersionExchangeImpl extends DaemonVersionExchange { +public class DaemonVersionExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/version"; + } @Override public boolean requiresCompletedStartup() { return false; } + @Override + public boolean requiresBody() { + return false; + } + @Override public Object handle(HttpExchange exchange, Request msg) { var jvmVersion = System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name") + " (" @@ -34,4 +48,30 @@ public class DaemonVersionExchangeImpl extends DaemonVersionExchange { public boolean requiresEnabledApi() { return false; } + + @Jacksonized + @Builder + @Value + public static class Request {} + + @Jacksonized + @Builder + @Value + public static class Response { + + @NonNull + String version; + + @NonNull + String canonicalVersion; + + @NonNull + String buildVersion; + + @NonNull + String jvmVersion; + + @NonNull + String plan; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/FsBlobExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/FsBlobExchange.java new file mode 100644 index 000000000..8483bf9e6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/FsBlobExchange.java @@ -0,0 +1,53 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.beacon.BlobManager; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class FsBlobExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/blob"; + } + + @Override + public boolean readRawRequestBody() { + return true; + } + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) { + var id = UUID.randomUUID(); + + var size = exchange.getRequestBody().available(); + if (size > 100_000_000) { + BlobManager.get().store(id, exchange.getRequestBody()); + } else { + BlobManager.get().store(id, exchange.getRequestBody().readAllBytes()); + } + return Response.builder().blob(id).build(); + } + + @Jacksonized + @Builder + @Value + public static class Request {} + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + UUID blob; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsReadExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/FsReadExchange.java similarity index 63% rename from app/src/main/java/io/xpipe/app/beacon/impl/FsReadExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/FsReadExchange.java index b3db97e8b..4503387a8 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsReadExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/FsReadExchange.java @@ -1,29 +1,44 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.beacon.BlobManager; -import io.xpipe.app.ext.ConnectionFileSystem; +import io.xpipe.app.fs.ShellFileSystem; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.FixedSizeInputStream; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.FsReadExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.io.BufferedInputStream; import java.io.OutputStream; import java.nio.file.Files; +import java.util.UUID; -public class FsReadExchangeImpl extends FsReadExchange { +public class FsReadExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/read"; + } @Override @SneakyThrows public Object handle(HttpExchange exchange, Request msg) { - var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); - var fs = new ConnectionFileSystem(shell.getControl()); + var shell = AppBeaconServer.get().getCache().getShellSession(msg.getStore()); + var fs = new ShellFileSystem(shell.getControl()); + + if (!msg.getPath().isAbsolute()) { + throw new BeaconClientException("File path " + msg.getPath() + " is not absolute"); + } if (!fs.fileExists(msg.getPath())) { - throw new BeaconClientException("File does not exist"); + throw new BeaconClientException("File " + msg.getPath() + " does not exist"); } var size = fs.getFileSize(msg.getPath()); @@ -56,4 +71,20 @@ public class FsReadExchangeImpl extends FsReadExchange { } return Response.builder().build(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + + @NonNull + FilePath path; + } + + @Jacksonized + @Builder + @Value + public static class Response {} } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/FsScriptExchange.java similarity index 50% rename from app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/FsScriptExchange.java index eb46b4edf..34fe30b22 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/FsScriptExchange.java @@ -1,27 +1,56 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.beacon.BlobManager; import io.xpipe.app.process.ScriptHelper; -import io.xpipe.beacon.api.FsScriptExchange; +import io.xpipe.app.util.FilePath; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.nio.charset.StandardCharsets; +import java.util.UUID; -public class FsScriptExchangeImpl extends FsScriptExchange { +public class FsScriptExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/script"; + } @Override @SneakyThrows public Object handle(HttpExchange exchange, Request msg) { - var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); + var shell = AppBeaconServer.get().getCache().getShellSession(msg.getStore()); String data; try (var in = BlobManager.get().getBlob(msg.getBlob())) { data = new String(in.readAllBytes(), StandardCharsets.UTF_8); } - data = shell.getControl().getShellDialect().prepareScriptContent(shell.getControl(), data); var file = ScriptHelper.createExecScript(shell.getControl(), data); return Response.builder().path(file).build(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + + @NonNull + UUID blob; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + FilePath path; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/FsWriteExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/FsWriteExchange.java new file mode 100644 index 000000000..5a342dc34 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/FsWriteExchange.java @@ -0,0 +1,65 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.beacon.BlobManager; +import io.xpipe.app.fs.ShellFileSystem; +import io.xpipe.app.util.FilePath; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class FsWriteExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/fs/write"; + } + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) { + var shell = AppBeaconServer.get().getCache().getShellSession(msg.getStore()); + var fs = new ShellFileSystem(shell.getControl()); + + if (!msg.getPath().isAbsolute()) { + throw new BeaconClientException("File path " + msg.getPath() + " is not absolute"); + } + + if (!fs.directoryExists(msg.getPath().getParent())) { + throw new BeaconClientException("Directory " + msg.getPath().getParent() + " does not exist"); + } + + try (var in = BlobManager.get().getBlob(msg.getBlob()); + var os = fs.openOutput(msg.getPath(), BlobManager.get().getSize(msg.getBlob()))) { + in.transferTo(os); + } + return Response.builder().build(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + + @NonNull + UUID blob; + + @NonNull + FilePath path; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/HandshakeExchange.java similarity index 59% rename from app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/HandshakeExchange.java index 629ea5370..1244596d6 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/HandshakeExchange.java @@ -1,18 +1,33 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; -import io.xpipe.app.beacon.AppBeaconServer; -import io.xpipe.app.beacon.BeaconSession; +import io.xpipe.app.beacon.*; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.beacon.BeaconAuthMethod; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.HandshakeExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.util.UUID; -public class HandshakeExchangeImpl extends HandshakeExchange { +public class HandshakeExchange extends BeaconInterface { + + @Override + public boolean acceptInShutdown() { + return true; + } + + @Override + public boolean requiresAuthentication() { + return false; + } + + @Override + public String getPath() { + return "/handshake"; + } @Override public boolean requiresCompletedStartup() { @@ -39,7 +54,7 @@ public class HandshakeExchangeImpl extends HandshakeExchange { return false; } - private boolean checkAuth(BeaconAuthMethod authMethod) { + private boolean checkAuth(io.xpipe.app.beacon.BeaconAuthMethod authMethod) { if (authMethod instanceof BeaconAuthMethod.Local local) { var c = local.getAuthFileContent().strip(); return AppBeaconServer.get().getLocalAuthSecret().equals(c); @@ -52,4 +67,23 @@ public class HandshakeExchangeImpl extends HandshakeExchange { return false; } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + BeaconAuthMethod auth; + + @NonNull + BeaconClientInformation client; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + String sessionToken; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/SecretDecryptExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/SecretDecryptExchange.java new file mode 100644 index 000000000..38154b2d5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/SecretDecryptExchange.java @@ -0,0 +1,48 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.secret.MultiPrincipalSecret; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import tools.jackson.databind.JsonNode; + +public class SecretDecryptExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/secret/decrypt"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { + var secret = MultiPrincipalSecret.deserialize(msg.getEncrypted()); + if (secret == null || secret.getInternalSecret() == null) { + throw new BeaconClientException("Unable to parse or decrypt secret"); + } + + return Response.builder() + .decrypted(new String(secret.getInternalSecret().getSecret())) + .build(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + JsonNode encrypted; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + String decrypted; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/SecretEncryptExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/SecretEncryptExchange.java new file mode 100644 index 000000000..94fbcbea8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/SecretEncryptExchange.java @@ -0,0 +1,88 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.secret.DataStorageAccessHandler; +import io.xpipe.app.secret.EncryptionPrincipal; +import io.xpipe.app.secret.InPlaceSecretValue; +import io.xpipe.app.secret.MultiPrincipalSecret; +import io.xpipe.app.util.UuidHelper; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import tools.jackson.databind.JsonNode; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +public class SecretEncryptExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/secret/encrypt"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { + Set resolvedPrincipals = new LinkedHashSet<>(); + if (msg.getPrincipals() != null) { + EncryptionPrincipal p; + for (String pr : msg.getPrincipals()) { + var byName = DataStorageAccessHandler.getInstance().getAllEncryptionPrincipals().stream() + .filter(encryptionPrincipal -> + encryptionPrincipal.getName().equals(pr)) + .findFirst(); + if (byName.isPresent()) { + p = byName.get(); + } else { + var uuid = UuidHelper.parse(pr); + if (uuid.isPresent()) { + var principal = DataStorageAccessHandler.getInstance().getEncryptionPrincipal(uuid.get()); + p = principal.orElse(null); + } else { + p = null; + } + } + + if (p == null) { + throw new BeaconClientException("Unknown principal " + pr); + } + + if (!p.isAccessible()) { + throw new BeaconClientException("Principal " + p.getName() + " is not accessible"); + } + + resolvedPrincipals.add(p); + } + } + + if (resolvedPrincipals.isEmpty()) { + resolvedPrincipals.add(DataStorageAccessHandler.getInstance().getEncryptAllPrincipal()); + } + + var secret = MultiPrincipalSecret.of(InPlaceSecretValue.of(msg.getValue()), resolvedPrincipals); + return Response.builder().encrypted(secret.serialize()).build(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String value; + + List principals; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + JsonNode encrypted; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/ShellExecExchange.java similarity index 55% rename from app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/ShellExecExchange.java index 738fe60fe..c41415e17 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/ShellExecExchange.java @@ -1,19 +1,29 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; import io.xpipe.app.beacon.AppBeaconServer; -import io.xpipe.beacon.api.ShellExecExchange; +import io.xpipe.app.beacon.BeaconInterface; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; -public class ShellExecExchangeImpl extends ShellExecExchange { +public class ShellExecExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/exec"; + } @Override @SneakyThrows public Object handle(HttpExchange exchange, Request msg) { - var existing = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); + var existing = AppBeaconServer.get().getCache().getShellSession(msg.getStore()); AtomicReference out = new AtomicReference<>(); AtomicReference err = new AtomicReference<>(); long exitCode; @@ -30,4 +40,28 @@ public class ShellExecExchangeImpl extends ShellExecExchange { .exitCode(exitCode) .build(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + + @NonNull + String command; + } + + @Jacksonized + @Builder + @Value + public static class Response { + long exitCode; + + @NonNull + String stdout; + + @NonNull + String stderr; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/ShellStartExchange.java similarity index 61% rename from app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/ShellStartExchange.java index 548950b1a..45470b4b0 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/ShellStartExchange.java @@ -1,23 +1,36 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.beacon.BeaconShellSession; -import io.xpipe.app.ext.ShellStore; import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.ShellStartExchange; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.store.ShellStore; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.JacksonMapper; +import io.xpipe.app.util.OsType; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; -public class ShellStartExchangeImpl extends ShellStartExchange { +import java.util.UUID; + +public class ShellStartExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/start"; + } @Override @SneakyThrows public Object handle(HttpExchange exchange, Request msg) { var e = DataStorage.get() - .getStoreEntryIfPresent(msg.getConnection()) + .getStoreEntryIfPresent(msg.getStore()) .orElseThrow(() -> new BeaconClientException("Unknown connection")); if (!(e.getStore() instanceof ShellStore s)) { throw new BeaconClientException("Not a shell connection"); @@ -42,7 +55,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange { AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(e, control)); } var ttyState = - JacksonMapper.getDefault().valueToTree(control.getTtyState()).asText(); + JacksonMapper.getDefault().valueToTree(control.getTtyState()).asString(); return Response.builder() .shellDialect(control.getShellDialect().getId()) .osType(control.getOsType()) @@ -51,4 +64,32 @@ public class ShellStartExchangeImpl extends ShellStartExchange { .ttyState(ttyState) .build(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + String shellDialect; + + @NonNull + OsType.Any osType; + + @NonNull + String osName; + + @NonNull + String ttyState; + + @NonNull + FilePath temp; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/ShellStopExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/ShellStopExchange.java new file mode 100644 index 000000000..638dda1f2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/ShellStopExchange.java @@ -0,0 +1,43 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconInterface; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class ShellStopExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/stop"; + } + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) { + var e = AppBeaconServer.get().getCache().getShellSession(msg.getStore()); + e.getControl().close(); + AppBeaconServer.get().getCache().getShellSessions().remove(e); + return Response.builder().build(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/SshLaunchExchange.java similarity index 66% rename from app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/SshLaunchExchange.java index 3e1472e95..02d21048a 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/SshLaunchExchange.java @@ -1,15 +1,24 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; -import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.ext.ProcModuleProvider; import io.xpipe.app.process.ShellDialects; import io.xpipe.app.terminal.TerminalLauncherManager; -import io.xpipe.beacon.api.SshLaunchExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.util.List; -public class SshLaunchExchangeImpl extends SshLaunchExchange { +public class SshLaunchExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/sshLaunch"; + } @Override public Object handle(HttpExchange exchange, Request msg) throws Exception { @@ -29,7 +38,7 @@ public class SshLaunchExchangeImpl extends SshLaunchExchange { // There are sometimes multiple requests by a terminal client (e.g. Termius) // This might fail sometimes, but it is expected var r = TerminalLauncherManager.sshLaunchExchange(); - var c = ProcessControlProvider.get() + var c = ProcModuleProvider.get() .getEffectiveLocalDialect() .getOpenScriptCommand(r.toString()) .buildBaseParts(null); @@ -40,4 +49,19 @@ public class SshLaunchExchangeImpl extends SshLaunchExchange { public boolean requiresEnabledApi() { return false; } + + @Jacksonized + @Builder + @Value + public static class Request { + String arguments; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List command; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/StoreAddExchange.java similarity index 68% rename from app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/StoreAddExchange.java index 6c0ea16e1..9cb6aa1b7 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/StoreAddExchange.java @@ -1,17 +1,35 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; -import io.xpipe.app.ext.DataStore; -import io.xpipe.app.ext.ValidationException; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.ConnectionAddExchange; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.store.DataStore; +import io.xpipe.app.util.JacksonMapper; +import io.xpipe.app.util.ValidationException; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import tools.jackson.databind.JsonNode; -public class ConnectionAddExchangeImpl extends ConnectionAddExchange { +import java.util.List; +import java.util.UUID; + +public class StoreAddExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/store/add"; + } + + @Override + public List getPathAliases() { + return List.of("/connection/add"); + } @Override public Object handle(HttpExchange exchange, Request msg) throws Throwable { @@ -22,7 +40,7 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange { var foundStore = DataStorage.get().getStoreEntryIfPresent(store, false); if (foundStore.isPresent()) { - return Response.builder().connection(foundStore.get().getUuid()).build(); + return Response.builder().store(foundStore.get().getUuid()).build(); } var foundName = DataStorage.get().getStoreEntryIfPresent(msg.getName()); @@ -31,7 +49,7 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange { // Only allow updates for the same type of store if (foundNameStore != null && foundNameStore.getClass().equals(store.getClass())) { DataStorage.get().updateEntryStore(foundName.get(), store); - return Response.builder().connection(foundName.get().getUuid()).build(); + return Response.builder().store(foundName.get().getUuid()).build(); } } @@ -76,11 +94,35 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange { .orElseThrow()); } - return Response.builder().connection(entry.getUuid()).build(); + return Response.builder().store(entry.getUuid()).build(); } @Override public Object getSynchronizationObject() { return DataStorage.get(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String name; + + @NonNull + JsonNode data; + + @NonNull + Boolean validate; + + UUID category; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + UUID store; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/StoreInfoExchange.java similarity index 58% rename from app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/StoreInfoExchange.java index 2a2ac542f..52e354109 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/StoreInfoExchange.java @@ -1,26 +1,43 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.ConnectionInfoExchange; -import io.xpipe.core.StorePath; +import io.xpipe.app.util.StorePath; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import org.apache.commons.lang3.ClassUtils; +import java.time.Instant; import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; -public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange { +public class StoreInfoExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/store/info"; + } + + @Override + public List getPathAliases() { + return List.of("/connection/info"); + } @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { var list = new ArrayList(); - for (UUID uuid : msg.getConnections()) { + for (UUID uuid : msg.getStores()) { var e = DataStorage.get() .getStoreEntryIfPresent(uuid) - .orElseThrow(() -> new BeaconClientException("Unknown connection: " + uuid)); + .orElseThrow(() -> new BeaconClientException("Unknown store: " + uuid)); var names = DataStorage.get() .getStorePath(DataStorage.get() @@ -42,7 +59,8 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange { var apply = InfoResponse.builder() .lastModified(e.getLastModified()) .lastUsed(e.getLastUsed()) - .connection(e.getUuid()) + .created(e.getCreated()) + .store(e.getUuid()) .category(cat) .name(DataStorage.get().getStorePath(e)) .rawData(e.getStore()) @@ -60,4 +78,58 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange { public Object getSynchronizationObject() { return DataStorage.get(); } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + List stores; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List<@NonNull InfoResponse> infos; + } + + @Jacksonized + @Builder + @Value + public static class InfoResponse { + @NonNull + UUID store; + + @NonNull + StorePath category; + + @NonNull + StorePath name; + + @NonNull + String type; + + @NonNull + Object rawData; + + @NonNull + Object usageCategory; + + @NonNull + Instant lastUsed; + + @NonNull + Instant lastModified; + + @NonNull + Instant created; + + @NonNull + Object state; + + @NonNull + Map cache; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/StoreQueryExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/StoreQueryExchange.java new file mode 100644 index 000000000..30c28a01b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/StoreQueryExchange.java @@ -0,0 +1,62 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStorageQuery; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.UUID; + +public class StoreQueryExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/store/query"; + } + + @Override + public List getPathAliases() { + return List.of("/connection/query"); + } + + @Override + public Object handle(HttpExchange exchange, Request msg) { + var found = DataStorageQuery.queryEntry(msg.getCategoryFilter(), msg.getStoreFilter(), msg.getTypeFilter()); + return Response.builder() + .found(found.stream().map(entry -> entry.getUuid()).toList()) + .build(); + } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String categoryFilter; + + @NonNull + String storeFilter; + + @NonNull + String typeFilter; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List<@NonNull UUID> found; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/StoreRefreshExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/StoreRefreshExchange.java new file mode 100644 index 000000000..6e56822c8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/StoreRefreshExchange.java @@ -0,0 +1,59 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.store.FixedHierarchyStore; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.UUID; + +public class StoreRefreshExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/store/refresh"; + } + + @Override + public List getPathAliases() { + return List.of("/connection/refresh"); + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws Throwable { + var e = DataStorage.get() + .getStoreEntryIfPresent(msg.getStore()) + .orElseThrow(() -> new BeaconClientException("Unknown store: " + msg.getStore())); + if (e.getStore() instanceof FixedHierarchyStore) { + DataStorage.get().refreshChildren(e, true); + } else { + e.validateOrThrow(); + } + return Response.builder().build(); + } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID store; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/StoreRemoveExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/StoreRemoveExchange.java new file mode 100644 index 000000000..a867d0c9e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/StoreRemoveExchange.java @@ -0,0 +1,60 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class StoreRemoveExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/store/remove"; + } + + @Override + public List getPathAliases() { + return List.of("/connection/remove"); + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { + var entries = new ArrayList(); + for (UUID uuid : msg.getStores()) { + var e = DataStorage.get() + .getStoreEntryIfPresent(uuid) + .orElseThrow(() -> new BeaconClientException("Unknown store: " + uuid)); + entries.add(e); + } + DataStorage.get().deleteWithChildren(entries.toArray(DataStoreEntry[]::new)); + return Response.builder().build(); + } + + @Override + public Object getSynchronizationObject() { + return DataStorage.get(); + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + List stores; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/api/TerminalExternalLaunchExchange.java similarity index 67% rename from app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/api/TerminalExternalLaunchExchange.java index 8c2c8b7c4..26422fbac 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/TerminalExternalLaunchExchange.java @@ -1,21 +1,30 @@ -package io.xpipe.app.beacon.impl; +package io.xpipe.app.beacon.api; +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.beacon.BeaconServerException; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.app.ext.ShellStore; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageQuery; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.store.ShellStore; import io.xpipe.app.terminal.TerminalLauncherManager; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.TerminalExternalLaunchExchange; import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; import java.util.List; -public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange { +public class TerminalExternalLaunchExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/terminal/externalLaunch"; + } @Override public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException { @@ -25,7 +34,7 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx } if (found.size() > 1) { - throw new BeaconClientException("Multiple connections found: " + throw new BeaconClientException("Multiple stores found: " + found.stream().map(DataStoreEntry::getName).toList()); } @@ -66,4 +75,23 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx } return r; } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String connection; + + @NonNull + List arguments; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List command; + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/api/TerminalLaunchExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/TerminalLaunchExchange.java new file mode 100644 index 000000000..226366623 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/TerminalLaunchExchange.java @@ -0,0 +1,50 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.beacon.BeaconServerException; +import io.xpipe.app.terminal.TerminalLauncherManager; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.nio.file.Path; +import java.util.UUID; + +public class TerminalLaunchExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/terminal/launch"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException { + var r = TerminalLauncherManager.launchExchange(msg.getRequest()); + return Response.builder().targetFile(r).build(); + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID request; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + Path targetFile; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/TerminalPrepareExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/TerminalPrepareExchange.java new file mode 100644 index 000000000..79ebb4e3d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/TerminalPrepareExchange.java @@ -0,0 +1,54 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.prefs.AppPrefs; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class TerminalPrepareExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/terminal/prepare"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) { + var term = AppPrefs.get().terminalType().getValue(); + var unicode = term.supportsUnicode(); + var escapes = term.supportsEscapes(); + return Response.builder() + .supportsUnicode(unicode) + .supportsEscapeSequences(escapes) + .build(); + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID request; + + long pid; + } + + @Jacksonized + @Builder + @Value + public static class Response { + boolean supportsUnicode; + boolean supportsEscapeSequences; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/TerminalRegisterExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/TerminalRegisterExchange.java new file mode 100644 index 000000000..2a013ed95 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/TerminalRegisterExchange.java @@ -0,0 +1,49 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconClientException; +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.terminal.TerminalLauncherManager; +import io.xpipe.app.terminal.TerminalView; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class TerminalRegisterExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/terminal/register"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { + TerminalView.get().open(msg.getRequest(), msg.getPid()); + TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid()); + return Response.builder().build(); + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID request; + + long pid; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/api/TerminalWaitExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/TerminalWaitExchange.java new file mode 100644 index 000000000..a55f550de --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/api/TerminalWaitExchange.java @@ -0,0 +1,45 @@ +package io.xpipe.app.beacon.api; + +import io.xpipe.app.beacon.BeaconInterface; +import io.xpipe.app.beacon.BeaconServerException; +import io.xpipe.app.terminal.TerminalLauncherManager; + +import com.sun.net.httpserver.HttpExchange; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class TerminalWaitExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/terminal/wait"; + } + + @Override + public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException { + TerminalLauncherManager.waitExchange(msg.getRequest()); + return Response.builder().build(); + } + + @Override + public boolean requiresEnabledApi() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID request; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryQueryExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/CategoryQueryExchangeImpl.java deleted file mode 100644 index f79dd0de4..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/CategoryQueryExchangeImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.storage.DataStorageQuery; -import io.xpipe.beacon.api.CategoryQueryExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class CategoryQueryExchangeImpl extends CategoryQueryExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) { - var found = DataStorageQuery.queryCategory(msg.getFilter()); - return Response.builder() - .found(found.stream().map(entry -> entry.getUuid()).toList()) - .build(); - } - - @Override - public Object getSynchronizationObject() { - return DataStorage.get(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java deleted file mode 100644 index 37dfdc5f1..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.storage.DataStorageQuery; -import io.xpipe.beacon.api.ConnectionQueryExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) { - var found = - DataStorageQuery.queryEntry(msg.getCategoryFilter(), msg.getConnectionFilter(), msg.getTypeFilter()); - return Response.builder() - .found(found.stream().map(entry -> entry.getUuid()).toList()) - .build(); - } - - @Override - public Object getSynchronizationObject() { - return DataStorage.get(); - } -} 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 deleted file mode 100644 index 4fabec9c8..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.ext.FixedHierarchyStore; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.ConnectionRefreshExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) throws Throwable { - var e = DataStorage.get() - .getStoreEntryIfPresent(msg.getConnection()) - .orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection())); - if (e.getStore() instanceof FixedHierarchyStore) { - DataStorage.get().refreshChildren(e, true); - } else { - e.validateOrThrow(); - } - return Response.builder().build(); - } - - @Override - public Object getSynchronizationObject() { - return DataStorage.get(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java deleted file mode 100644 index 1fb6d4176..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.ConnectionRemoveExchange; - -import com.sun.net.httpserver.HttpExchange; - -import java.util.ArrayList; -import java.util.UUID; - -public class ConnectionRemoveExchangeImpl extends ConnectionRemoveExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { - var entries = new ArrayList(); - for (UUID uuid : msg.getConnections()) { - var e = DataStorage.get() - .getStoreEntryIfPresent(uuid) - .orElseThrow(() -> new BeaconClientException("Unknown connection: " + uuid)); - entries.add(e); - } - DataStorage.get().deleteWithChildren(entries.toArray(DataStoreEntry[]::new)); - return Response.builder().build(); - } - - @Override - public Object getSynchronizationObject() { - return DataStorage.get(); - } -} 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 deleted file mode 100644 index 74854d1d3..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.core.mode.AppOperationMode; -import io.xpipe.beacon.api.DaemonStatusExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class DaemonStatusExchangeImpl extends DaemonStatusExchange { - - @Override - public boolean requiresCompletedStartup() { - return false; - } - - @Override - public Object handle(HttpExchange exchange, Request body) { - String mode; - if (AppOperationMode.get() == null) { - mode = "none"; - } else { - mode = AppOperationMode.get().getId(); - } - - return Response.builder().mode(mode).build(); - } - - @Override - public boolean requiresEnabledApi() { - return false; - } -} 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 deleted file mode 100644 index ef5ffa7de..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.core.mode.AppOperationMode; -import io.xpipe.app.util.ThreadHelper; -import io.xpipe.beacon.api.DaemonStopExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class DaemonStopExchangeImpl extends DaemonStopExchange { - - @Override - public boolean requiresCompletedStartup() { - return false; - } - - @Override - public Object handle(HttpExchange exchange, Request msg) { - ThreadHelper.runAsync(() -> { - ThreadHelper.sleep(1000); - AppOperationMode.close(); - }); - return Response.builder().success(true).build(); - } - - @Override - public boolean requiresEnabledApi() { - return false; - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java deleted file mode 100644 index 2c404ceac..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsBlobExchangeImpl.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.beacon.BlobManager; -import io.xpipe.beacon.api.FsBlobExchange; - -import com.sun.net.httpserver.HttpExchange; -import lombok.SneakyThrows; - -import java.util.UUID; - -public class FsBlobExchangeImpl extends FsBlobExchange { - - @Override - @SneakyThrows - public Object handle(HttpExchange exchange, Request msg) { - var id = UUID.randomUUID(); - - var size = exchange.getRequestBody().available(); - if (size > 100_000_000) { - BlobManager.get().store(id, exchange.getRequestBody()); - } else { - BlobManager.get().store(id, exchange.getRequestBody().readAllBytes()); - } - return Response.builder().blob(id).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java deleted file mode 100644 index fed9d0cc0..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsWriteExchangeImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.beacon.AppBeaconServer; -import io.xpipe.app.beacon.BlobManager; -import io.xpipe.app.ext.ConnectionFileSystem; -import io.xpipe.beacon.api.FsWriteExchange; - -import com.sun.net.httpserver.HttpExchange; -import lombok.SneakyThrows; - -public class FsWriteExchangeImpl extends FsWriteExchange { - - @Override - @SneakyThrows - public Object handle(HttpExchange exchange, Request msg) { - var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); - var fs = new ConnectionFileSystem(shell.getControl()); - try (var in = BlobManager.get().getBlob(msg.getBlob()); - var os = fs.openOutput(msg.getPath(), in.available())) { - in.transferTo(os); - } - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/SecretDecryptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/SecretDecryptExchangeImpl.java deleted file mode 100644 index 0be181103..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/SecretDecryptExchangeImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.storage.DataStorageSecret; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.SecretDecryptExchange; - -import com.sun.net.httpserver.HttpExchange; - -import java.io.IOException; - -public class SecretDecryptExchangeImpl extends SecretDecryptExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException { - var secret = DataStorageSecret.deserialize(msg.getEncrypted()); - if (secret == null) { - throw new BeaconClientException("Unable to parse secret"); - } - - return Response.builder().decrypted(new String(secret.getSecret())).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/SecretEncryptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/SecretEncryptExchangeImpl.java deleted file mode 100644 index ceee66979..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/SecretEncryptExchangeImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.storage.DataStorageSecret; -import io.xpipe.beacon.api.SecretEncryptExchange; -import io.xpipe.core.InPlaceSecretValue; - -import com.sun.net.httpserver.HttpExchange; - -public class SecretEncryptExchangeImpl extends SecretEncryptExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) { - var secret = DataStorageSecret.ofCurrentSecret(InPlaceSecretValue.of(msg.getValue())); - return Response.builder().encrypted(secret.serialize(true)).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java deleted file mode 100644 index 5329275e6..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.beacon.AppBeaconServer; -import io.xpipe.beacon.api.ShellStopExchange; - -import com.sun.net.httpserver.HttpExchange; -import lombok.SneakyThrows; - -public class ShellStopExchangeImpl extends ShellStopExchange { - - @Override - @SneakyThrows - public Object handle(HttpExchange exchange, Request msg) { - var e = AppBeaconServer.get().getCache().getShellSession(msg.getConnection()); - e.getControl().close(); - AppBeaconServer.get().getCache().getShellSessions().remove(e); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java deleted file mode 100644 index 650742201..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.terminal.TerminalLauncherManager; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.TerminalLaunchExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange { - @Override - public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException { - var r = TerminalLauncherManager.launchExchange(msg.getRequest()); - return Response.builder().targetFile(r).build(); - } - - @Override - public boolean requiresEnabledApi() { - return false; - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalPrepareExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalPrepareExchangeImpl.java deleted file mode 100644 index a69a3e921..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalPrepareExchangeImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.beacon.api.TerminalPrepareExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) { - var term = AppPrefs.get().terminalType().getValue(); - var unicode = term.supportsUnicode(); - var escapes = term.supportsEscapes(); - return Response.builder() - .supportsUnicode(unicode) - .supportsEscapeSequences(escapes) - .build(); - } - - @Override - public boolean requiresEnabledApi() { - return false; - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalRegisterExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalRegisterExchangeImpl.java deleted file mode 100644 index b96297be4..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalRegisterExchangeImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.terminal.TerminalLauncherManager; -import io.xpipe.app.terminal.TerminalView; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.api.TerminalRegisterExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class TerminalRegisterExchangeImpl extends TerminalRegisterExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException { - TerminalView.get().open(msg.getRequest(), msg.getPid()); - TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid()); - return Response.builder().build(); - } - - @Override - public boolean requiresEnabledApi() { - return false; - } -} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java deleted file mode 100644 index 0c9f44734..000000000 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.xpipe.app.beacon.impl; - -import io.xpipe.app.terminal.TerminalLauncherManager; -import io.xpipe.beacon.BeaconServerException; -import io.xpipe.beacon.api.TerminalWaitExchange; - -import com.sun.net.httpserver.HttpExchange; - -public class TerminalWaitExchangeImpl extends TerminalWaitExchange { - - @Override - public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException { - TerminalLauncherManager.waitExchange(msg.getRequest()); - return Response.builder().build(); - } - - @Override - public boolean requiresEnabledApi() { - return false; - } -} 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 5afd2a437..e9945117d 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 @@ -4,19 +4,22 @@ import io.xpipe.app.core.AppNames; import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; -import com.fasterxml.jackson.databind.ObjectMapper; +import javafx.beans.property.SimpleBooleanProperty; + import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import io.modelcontextprotocol.common.McpTransportContext; -import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.McpSchema; import lombok.SneakyThrows; import lombok.Value; +import tools.jackson.databind.json.JsonMapper; import java.io.IOException; import java.util.ArrayList; @@ -30,6 +33,7 @@ public class AppMcpServer { McpSyncServer mcpSyncServer; HttpStreamableServerTransportProvider transportProvider; List tools; + HttpHandler httpHandler = createHttpHandler(); public static AppMcpServer get() { return INSTANCE; @@ -38,7 +42,7 @@ public class AppMcpServer { @SneakyThrows public static void init() { var transportProvider = new HttpStreamableServerTransportProvider( - new JacksonMcpJsonMapper(new ObjectMapper()), + new JacksonMcpJsonMapper(JsonMapper.builder().build()), "/mcp", false, (serverRequest) -> McpTransportContext.EMPTY, @@ -52,11 +56,8 @@ public class AppMcpServer { McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider) .serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion()) - .capabilities(McpSchema.ServerCapabilities.builder() - .resources(false, false) - .tools(true) - .prompts(false) - .build()) + .capabilities( + McpSchema.ServerCapabilities.builder().tools(false).build()) .instructions(effectivePrompt) .build(); @@ -88,7 +89,8 @@ public class AppMcpServer { INSTANCE = null; } - public HttpHandler createHttpHandler() { + private HttpHandler createHttpHandler() { + var showingError = new SimpleBooleanProperty(); return new HttpHandler() { @Override @@ -102,13 +104,17 @@ public class AppMcpServer { if (!AppPrefs.get().enableMcpServer().get()) { transportProvider.sendError(exchange, 403, "MCP server is not enabled in the settings menu"); if (exchange.getRequestMethod().equals("POST")) { - ThreadHelper.runAsync(() -> { - ErrorEventFactory.fromMessage( - "An external request was made to the XPipe MCP server, however the MCP server is not enabled in the" - + " settings menu") - .expected() - .handle(); - }); + if (!showingError.get()) { + ThreadHelper.runAsync(() -> { + try (var ignored = new BooleanScope(showingError).start()) { + ErrorEventFactory.fromMessage( + "An external request was made to the XPipe MCP server, however the MCP server is not enabled in the" + + " settings menu") + .expected() + .handle(); + } + }); + } } return; } @@ -118,9 +124,15 @@ public class AppMcpServer { var msg = "Session ID required in mcp-session-id header." + " Check whether you are using the streamable HTTP transport and not something else like SSE."; transportProvider.sendError(exchange, 400, msg); - ThreadHelper.runAsync(() -> { - ErrorEventFactory.fromMessage(msg).expected().handle(); - }); + if (!showingError.get()) { + ThreadHelper.runAsync(() -> { + try (var ignored = new BooleanScope(showingError).start()) { + ErrorEventFactory.fromMessage(msg) + .expected() + .handle(); + } + }); + } return; } @@ -129,14 +141,18 @@ public class AppMcpServer { if (apiKey == null) { transportProvider.sendError(exchange, 403, "Header Authorization is not set"); if (exchange.getRequestMethod().equals("POST")) { - ThreadHelper.runAsync(() -> { - ErrorEventFactory.fromMessage( - "An external request was made to the XPipe MCP server without the header Authorization set. " - + "Please configure your MCP client with the Bearer API token you can find the API " - + "settings menu") - .expected() - .handle(); - }); + if (!showingError.get()) { + ThreadHelper.runAsync(() -> { + try (var ignored = new BooleanScope(showingError).start()) { + ErrorEventFactory.fromMessage( + "An external request was made to the XPipe MCP server without the header Authorization set. " + + "Please configure your MCP client with the Bearer API token you can find the API " + + "settings menu") + .expected() + .handle(); + } + }); + } } return; } @@ -146,12 +162,16 @@ public class AppMcpServer { if (!correct) { transportProvider.sendError(exchange, 403, "Invalid API key"); if (exchange.getRequestMethod().equals("POST")) { - ThreadHelper.runAsync(() -> { - ErrorEventFactory.fromMessage( - "The Authorization header sent by the MCP client is not correct") - .expected() - .handle(); - }); + if (!showingError.get()) { + ThreadHelper.runAsync(() -> { + try (var ignored = new BooleanScope(showingError).start()) { + ErrorEventFactory.fromMessage( + "The Authorization header sent by the MCP client is not correct") + .expected() + .handle(); + } + }); + } } return; } 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 eaac7a101..0d342f0bd 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 @@ -1,10 +1,8 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - package io.xpipe.app.beacon.mcp; +import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.util.JacksonMapper; import com.sun.net.httpserver.HttpExchange; import io.modelcontextprotocol.common.McpTransportContext; @@ -83,10 +81,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe public List protocolVersions() { return List.of( - ProtocolVersions.MCP_2024_11_05, - ProtocolVersions.MCP_2025_03_26, - ProtocolVersions.MCP_2025_06_18, - ProtocolVersions.MCP_2025_11_25); + ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25); } @Override @@ -174,7 +169,12 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe if (!badRequestErrors.isEmpty()) { String combinedMessage = String.join("; ", badRequestErrors); - this.sendError(exchange, 400, combinedMessage); + this.sendMcpError( + exchange, + 400, + McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND) + .message(combinedMessage) + .build()); return; } @@ -186,49 +186,8 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe } logger.debug("Handling GET request for session: {}", sessionId); - - McpTransportContext transportContext = this.contextExtractor.extract(exchange); - - try { - exchange.getResponseHeaders().add("Content-Type", TEXT_EVENT_STREAM); - exchange.getResponseHeaders().add("Content-Encoding", UTF_8); - exchange.getResponseHeaders().add("Cache-Control", "no-cache"); - exchange.getResponseHeaders().add("Connection", "keep-alive"); - exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); - exchange.sendResponseHeaders(200, 0); - - var writer = new PrintWriter(exchange.getResponseBody()); - HttpServletStreamableMcpSessionTransport sessionTransport = - new HttpServletStreamableMcpSessionTransport(sessionId, exchange, writer); - - // Check if this is a replay request - if (exchange.getRequestHeaders().getFirst(HttpHeaders.LAST_EVENT_ID) != null) { - String lastId = exchange.getRequestHeaders().getFirst(HttpHeaders.LAST_EVENT_ID); - - try { - session.replay(lastId) - .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) - .toIterable() - .forEach(message -> { - try { - sessionTransport - .sendMessage(message) - .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) - .block(); - } catch (Exception e) { - logger.error("Failed to replay message: {}", e.getMessage()); - exchange.close(); - } - }); - } catch (Exception e) { - logger.error("Failed to replay messages: {}", e.getMessage()); - exchange.close(); - } - } - } catch (Exception e) { - logger.error("Failed to handle GET request for session {}: {}", sessionId, e.getMessage()); - sendError(exchange, 500, null); - } + // We don't support SSE + sendError(exchange, 404, null); } public void sendError(HttpExchange exchange, int code, String message) throws IOException { @@ -239,7 +198,21 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe os.write(b); } - TrackEvent.error("MCP server error " + code + ": " + message); + TrackEvent.withError("MCP server error " + code).tag("message", message).handle(); + } + + public void sendMcpError(HttpExchange exchange, int httpCode, McpError mcpError) throws IOException { + exchange.getResponseHeaders().add("Content-Encoding", UTF_8); + exchange.getResponseHeaders().add("Content-Type", APPLICATION_JSON); + var jsonError = jsonMapper.writeValueAsString(mcpError); + var b = jsonError.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(httpCode, b.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(b); + } + TrackEvent.withError("MCP server error " + httpCode) + .tag("error", JacksonMapper.getDefault().readTree(jsonError).toPrettyString()) + .handle(); } public void doPost(HttpExchange exchange) throws IOException { @@ -269,6 +242,13 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe try { var body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + var log = AppProperties.get().isPrintBeaconMessages(); + if (log) { + TrackEvent.withTrace("Received MCP request") + .tag("request", "POST") + .tag("data", JacksonMapper.getDefault().readTree(body).toPrettyString()) + .handle(); + } McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body); @@ -277,7 +257,12 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe && jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) { if (!badRequestErrors.isEmpty()) { String combinedMessage = String.join("; ", badRequestErrors); - this.sendError(exchange, 400, combinedMessage); + this.sendMcpError( + exchange, + 400, + McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND) + .message(combinedMessage) + .build()); return; } @@ -294,6 +279,17 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null)); var jsonBytes = jsonResponse.getBytes(StandardCharsets.UTF_8); + if (log) { + TrackEvent.withTrace("Sending MCP response") + .tag("id", init.session().getId()) + .tag( + "data", + JacksonMapper.getDefault() + .readTree(jsonResponse) + .toPrettyString()) + .handle(); + } + exchange.getResponseHeaders().add("Content-Type", APPLICATION_JSON); exchange.getResponseHeaders().add("Content-Encoding", UTF_8); exchange.getResponseHeaders() @@ -303,7 +299,12 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe return; } catch (Exception e) { logger.error("Failed to initialize session: {}", e.getMessage()); - this.sendError(exchange, 500, "Failed to initialize session: " + e.getMessage()); + this.sendMcpError( + exchange, + 500, + McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR) + .message("Failed to initialize session: " + e.getMessage()) + .build()); return; } } @@ -316,14 +317,24 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe if (!badRequestErrors.isEmpty()) { String combinedMessage = String.join("; ", badRequestErrors); - this.sendError(exchange, 400, combinedMessage); + this.sendMcpError( + exchange, + 400, + McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND) + .message(combinedMessage) + .build()); return; } McpStreamableServerSession session = this.sessions.get(sessionId); if (session == null) { - this.sendError(exchange, 404, "Session not found: " + sessionId + ". Was the session not refreshed?"); + this.sendMcpError( + exchange, + 404, + McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR) + .message("Session not found: " + sessionId + ". Was the session not refreshed?") + .build()); return; } @@ -332,11 +343,25 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) .block(); exchange.sendResponseHeaders(200, -1); + if (log) { + TrackEvent.withTrace("Sending MCP response") + .tag("id", sessionId) + .tag("request", "POST") + .tag("data", "") + .handle(); + } } else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) { session.accept(jsonrpcNotification) .contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)) .block(); exchange.sendResponseHeaders(202, -1); + if (log) { + TrackEvent.withTrace("Sending MCP response") + .tag("id", sessionId) + .tag("request", "POST") + .tag("data", "") + .handle(); + } } else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) { // For streaming responses, we need to return SSE exchange.getResponseHeaders().add("Content-Type", TEXT_EVENT_STREAM); @@ -360,15 +385,30 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe exchange.close(); } } else { - this.sendError(exchange, 500, "Unknown message type"); + this.sendMcpError( + exchange, + 500, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Unknown message type") + .build()); } } catch (IllegalArgumentException | IOException e) { logger.error("Failed to deserialize message: {}", e.getMessage()); - this.sendError(exchange, 400, "Invalid message format: " + e.getMessage()); + this.sendMcpError( + exchange, + 400, + McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST) + .message("Invalid message format: " + e.getMessage()) + .build()); } catch (Exception e) { logger.error("Error handling message: {}", e.getMessage()); try { - this.sendError(exchange, 500, "Error processing message: " + e.getMessage()); + this.sendMcpError( + exchange, + 500, + McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR) + .message("Error processing message: " + e.getMessage()) + .build()); } catch (IOException ex) { logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage()); sendError(exchange, 500, "Error processing message"); @@ -381,8 +421,15 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe } protected void doDelete(HttpExchange exchange) throws IOException { - String requestURI = exchange.getRequestURI().toString(); + var log = AppProperties.get().isPrintBeaconMessages(); + if (log) { + TrackEvent.withTrace("Received MCP request") + .tag("request", "DELETE") + .tag("data", "") + .handle(); + } + if (!requestURI.endsWith(mcpEndpoint)) { sendError(exchange, 404, null); return; @@ -401,7 +448,12 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe 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"); + this.sendMcpError( + exchange, + 400, + McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND) + .message("Session ID required in mcp-session-id header") + .build()); return; } @@ -419,9 +471,22 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe .block(); this.sessions.remove(sessionId); exchange.sendResponseHeaders(200, -1); + if (log) { + TrackEvent.withTrace("Sending MCP response") + .tag("id", sessionId) + .tag("request", "DELETE") + .tag("data", "") + .handle(); + } } catch (Exception e) { logger.error("Failed to delete session {}: {}", sessionId, e.getMessage()); try { + this.sendMcpError( + exchange, + 500, + McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR) + .message(e.getMessage()) + .build()); sendError(exchange, 500, e.getMessage()); } catch (IOException ex) { logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage()); @@ -431,6 +496,15 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe } private void sendEvent(PrintWriter writer, String eventType, String data, String id) throws IOException { + var log = AppProperties.get().isPrintBeaconMessages(); + if (log) { + TrackEvent.withTrace("Sending MCP response") + .tag("id", id) + .tag("event", eventType) + .tag("data", JacksonMapper.getDefault().readTree(data).toPrettyString()) + .handle(); + } + if (id != null) { writer.write("id: " + id + "\n"); } @@ -488,7 +562,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe var clientDisconnected = "Client disconnected".equals(e.getMessage()); if (!clientDisconnected) { logger.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage()); - HttpStreamableServerTransportProvider.this.sessions.remove(this.sessionId); + // HttpStreamableServerTransportProvider.this.sessions.remove(this.sessionId); exchange.close(); } } finally { diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java index 68d528dbb..2d6ee6b41 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java @@ -1,6 +1,6 @@ package io.xpipe.app.beacon.mcp; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.util.JacksonMapper; import io.modelcontextprotocol.spec.McpSchema; diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java index e664d033c..7da7c588b 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java @@ -1,13 +1,13 @@ package io.xpipe.app.beacon.mcp; -import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.beacon.BeaconClientException; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.process.ShellControl; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageQuery; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.core.FilePath; +import io.xpipe.app.store.ShellStore; +import io.xpipe.app.util.FilePath; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpSchema; @@ -81,6 +81,10 @@ public interface McpToolHandler } public String getStringArgument(String key) throws BeaconClientException { + if (request.arguments() == null) { + throw new BeaconClientException("Missing argument for key " + key); + } + var o = request.arguments().get(key); if (o == null) { throw new BeaconClientException("Missing argument for key " + key); 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 e0e6f86c4..1c3fc5915 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 @@ -1,32 +1,36 @@ package io.xpipe.app.beacon.mcp; -import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.*; +import io.xpipe.app.beacon.api.HandshakeExchange; import io.xpipe.app.core.AppExtensionManager; import io.xpipe.app.core.AppNames; import io.xpipe.app.ext.*; -import io.xpipe.app.hub.comp.StoreEntryWrapper; -import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileInfo; +import io.xpipe.app.fs.ShellFileSystem; +import io.xpipe.app.hub.list.StoreViewState; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.process.ScriptHelper; import io.xpipe.app.process.ShellControl; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageQuery; +import io.xpipe.app.store.SingletonSessionStore; import io.xpipe.app.terminal.TerminalLaunch; import io.xpipe.app.util.CommandDialog; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.HttpHelper; -import io.xpipe.beacon.BeaconClientException; -import io.xpipe.beacon.BeaconInterface; -import io.xpipe.core.FilePath; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.util.JacksonMapper; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.spec.McpSchema; import lombok.Builder; import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.JsonNodeFactory; import java.io.IOException; import java.net.URI; @@ -84,12 +88,35 @@ public final class McpTools { throw new BeaconClientException("No API endpoint found for path " + path); } + var handshakeRequest = HandshakeExchange.Request.builder() + .client(BeaconClientInformation.Mcp.builder().build()) + .auth(BeaconAuthMethod.ApiKey.builder() + .key(AppPrefs.get().apiKey().get()) + .build()) + .build(); + var handshakeReq = HttpRequest.newBuilder() + .uri(URI.create( + "http://localhost:" + AppBeaconServer.get().getPort() + "/handshake")) + .POST(HttpRequest.BodyPublishers.ofString( + JacksonMapper.getDefault().writeValueAsString(handshakeRequest))) + .build(); + var handshakeRes = HttpHelper.client().send(handshakeReq, HttpResponse.BodyHandlers.ofString()); + var handshakeResJson = JacksonMapper.getDefault().readTree(handshakeRes.body()); + if (handshakeRes.statusCode() >= 400) { + return McpSchema.CallToolResult.builder() + .addTextContent(handshakeResJson.toPrettyString()) + .isError(true) + .build(); + } + + var handshakeResObject = + JacksonMapper.getDefault().treeToValue(handshakeResJson, HandshakeExchange.Response.class); + var token = handshakeResObject.getSessionToken(); + var httpReq = HttpRequest.newBuilder() .uri(URI.create( "http://localhost:" + AppBeaconServer.get().getPort() + path)) - .header( - "Authorization", - "Bearer " + AppPrefs.get().apiKey().get()) + .header("Authorization", "Bearer " + token) .POST(HttpRequest.BodyPublishers.ofString(payloadJson.toPrettyString())) .build(); var httpRes = HttpHelper.client().send(httpReq, HttpResponse.BodyHandlers.ofString()); @@ -148,15 +175,20 @@ public final class McpTools { var r = ConnectionResource.builder() .name(e.getName()) .path(DataStorage.get().getStorePath(e).toString()) - .information(info) + .information(info != null ? info.toJoinedString() : null) .notes(e.getNotes()) .build(); list.add(r); } + JsonMapper mapper = JacksonMapper.getDefault() + .rebuild() + .changeDefaultPropertyInclusion( + value -> value.withValueInclusion(JsonInclude.Include.NON_NULL)) + .build(); var json = JsonNodeFactory.instance.arrayNode(); for (var e : list) { - json.add(JacksonMapper.getDefault().valueToTree(e)); + json.add(mapper.valueToTree(e)); } var object = JsonNodeFactory.instance.objectNode(); @@ -180,7 +212,7 @@ public final class McpTools { var shellStore = req.getShellStoreRef(system, false); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); var path = req.getFilePath(shellSession.getControl(), "path"); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); if (!fs.fileExists(path)) { throw new BeaconClientException("File " + path + " does not exist"); @@ -203,17 +235,16 @@ public final class McpTools { .tool(tool) .callHandler(McpToolHandler.of((req) -> { var system = req.getStringArgument("system"); - var recursive = req.getOptionalBooleanArgument("recursive").orElse(false); var shellStore = req.getShellStoreRef(system, false); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); var path = req.getFilePath(shellSession.getControl(), "path"); if (!fs.directoryExists(path)) { throw new BeaconClientException("Directory " + path + " does not exist"); } - try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) { + try (var stream = fs.listFiles(fs, path)) { var list = stream.toList(); var builder = McpSchema.CallToolResult.builder(); for (FileEntry e : list) { @@ -233,16 +264,17 @@ public final class McpTools { var system = req.getStringArgument("system"); var pattern = req.getStringArgument("name"); var shellStore = req.getShellStoreRef(system, false); + var recursive = req.getOptionalBooleanArgument("recursive").orElse(false); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); var path = req.getFilePath(shellSession.getControl(), "path"); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); if (!fs.directoryExists(path)) { throw new BeaconClientException("Directory " + path + " does not exist"); } var regex = Pattern.compile(DataStorageQuery.toRegex(pattern)); - try (var stream = fs.listFiles(fs, path)) { + try (var stream = recursive ? fs.listFilesRecursively(fs, path).stream() : fs.listFiles(fs, path)) { var list = stream.toList(); var builder = McpSchema.CallToolResult.builder(); list.stream() @@ -267,7 +299,7 @@ public final class McpTools { var shellStore = req.getShellStoreRef(system, false); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); var path = req.getFilePath(shellSession.getControl(), "path"); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); if (!fs.fileExists(path) && !fs.directoryExists(path)) { throw new BeaconClientException("Path " + path + " does not exist"); @@ -309,7 +341,7 @@ public final class McpTools { var shellStore = req.getShellStoreRef(system, true); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); var path = req.getFilePath(shellSession.getControl(), "path"); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); if (fs.fileExists(path)) { throw new BeaconClientException("File " + path + " does already exist"); @@ -342,7 +374,7 @@ public final class McpTools { var shellStore = req.getShellStoreRef(system, true); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); var path = req.getFilePath(shellSession.getControl(), "path"); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); var b = content.getBytes(StandardCharsets.UTF_8); try (var out = fs.openOutput(path, b.length)) { @@ -365,7 +397,7 @@ public final class McpTools { var shellStore = req.getShellStoreRef(system, true); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); var path = req.getFilePath(shellSession.getControl(), "path"); - var fs = new ConnectionFileSystem(shellSession.getControl()); + var fs = new ShellFileSystem(shellSession.getControl()); if (fs.fileExists(path)) { throw new BeaconClientException("Directory " + path + " does already exist"); @@ -390,7 +422,7 @@ public final class McpTools { var shellStore = req.getShellStoreRef(system, true); var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore); - var r = ProcessControlProvider.get().executeMcpCommand(shellSession.getControl(), command); + var r = ProcModuleProvider.get().executeMcpCommand(shellSession.getControl(), command); return r; })) .build(); @@ -415,8 +447,9 @@ public final class McpTools { .findModule(AppNames.extModuleName("base")) .orElseThrow(), AppNames.extModuleName("base") + ".script.ScriptStore"); - var method = clazz.getDeclaredMethod("assembleScriptChain", ShellControl.class); - var command = (String) method.invoke(script.getStore(), shellSession.getControl()); + var method = clazz.getDeclaredMethod("assembleScriptChain", ShellControl.class, boolean.class); + var command = + (String) method.invoke(script.getStore(), shellSession.getControl(), arguments != null); var scriptFile = ScriptHelper.createExecScript(shellSession.getControl(), command); var out = shellSession .getControl() @@ -424,7 +457,7 @@ public final class McpTools { .getControl() .getShellDialect() .runScriptCommand(shellSession.getControl(), scriptFile.toString()) - + arguments) + + (arguments != null ? " " + arguments : "")) .withWorkingDirectory(directory) .readStdoutOrThrow(); var formatted = CommandDialog.formatOutput(out); 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 1589458a0..ee3708f80 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -8,24 +8,23 @@ import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppLayoutModel; -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.StoreFilter; -import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.hub.entry.StoreEntryWrapper; +import io.xpipe.app.hub.list.StoreFilter; +import io.xpipe.app.hub.list.StoreViewState; import io.xpipe.app.platform.BindingsHelper; import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.PlatformThread; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.FileSystemStore; +import io.xpipe.app.store.ShellStore; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.FileReference; import io.xpipe.app.util.ObservableSubscriber; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FilePath; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.*; import javafx.collections.ListChangeListener; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; @@ -56,7 +55,12 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { Consumer file, boolean save, boolean directory, - Predicate filter) { + Predicate filter, + BooleanProperty busy) { + if (store.get() == null && DataStorage.get().getStoreEntries().stream().noneMatch(filter)) { + return; + } + var model = new BrowserFileChooserSessionModel(directory); model.setOnFinish(fileStores -> { file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null); @@ -73,18 +77,21 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { }); var selectionField = new TextFieldComp(selection); selectionField.apply(struc -> { - struc.setEditable(false); AppFontSizes.base(struc); }); selectionField.style("chooser-selection"); selectionField.hgrow(); - var modal = ModalOverlay.of(save ? "saveFileTitle" : "openFileTitle", comp); + var modal = ModalOverlay.of( + save + ? (directory ? "saveDirectoryTitle" : "saveFileTitle") + : (directory ? "openDirectoryTitle" : "openFileTitle"), + comp); modal.setRequireCloseButtonForClose(true); modal.addButtonBarComp(selectionField); modal.addButton(new ModalButton("select", () -> model.finishChooser(), true, true)); modal.show(); ThreadHelper.runAsync(() -> { - model.openFileSystemAsync(store.get(), null, (sc) -> initialPath.get(), model.getBusy()); + model.openFileSystemAsync(store.get(), null, (sc) -> initialPath.get(), busy); }); } @@ -154,7 +161,12 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { if (selected != null) { s.getChildren().setAll(new BrowserFileSystemTabComp(selected, false).build()); } else { - s.getChildren().clear(); + s.getChildren() + .setAll(new LoadingIconComp( + new ReadOnlyBooleanWrapper(true), node -> AppFontSizes.title(node)) + .prefWidth(50) + .prefHeight(50) + .build()); } }); }); 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 f947377ee..349a0c46b 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java @@ -2,14 +2,15 @@ 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.fs.FileKind; +import io.xpipe.app.fs.FileSystem; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.FileSystemStore; import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.FailableFunction; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.FileReference; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FailableFunction; -import io.xpipe.core.FilePath; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -35,16 +36,35 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel< public BrowserFileChooserSessionModel(boolean directory) { this.directory = directory; - selectedEntry.addListener((observable, oldValue, newValue) -> { - if (newValue == null) { + selectedEntry.addListener((modelObservable, oldModel, newModel) -> { + if (newModel == null) { fileSelection.clear(); return; } - fileSelection.setAll(newValue.getFileList().getSelection()); - newValue.getFileList().getSelection().addListener((ListChangeListener) c -> { - fileSelection.setAll(newValue.getFileList().getSelection()); + newModel.getFileList().getSelection().addListener((ListChangeListener) c -> { + var updated = newModel.getFileList() + .getSelection() + .filtered(browserEntry -> (directory + && browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY) + || (!directory && browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY)); + if (!updated.isEmpty()) { + fileSelection.setAll(updated); + } }); + + if (directory) { + if (newModel.getCurrentDirectory() != null) { + fileSelection.setAll(new BrowserEntry(newModel.getCurrentDirectory(), newModel.getFileList())); + } + newModel.getCurrentPath().addListener((observable, oldValue, newValue) -> { + if (newModel.getCurrentDirectory() != null) { + fileSelection.setAll(new BrowserEntry(newModel.getCurrentDirectory(), newModel.getFileList())); + } else { + fileSelection.clear(); + } + }); + } }); } 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 da9ccfd25..42478bf72 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java @@ -8,14 +8,13 @@ import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppLayoutModel; -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.StoreFilter; -import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.hub.entry.StoreEntryWrapper; +import io.xpipe.app.hub.list.StoreFilter; +import io.xpipe.app.hub.list.StoreViewState; import io.xpipe.app.platform.BindingsHelper; import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.PlatformThread; +import io.xpipe.app.store.ShellStore; import io.xpipe.app.util.ObservableSubscriber; import io.xpipe.app.util.ThreadHelper; @@ -80,7 +79,6 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder { loadingStack.apply(struc -> struc.setPickOnBounds(false)); var delayedStack = new DelayedInitComp( left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized()); - delayedStack.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000)); var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withOnDividerChange(d -> { @@ -201,6 +199,9 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder { cache.keySet().removeIf(browserSessionTab -> !all.contains(browserSessionTab)); if (newValue == null) { + struc.setMinWidth(0); + struc.setPrefWidth(Region.USE_COMPUTED_SIZE); + struc.setMaxWidth(Region.USE_COMPUTED_SIZE); struc.getChildren().clear(); return; } 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 2a49e5184..babb0631d 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java @@ -5,16 +5,16 @@ 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.fs.FileSystem; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.FileSystemStore; import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.FailableFunction; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FailableFunction; -import io.xpipe.core.FilePath; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; 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 00fd2fba7..d21c45001 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSessionTabsComp.java @@ -6,9 +6,9 @@ import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.comp.base.LoadingIconComp; import io.xpipe.app.comp.base.PrettyImageHelper; 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.core.AppLayoutModel; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.platform.MenuHelper; import io.xpipe.app.platform.PlatformThread; @@ -269,26 +269,28 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder { .paddingProperty() .bind(Bindings.createObjectBinding( () -> { - var w = App.getApp().getStage().getWidth(); - if (w >= 1000) { + if (!AppLayoutModel.get() + .getPortraitLayoutCollapsed() + .get()) { return new Insets(2, 0, 4, -leftPadding.get() + 3); } else { return new Insets(2, 0, 4, -leftPadding.get() - 4); } }, - App.getApp().getStage().widthProperty(), + AppLayoutModel.get().getPortraitLayoutCollapsed(), leftPadding)); tabs.paddingProperty() .bind(Bindings.createObjectBinding( () -> { - var w = App.getApp().getStage().getWidth(); - if (w >= 1000) { + if (!AppLayoutModel.get() + .getPortraitLayoutCollapsed() + .get()) { return new Insets(0, 0, 0, -5); } else { return new Insets(0, 0, 0, 5); } }, - App.getApp().getStage().widthProperty())); + AppLayoutModel.get().getPortraitLayoutCollapsed())); headerHeight.bind(headerArea.heightProperty()); }); } @@ -511,8 +513,6 @@ public class BrowserSessionTabsComp extends SimpleRegionBuilder { 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/BrowserStoreSessionTab.java b/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java index 3cb961ab4..957dff131 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java @@ -1,10 +1,10 @@ package io.xpipe.app.browser; import io.xpipe.app.comp.BaseRegionBuilder; -import io.xpipe.app.ext.DataStore; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.DataStore; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; 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 d91faab82..b9ec8ea85 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 @@ -5,10 +5,10 @@ import io.xpipe.app.browser.BrowserStoreSessionTab; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.core.AppLayoutModel; -import io.xpipe.app.ext.FileSystemStore; import io.xpipe.app.hub.action.StoreAction; import io.xpipe.app.issue.ErrorEventFactory; -import io.xpipe.core.FilePath; +import io.xpipe.app.store.FileSystemStore; +import io.xpipe.app.util.FilePath; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; 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 fae2be394..80f8fd82a 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 @@ -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.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java index e5c6dbc3b..bf0d56a54 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java @@ -2,7 +2,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.core.FilePath; +import io.xpipe.app.util.FilePath; import lombok.NonNull; import lombok.experimental.SuperBuilder; 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 27ffffb6f..6e55ea82b 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.app.ext.FileKind; +import io.xpipe.app.fs.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 791fade85..27924c685 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.app.ext.FileKind; +import io.xpipe.app.fs.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 cf7bc834b..518ac052b 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,8 +3,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.ext.FileKind; -import io.xpipe.core.FilePath; +import io.xpipe.app.fs.FileKind; +import io.xpipe.app.util.FilePath; import lombok.NonNull; import lombok.experimental.SuperBuilder; 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 622fa01cb..0eeafe849 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.app.ext.FileKind; +import io.xpipe.app.fs.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 ef86d60ed..7d5af6a6b 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.app.ext.FileKind; +import io.xpipe.app.fs.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 9de70d022..f1cf2672b 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,11 +4,11 @@ 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.fs.FileKind; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.LocalShell; import io.xpipe.app.process.ShellControl; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; 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 b2a6cd233..4f3cfa10a 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,8 +5,8 @@ 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.app.ext.FileKind; -import io.xpipe.core.OsType; +import io.xpipe.app.fs.FileKind; +import io.xpipe.app.util.OsType; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java index 6e3f0fd28..2db08251c 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java @@ -6,12 +6,12 @@ import io.xpipe.app.action.StoreContextAction; import io.xpipe.app.browser.file.BrowserFileTransferOperation; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.app.ext.FileSystemStore; import io.xpipe.app.process.ParentSystemAccess; import io.xpipe.app.process.ShellControl; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.FileSystemStore; import lombok.NonNull; import lombok.experimental.SuperBuilder; 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 05e870e98..bb47f4194 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 @@ -3,9 +3,9 @@ package io.xpipe.app.browser.file; import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.GlobalTimer; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FilePath; import javafx.application.Platform; import javafx.css.PseudoClass; 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 f65123ca3..ffb8dbad2 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 @@ -1,7 +1,7 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.ext.FileEntry; -import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.ext.ProcModuleProvider; +import io.xpipe.app.fs.FileEntry; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.platform.GlobalClipboard; import io.xpipe.app.platform.GlobalObjectProperty; @@ -120,7 +120,7 @@ public class BrowserClipboard { public String toClipboardString() { return entries.stream() .map(fileEntry -> "\"" + fileEntry.getRawFileEntry().getPath() + "\"") - .collect(Collectors.joining(ProcessControlProvider.get() + .collect(Collectors.joining(ProcModuleProvider.get() .getEffectiveLocalDialect() .getNewLine() .getNewLineString())); 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 ac192d613..af1376942 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 @@ -1,7 +1,13 @@ package io.xpipe.app.browser.file; import io.xpipe.app.comp.*; -import io.xpipe.app.hub.comp.*; +import io.xpipe.app.hub.category.StoreCategoryWrapper; +import io.xpipe.app.hub.entry.StoreEntryWrapper; +import io.xpipe.app.hub.list.StoreFilter; +import io.xpipe.app.hub.section.StoreSection; +import io.xpipe.app.hub.section.StoreSectionMiniComp; +import io.xpipe.app.hub.section.StoreSectionSelector; +import io.xpipe.app.hub.section.StoreSectionState; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.storage.DataStoreEntry; @@ -15,7 +21,6 @@ import javafx.scene.control.Button; import javafx.scene.layout.Region; import java.util.HashSet; -import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Predicate; @@ -68,16 +73,22 @@ public final class BrowserConnectionListComp extends SimpleRegionBuilder { }); }; + var selector = new StoreSectionSelector() { + + @Override + public boolean excludeNonShown() { + return true; + } + + @Override + public boolean matches(StoreEntryWrapper wrapper) { + return applicable.test(wrapper); + } + }; + var sectionState = new StoreSectionState( + filter, selector, category, FXCollections.emptyObservableList(), new ReadOnlyBooleanWrapper(true)); var section = new StoreSectionMiniComp( - StoreSection.createTopLevel( - StoreViewState.get().getAllEntries(), - Set.of(), - this::filter, - filter, - category, - StoreViewState.get().getEntriesListVisibilityObservable(), - StoreViewState.get().getEntriesListUpdateObservable(), - new ReadOnlyBooleanWrapper(true)), + sectionState.getRootSection(), augment, selectedAction -> { BooleanProperty busy = new SimpleBooleanProperty(false); 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 884a2bb37..84eed633c 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,10 +4,10 @@ import io.xpipe.app.comp.SimpleRegionBuilder; 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.StoreCategoryChoiceComp; -import io.xpipe.app.hub.comp.StoreCategoryWrapper; -import io.xpipe.app.hub.comp.StoreFilter; -import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.hub.category.StoreCategoryChoiceComp; +import io.xpipe.app.hub.category.StoreCategoryWrapper; +import io.xpipe.app.hub.list.StoreFilter; +import io.xpipe.app.hub.list.StoreViewState; import io.xpipe.app.util.ObservableSubscriber; import javafx.beans.property.Property; 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 a70f0e648..cd9ecadd5 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 @@ -4,7 +4,7 @@ import io.xpipe.app.comp.base.ModalButton; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.beans.property.SimpleObjectProperty; 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 c580cf429..2a19f9c69 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 @@ -2,8 +2,8 @@ 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.app.ext.FileKind; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; import lombok.Getter; 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 index 9f06b46ee..3c84c48fc 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileDuplicates.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileDuplicates.java @@ -1,7 +1,7 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.ext.FileSystem; -import io.xpipe.core.FilePath; +import io.xpipe.app.fs.FileSystem; +import io.xpipe.app.util.FilePath; import java.util.regex.Pattern; @@ -19,13 +19,17 @@ public class BrowserFileDuplicates { } private static FilePath renameFile(FilePath target, boolean dir) { - var name = dir || target.isDotFile() ? target.getFileName() : target.getBaseName().getFileName(); + var name = dir || target.isDotFile() + ? target.getFileName() + : target.getBaseName().getFileName(); var pattern = Pattern.compile("(.+)_(\\d+)"); var matcher = pattern.matcher(name); if (matcher.matches()) { try { var number = Integer.parseInt(matcher.group(2)); - var suffix = dir || target.isDotFile() ? "" : target.getExtension().map(s -> "." + s).orElse(""); + var suffix = dir || target.isDotFile() + ? "" + : target.getExtension().map(s -> "." + s).orElse(""); var newFile = target.getParent().join(matcher.group(1) + "_" + (number + 1) + suffix); return newFile; } catch (NumberFormatException ignored) { 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 index da52a9639..e64bc202c 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileInput.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileInput.java @@ -1,13 +1,13 @@ 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.fs.FileEntry; +import io.xpipe.app.fs.FileInfo; +import io.xpipe.app.fs.ShellFileSystem; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ElevationFunction; -import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import java.io.InputStream; @@ -81,7 +81,7 @@ public interface BrowserFileInput { .elevated(ElevationFunction.elevated(null)) .start() : model.getFileSystem().getShell().orElseThrow().start(); - var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem(); + var fs = elevate ? new ShellFileSystem(sc) : model.getFileSystem(); var output = new BrowserFileInput() { @Override 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 c319ce0b1..39cf4b8eb 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,9 +5,10 @@ import io.xpipe.app.comp.RegionDescriptor; import io.xpipe.app.comp.SimpleRegionBuilder; 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.core.AppSizeBreakpoints; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileInfo; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.util.*; @@ -42,7 +43,6 @@ public final class BrowserFileListComp extends SimpleRegionBuilder { private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty"); private static final PseudoClass FILE = PseudoClass.getPseudoClass("file"); private static final PseudoClass FOLDER = PseudoClass.getPseudoClass("folder"); - private static final PseudoClass DRAG = PseudoClass.getPseudoClass("drag"); private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over"); private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current"); @@ -193,7 +193,7 @@ public final class BrowserFileListComp extends SimpleRegionBuilder { TableColumn sizeCol) { table.widthProperty().subscribe((newValue) -> { if (fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) { - ownerCol.setVisible(newValue.doubleValue() > 1000); + ownerCol.setVisible(!AppSizeBreakpoints.compactMode().get()); } if (fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) { @@ -483,7 +483,6 @@ public final class BrowserFileListComp extends SimpleRegionBuilder { }); row.itemProperty().addListener((observable, oldValue, newValue) -> { - row.pseudoClassStateChanged(DRAG, false); row.pseudoClassStateChanged(DRAG_OVER, false); }); 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 70a4a81e0..af01f46b2 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 @@ -2,12 +2,12 @@ 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.fs.FileKind; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.GlobalTimer; +import io.xpipe.app.util.OsType; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.OsType; import javafx.scene.Node; import javafx.scene.control.ContextMenu; 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 b30c79425..19a91f158 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 @@ -103,7 +103,7 @@ public class BrowserFileListFilterComp extends RegionStructureBuilder { if (val) { text.setPrefWidth(250); 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 8e303e5c7..5b23beba8 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 @@ -1,11 +1,11 @@ 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.fs.FileEntry; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; 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 83fb45a5b..513d986f7 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 @@ -3,13 +3,13 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.icon.BrowserIconManager; import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.comp.base.PrettyImageHelper; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.MenuHelper; import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FilePath; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -133,7 +133,7 @@ class BrowserFileListNameCell extends TableCell { if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) { var cm = new BrowserContextMenu( fileList.getFileSystemModel(), getTableRow().getItem(), false); - MenuHelper.toggleMenuShow(cm, this, Side.RIGHT); + MenuHelper.show(cm, this, Side.RIGHT); event.consume(); } }); 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 b559aeb41..7e758105f 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,7 +2,7 @@ package io.xpipe.app.browser.file; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.app.ext.FileEntry; +import io.xpipe.app.fs.FileEntry; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.FileBridge; 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 b3584a086..88641c548 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,16 +1,16 @@ 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.fs.FileEntry; +import io.xpipe.app.fs.FileInfo; +import io.xpipe.app.fs.ShellFileSystem; 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 io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import java.io.OutputStream; import java.util.List; @@ -87,7 +87,7 @@ public interface BrowserFileOutput { .elevated(ElevationFunction.elevated(null)) .start() : model.getFileSystem().getShell().orElseThrow().start(); - var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem(); + var fs = elevate ? new ShellFileSystem(sc) : model.getFileSystem(); var checkSudoersFile = shell.isPresent() && file.getPath().startsWith("/etc/sudo"); var output = 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 483a5f4f4..2b0d6e694 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 @@ -6,7 +6,7 @@ import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.comp.base.HorizontalComp; import io.xpipe.app.comp.base.ListBoxViewComp; -import io.xpipe.app.ext.FileEntry; +import io.xpipe.app.fs.FileEntry; import javafx.collections.ObservableList; import javafx.geometry.Pos; 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 c570c8d4b..f0df5c589 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,11 +1,11 @@ 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.fs.FileEntry; +import io.xpipe.app.fs.FileKind; +import io.xpipe.app.fs.FileSystem; import io.xpipe.app.issue.ErrorEventFactory; -import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import java.time.Instant; import java.util.List; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java index 0d9603e41..691ec41d9 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser.file; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java index 777c19e4a..15d1d5963 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemSavedState.java @@ -1,29 +1,28 @@ package io.xpipe.app.browser.file; import io.xpipe.app.core.AppCache; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.GlobalTimer; -import io.xpipe.core.FilePath; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.util.JacksonMapper; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; import lombok.*; import lombok.extern.jackson.Jacksonized; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.ser.std.StdSerializer; -import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.*; @@ -59,8 +58,8 @@ public class BrowserFileSystemSavedState { } static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) { - var state = AppCache.getNonNull( - "fs-state-" + model.getEntry().get().getUuid(), BrowserFileSystemSavedState.class, () -> { + var state = AppCache.getNonNullMapEntry( + "fs-state", model.getEntry().get().getUuid().toString(), BrowserFileSystemSavedState.class, () -> { return new BrowserFileSystemSavedState(); }); state.setModel(model); @@ -72,7 +71,7 @@ public class BrowserFileSystemSavedState { return; } - AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this); + AppCache.updateMapEntry("fs-state", model.getEntry().get().getUuid().toString(), this); } public void cd(FilePath dir) { @@ -126,8 +125,7 @@ public class BrowserFileSystemSavedState { } @Override - public void serialize(BrowserFileSystemSavedState value, JsonGenerator gen, SerializerProvider provider) - throws IOException { + public void serialize(BrowserFileSystemSavedState value, JsonGenerator gen, SerializationContext context) { var node = JsonNodeFactory.instance.objectNode(); node.set("recentDirectories", JacksonMapper.getDefault().valueToTree(value.getRecentDirectories())); gen.writeTree(node); 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 b6819fbae..981742099 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 @@ -3,14 +3,14 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.BrowserFullSessionModel; import io.xpipe.app.browser.menu.BrowserMenuProviders; import io.xpipe.app.comp.*; -import io.xpipe.app.comp.augment.ContextMenuAugment; import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppFontSizes; +import io.xpipe.app.core.AppSizeBreakpoints; import io.xpipe.app.platform.InputHelper; import io.xpipe.app.platform.MenuHelper; import io.xpipe.app.platform.PlatformThread; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.GlobalTimer; -import io.xpipe.core.FilePath; import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyBooleanWrapper; @@ -59,7 +59,7 @@ public class BrowserFileSystemTabComp extends SimpleRegionBuilder { .shortcut(new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN)) .build() .apply(overview); - overview.disableProperty().bind(model.getInOverview()); + overview.disableProperty().bind(PlatformThread.sync(model.getInOverview())); InputHelper.onKeyCombination( root, new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN), true, keyEvent -> { overview.fire(); @@ -80,12 +80,12 @@ public class BrowserFileSystemTabComp extends SimpleRegionBuilder { null, () -> new BrowserContextMenu(model, null, false)) .accept(menuButton); - menuButton.disableProperty().bind(model.getInOverview()); + menuButton.disableProperty().bind(PlatformThread.sync(model.getInOverview())); RegionDescriptor.builder().nameKey("directoryOptions").build().apply(menuButton); var smallWidth = Bindings.createBooleanBinding( () -> { - return root.getWidth() < 450; + return root.getWidth() < 550; }, root.widthProperty()); @@ -121,6 +121,22 @@ public class BrowserFileSystemTabComp extends SimpleRegionBuilder { topBar.getChildren().setAll(leftBox, new Spacer(6), navBar.get(), new Spacer(6), rightBox); topBar.setMinWidth(0); + var showAll = Bindings.createBooleanBinding( + () -> { + return !AppSizeBreakpoints.portraitMode().get() + || !navBar.textField().isFocused(); + }, + AppSizeBreakpoints.portraitMode(), + navBar.textField().focusedProperty()); + leftBox.visibleProperty().bind(showAll); + leftBox.managedProperty().bind(leftBox.visibleProperty()); + rightBox.visibleProperty().bind(showAll); + rightBox.managedProperty().bind(leftBox.visibleProperty()); + topBar.getChildren().get(1).visibleProperty().bind(showAll); + topBar.getChildren().get(1).managedProperty().bind(showAll); + topBar.getChildren().get(3).visibleProperty().bind(showAll); + topBar.getChildren().get(3).managedProperty().bind(showAll); + if (model.getBrowserModel() instanceof BrowserFullSessionModel fullSessionModel) { var pinButton = new Button(); RegionDescriptor.builder().nameKey("pinTab").build().apply(pinButton); @@ -143,6 +159,8 @@ public class BrowserFileSystemTabComp extends SimpleRegionBuilder { } e.consume(); }); + pinButton.managedProperty().bind(smallWidth.not()); + pinButton.visibleProperty().bind(pinButton.managedProperty()); rightBox.getChildren().add(1, pinButton); squaredSize(navBar.get(), pinButton, true); } 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 fcc896f52..1fba6b2b6 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,18 +9,23 @@ import io.xpipe.app.browser.menu.BrowserMenuItemProvider; import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.ext.*; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; +import io.xpipe.app.fs.FileSystem; +import io.xpipe.app.fs.WrapperFileSystem; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.process.*; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.store.FileSystemStore; import io.xpipe.app.terminal.*; import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.FailableFunction; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FailableFunction; -import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -165,7 +170,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab shellControl.getShellDialect()) .orElse(null) == ShellDialects.CMD) { - var env = - ProcessControlProvider.get().subShellEnvironment(getEntry().asNeeded(), ShellDialects.POWERSHELL); + var env = ProcModuleProvider.get().subShellEnvironment(getEntry().asNeeded(), ShellDialects.POWERSHELL); var entry = DataStoreEntry.createNew(getName().getValue() + " (PowerShell)", env); entry.setColor(DataStorage.get().getEffectiveColor(getEntry().get())); entry.setCategoryUuid(getEntry().get().getCategoryUuid()); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java index f7a24443c..439e05d05 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -1,12 +1,14 @@ package io.xpipe.app.browser.file; +import io.xpipe.app.core.AppCache; import io.xpipe.app.core.mode.AppOperationMode; -import io.xpipe.app.ext.FileEntry; -import io.xpipe.app.ext.FileKind; -import io.xpipe.app.ext.FileSystem; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; +import io.xpipe.app.fs.FileSystem; import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.GlobalTimer; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FilePath; import javafx.beans.property.BooleanProperty; import javafx.beans.value.ChangeListener; @@ -20,6 +22,7 @@ import java.time.Duration; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -262,7 +265,7 @@ public class BrowserFileTransferOperation { // Source might have been deleted meanwhile var exists = source.getFileSystem().directoryExists(source.getPath()); if (!exists) { - progress.accept(BrowserTransferProgress.finished(source.getName(), 0)); + updateProgress(BrowserTransferProgress.finished(source.getName(), 0)); return; } @@ -274,7 +277,7 @@ public class BrowserFileTransferOperation { var baseRelative = source.getPath().getParent().toDirectory(); source.getFileSystem().traverseFilesRecursively(source.getFileSystem(), source.getPath(), fileEntry -> { if (cancelled()) { - progress.accept(BrowserTransferProgress.finished(source.getName() + " ...", totalSize.get())); + updateProgress(BrowserTransferProgress.finished(source.getName() + " ...", totalSize.get())); return false; } @@ -290,7 +293,7 @@ public class BrowserFileTransferOperation { // This one is up-to-date and does not need to be recalculated // If we don't have a size, it doesn't matter that much as the total size is only for display totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0)); - progress.accept(new BrowserTransferProgress(source.getName() + " ...", 0, totalSize.get())); + updateProgress(new BrowserTransferProgress(source.getName() + " ...", 0, totalSize.get())); } return true; }); @@ -298,7 +301,7 @@ public class BrowserFileTransferOperation { // Source might have been deleted meanwhile var exists = source.getFileSystem().fileExists(source.getPath()); if (!exists) { - progress.accept(BrowserTransferProgress.finished(source.getName(), 0)); + updateProgress(BrowserTransferProgress.finished(source.getName(), 0)); return; } @@ -307,7 +310,7 @@ public class BrowserFileTransferOperation { totalSize.addAndGet(source.getFileSizeLong().orElse(0)); } else { // Unsupported type, e.g. a socket - progress.accept(BrowserTransferProgress.finished(source.getName(), 0)); + updateProgress(BrowserTransferProgress.finished(source.getName(), 0)); return; } @@ -334,6 +337,8 @@ public class BrowserFileTransferOperation { throw new IllegalStateException(); } + var accurateTransferProgress = + optimizedSourceFs.hasAccurateProgress() && targetFs.hasAccurateProgress(); if (sourceFile.getKind() == FileKind.DIRECTORY) { targetFs.mkdirs(targetFile); } else if (sourceFile.getKind() == FileKind.FILE) { @@ -350,7 +355,60 @@ public class BrowserFileTransferOperation { } } - transfer(sourceFile.getPath(), optimizedSourceFs, targetFile, targetFs, transferred, totalSize); + var transferRunning = new AtomicBoolean(true); + var sourceId = optimizedSourceFs.getIdentifier(); + var targetId = targetFs.getIdentifier(); + var fileSize = optimizedSourceFs.getFileSize(sourceFile.getPath()); + var startTransferred = transferred.get(); + var canCacheProgressInfo = sourceId.isPresent() && targetId.isPresent(); + if (!accurateTransferProgress && canCacheProgressInfo) { + var mapKey = sourceId.get() + "-" + targetId.get(); + Long cachedSpeed = + AppCache.getNonNullMapEntry("transferSpeedEstimate", mapKey, Long.class, () -> null); + var counter = new AtomicInteger(); + if (cachedSpeed != null) { + GlobalTimer.scheduleUntil(Duration.ofMillis(100), false, () -> { + if (!transferRunning.get()) { + return true; + } + + // Divide by 9 and not 10 to overreport progress a bit. Better than underreporting + var addedProgress = + Math.min(fileSize, (long) counter.incrementAndGet() * cachedSpeed / 9); + updateProgress(new BrowserTransferProgress( + sourceFile.getName(), startTransferred + addedProgress, totalSize.get())); + return false; + }); + } else { + updateProgress(new BrowserTransferProgress(sourceFile.getName(), 0, 0)); + } + } else if (!accurateTransferProgress) { + updateProgress(new BrowserTransferProgress(sourceFile.getName(), 0, 0)); + } + + var startProgress = + new BrowserTransferProgress(source.getName(), transferred.get(), totalSize.get()); + try { + transfer( + sourceFile.getPath(), + optimizedSourceFs, + targetFile, + targetFs, + transferred, + totalSize, + fileSize, + accurateTransferProgress); + } finally { + transferRunning.set(false); + } + + if (sourceId.isPresent() && targetId.isPresent()) { + var mapKey = sourceId.get() + "-" + targetId.get(); + var speed = BrowserTransferProgress.estimateTransferSpeed( + List.of(startProgress), + new BrowserTransferProgress(sourceFile.getName(), transferred.get(), totalSize.get())); + AppCache.updateMapEntry("transferSpeedEstimate", mapKey, speed); + } } } } finally { @@ -407,19 +465,23 @@ public class BrowserFileTransferOperation { FilePath targetFile, FileSystem targetFs, AtomicLong transferred, - AtomicLong totalSize) + AtomicLong totalSize, + long fileSize, + boolean reportProgress) throws Exception { if (cancelled()) { return; } - updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), 0, 0)); - - var fileSize = sourceFs.getFileSize(sourceFile); + if (reportProgress) { + updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), totalSize.get())); + } if (transferInline(sourceFile, sourceFs, targetFile, targetFs) || cancelled()) { if (!cancelled()) { - updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), fileSize)); + transferred.addAndGet(fileSize); + updateProgress( + new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), totalSize.get())); } return; } @@ -442,7 +504,7 @@ public class BrowserFileTransferOperation { } outputStream = targetFs.openOutput(targetFile, fileSize); - transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, fileSize); + transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, fileSize, reportProgress); } catch (Exception ex) { // Mark progress as finished to reset any progress display updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), transferred.get())); @@ -516,10 +578,13 @@ public class BrowserFileTransferOperation { OutputStream outputStream, AtomicLong transferred, AtomicLong total, - long expectedFileSize) + long expectedFileSize, + boolean reportProgress) throws Exception { - // Initialize progress immediately prior to reading anything - updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get())); + if (reportProgress) { + // Initialize progress immediately prior to reading anything + updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get())); + } var killStreams = new AtomicBoolean(false); var exception = new AtomicReference(); @@ -543,8 +608,10 @@ public class BrowserFileTransferOperation { outputStream.write(buffer, 0, read); transferred.addAndGet(read); readCount.addAndGet(read); - updateProgress( - new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get())); + if (reportProgress) { + updateProgress( + new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get())); + } } outputStream.flush(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedState.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedState.java index 90ff71084..1f7314528 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedState.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedState.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser.file; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.collections.ObservableList; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java index 49b01704d..84ded07bb 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java @@ -1,20 +1,20 @@ package io.xpipe.app.browser.file; import io.xpipe.app.core.AppCache; -import io.xpipe.core.JacksonMapper; +import io.xpipe.app.util.JacksonMapper; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.SneakyThrows; import lombok.Value; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.node.ObjectNode; import java.util.List; 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 c63f13604..0bb6461cb 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 @@ -6,6 +6,7 @@ import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.SimpleRegionBuilder; import io.xpipe.app.comp.base.*; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppSizeBreakpoints; import io.xpipe.app.platform.DerivedObservableList; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.prefs.AppPrefs; @@ -18,7 +19,6 @@ import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.layout.*; @@ -70,7 +70,7 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder { var state = BrowserHistorySavedStateImpl.get(); var welcome = new BrowserGreetingComp(); - var header = new LabelComp(AppI18n.observable("browserWelcomeSystems")); + var header = new LabelComp(AppI18n.observable("browserWelcomeSystems")).apply(label -> label.setWrapText(true)); var vbox = new VerticalComp(List.of(welcome, RegionBuilder.vspacer(4), header)); vbox.apply(struc -> struc.setAlignment(Pos.CENTER_LEFT)); @@ -108,7 +108,6 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder { layout.style("welcome"); layout.spacing(14); layout.maxWidth(1000); - layout.padding(new Insets(45, 40, 40, 50)); layout.apply(struc -> { struc.setMaxWidth(1000); }); @@ -147,7 +146,12 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder { } }); }) - .minWidth(300) + .apply(button -> button.minWidthProperty() + .bind(Bindings.createDoubleBinding( + () -> { + return AppSizeBreakpoints.portraitMode().get() ? 170.0 : 300.0; + }, + AppSizeBreakpoints.portraitMode()))) .describe( d -> d.name(new ReadOnlyStringWrapper(DataStorage.get().getStoreEntryDisplayName(entry.get())))) .disable(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 08e157e63..fd0969b0d 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,10 +1,10 @@ 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.FilePath; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; +import io.xpipe.app.fs.FileSystem; +import io.xpipe.app.store.LocalStore; +import io.xpipe.app.util.FilePath; import java.nio.file.Files; import java.nio.file.Path; 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 3998f587f..ba524d1a9 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 @@ -4,11 +4,12 @@ import io.xpipe.app.browser.icon.BrowserIconManager; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.RegionStructure; import io.xpipe.app.comp.RegionStructureBuilder; -import io.xpipe.app.comp.augment.ContextMenuAugment; import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.comp.base.ContextMenuAugment; import io.xpipe.app.comp.base.PrettyImageHelper; import io.xpipe.app.comp.base.TextFieldComp; import io.xpipe.app.core.AppFontSizes; +import io.xpipe.app.core.AppSizeBreakpoints; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.platform.MenuHelper; import io.xpipe.app.platform.PlatformThread; @@ -93,8 +94,10 @@ public class BrowserNavBarComp extends RegionStructureBuilder { return !pathRegion.isFocused() - && !model.getInOverview().get(); + && !model.getInOverview().get() + && !AppSizeBreakpoints.portraitMode().get(); }, + AppSizeBreakpoints.portraitMode(), pathRegion.focusedProperty(), PlatformThread.sync(model.getInOverview()))); var stack = new StackPane(pathRegion, breadcrumbsRegion); @@ -167,7 +170,10 @@ public class BrowserNavBarComp extends RegionStructureBuilder { struc.focusedProperty().subscribe(val -> { struc.pseudoClassStateChanged( - INVISIBLE, !val && !model.getInOverview().get()); + INVISIBLE, + !val + && !model.getInOverview().get() + && !AppSizeBreakpoints.portraitMode().get()); if (val) { Platform.runLater(() -> { @@ -176,6 +182,14 @@ public class BrowserNavBarComp extends RegionStructureBuilder { + struc.pseudoClassStateChanged( + INVISIBLE, + !struc.isFocused() + && !model.getInOverview().get() + && !AppSizeBreakpoints.portraitMode().get()); + }); + struc.addEventHandler(KeyEvent.KEY_PRESSED, ke -> { if (ke.getCode().equals(KeyCode.ENTER)) { ke.consume(); @@ -186,7 +200,11 @@ public class BrowserNavBarComp extends RegionStructureBuilder { - struc.pseudoClassStateChanged(INVISIBLE, !val && !struc.isFocused()); + struc.pseudoClassStateChanged( + INVISIBLE, + !val + && !struc.isFocused() + && !AppSizeBreakpoints.portraitMode().get()); }); }); }); 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 7b580cb74..a9afc3dce 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 @@ -5,11 +5,11 @@ import io.xpipe.app.comp.SimpleRegionBuilder; 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.fs.FileEntry; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.platform.DerivedObservableList; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.FilePath; import javafx.application.Platform; import javafx.beans.binding.Bindings; 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 9d709f256..745ece035 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 @@ -3,8 +3,8 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.icon.BrowserIcons; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.app.ext.FileEntry; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.BooleanAnimationTimer; import io.xpipe.app.platform.InputHelper; import io.xpipe.app.prefs.AppPrefs; 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 67765907b..839d2bced 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 @@ -3,7 +3,7 @@ package io.xpipe.app.browser.file; import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.SimpleRegionBuilder; -import io.xpipe.app.comp.augment.ContextMenuAugment; +import io.xpipe.app.comp.base.ContextMenuAugment; import io.xpipe.app.comp.base.HorizontalComp; import io.xpipe.app.comp.base.IconButtonComp; import io.xpipe.app.comp.base.LabelComp; @@ -39,8 +39,9 @@ public class BrowserStatusBarComp extends SimpleRegionBuilder { createClipboardStatus(), createSelectionStatus(), createKillButton())); + bar.minWidth(0); bar.spacing(15); - bar.style("status-bar"); + bar.style("status-bar").style("color-box"); bar.apply(struc -> { struc.widthProperty().subscribe(value -> { @@ -102,7 +103,7 @@ public class BrowserStatusBarComp extends SimpleRegionBuilder { // Handle unknown transfers if (p.getTotal() == 0) { - return HumanReadableFormat.byteCount(p.getTransferred()); + return "..."; } var elapsed = (p.getTotal() - p.getTransferred() / (double) p.getTotal()) * expected.toMillis(); 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 2133085ad..a500b8f1a 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 @@ -42,10 +42,8 @@ public class BrowserTransferComp extends SimpleRegionBuilder { .apply(struc -> struc.setTextAlignment(TextAlignment.CENTER)) .apply(struc -> struc.setContentDisplay(ContentDisplay.TOP)) .visible(model.getEmpty()); - var backgroundStack = new StackComp(List.of(background)) - .style("color-box") - .style("gray") - .style("download-background"); + var backgroundStack = + new StackComp(List.of(background)).style("color-box").style("download-background"); var binding = DerivedObservableList.wrap(model.getItems(), true) .mapped(item -> item.getBrowserEntry()) diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java index 8fb8b20dd..7e534c642 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java @@ -29,6 +29,14 @@ public class BrowserTransferProgress { return 0; } + if (list.size() == 1) { + var ms = Math.max( + 1, + now.getTimestamp().toEpochMilli() + - list.getFirst().getTimestamp().toEpochMilli()); + return Math.round((now.getTransferred() - list.getFirst().getTransferred()) / (ms / 1000.0)); + } + var rSize = list.size() > 1 ? list.size() - 1 : list.size(); var r = new double[rSize]; for (int i = 0; i < rSize; i++) { 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 c00044e22..2f7f7d1ba 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 @@ -1,8 +1,8 @@ package io.xpipe.app.browser.icon; import io.xpipe.app.core.AppResources; -import io.xpipe.app.ext.FileEntry; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; import java.io.BufferedReader; import java.io.InputStreamReader; 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 21ffa9073..8b7159b48 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 @@ -1,8 +1,8 @@ package io.xpipe.app.browser.icon; import io.xpipe.app.core.AppResources; -import io.xpipe.app.ext.FileEntry; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileEntry; +import io.xpipe.app.fs.FileKind; import lombok.Getter; 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 8564b3b03..b5b6afdf0 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 @@ -8,6 +8,7 @@ import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.comp.RegionDescriptor; import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.storage.DataStoreEntryRef; import javafx.scene.control.Button; @@ -88,7 +89,9 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider { b.setDisable(!isActive(model)); model.getCurrentPath().addListener((observable, oldValue, newValue) -> { - b.setDisable(!isActive(model)); + PlatformThread.runLaterIfNeeded(() -> { + b.setDisable(!isActive(model)); + }); }); return b; 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 9921cc458..9ac3c1185 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 @@ -8,7 +8,7 @@ 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 io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; 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 7b10a395f..07361e54c 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,7 +10,7 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.property.SimpleStringProperty; 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 4c6b8c3e2..c18bdfbdc 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,7 +10,7 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.property.SimpleStringProperty; 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 af2d7f0ab..aaf53572b 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,7 +10,7 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.property.SimpleStringProperty; 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 68ef93f9c..8a5710485 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,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.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; 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 da6546531..20b8db891 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,7 +6,7 @@ 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.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.ClipboardHelper; import io.xpipe.app.platform.LabelGraphic; 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 d053a8656..63191332f 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 @@ -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.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; 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 2f47a126a..04f0fd4ea 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,11 +6,11 @@ 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.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.OsType; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -48,10 +48,10 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider { @Override public KeyCombination getShortcut() { return switch (OsType.ofLocal()) { - case OsType.Linux linux -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN); - case OsType.MacOs macOs -> + case OsType.Linux ignored -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN); + case OsType.MacOs ignored -> new KeyCodeCombination(KeyCode.DOWN, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN); - case OsType.Windows windows -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN); + case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN); }; } 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 2fbd54a1b..786817976 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,7 +5,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.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/GradleRunMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/GradleRunMenuProvider.java index b6ec02b05..00a60ea16 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/GradleRunMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/GradleRunMenuProvider.java @@ -7,10 +7,10 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.process.CommandBuilder; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; 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 305901803..cf5170381 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,10 +12,10 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.platform.OptionsBuilder; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; 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 f26b838d1..cf8e87588 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,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.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; 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 588d3dd62..d9e95bcb4 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 @@ -8,7 +8,7 @@ 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 io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; 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 a6710e8ea..349b19676 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,9 +7,9 @@ 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.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; 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 3d11c58cd..c22dc2191 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 @@ -8,7 +8,7 @@ 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 io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; 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 d30887909..cc5c45c6b 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 @@ -5,10 +5,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.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.prefs.AppPrefs; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; 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 0e54c057c..eabbc178d 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,7 +7,7 @@ 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.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import javafx.beans.value.ObservableValue; 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 36d6b3227..d57246c4c 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,9 +5,9 @@ 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.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -46,9 +46,9 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider { @Override public KeyCombination getShortcut() { return switch (OsType.ofLocal()) { - case OsType.Linux linux -> new KeyCodeCombination(KeyCode.F2); - case OsType.MacOs macOs -> new KeyCodeCombination(KeyCode.ENTER); - case OsType.Windows windows -> new KeyCodeCombination(KeyCode.F2); + case OsType.Linux ignored -> new KeyCodeCombination(KeyCode.F2); + case OsType.MacOs ignored -> new KeyCodeCombination(KeyCode.ENTER); + case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.F2); }; } 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 b3cff50d6..54c011be6 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 @@ -5,12 +5,12 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel; 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.fs.FileEntry; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ShellDialects; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; 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 9fc8d2ae4..40b65dc07 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 @@ -11,7 +11,7 @@ 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.core.FilePath; +import io.xpipe.app.util.FilePath; import javafx.beans.value.ObservableValue; 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 6b8024dbb..1329b380b 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 @@ -11,7 +11,7 @@ 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.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; 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 986a7497b..899cb4bfa 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 @@ -10,7 +10,7 @@ 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.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; 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 7c2b95851..41e6983ee 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,9 +6,9 @@ import io.xpipe.app.browser.menu.*; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.FileKind; +import io.xpipe.app.fs.FileKind; import io.xpipe.app.platform.LabelGraphic; -import io.xpipe.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -109,7 +109,8 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { - var name = new SimpleStringProperty(entries.size() == 1 ? entries.getFirst().getFileName() + "." + getExtension() : null); + var name = new SimpleStringProperty( + entries.size() == 1 ? entries.getFirst().getFileName() + "." + getExtension() : null); var modal = ModalOverlay.of( "archiveName", RegionBuilder.of(() -> { @@ -201,7 +202,8 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return entries.stream().allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.FILE); + return entries.stream() + .allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.FILE); } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java index 8e4f54bfb..5ef22b080 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java @@ -3,12 +3,8 @@ 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.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.process.CommandBuilder; -import io.xpipe.app.process.ShellControl; -import io.xpipe.app.process.ShellDialects; -import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; +import io.xpipe.app.util.FilePath; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java index 7bb6ada13..dcaae7278 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java @@ -10,8 +10,7 @@ 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.core.OsType; +import io.xpipe.app.util.OsType; import javafx.beans.value.ObservableValue; @@ -39,8 +38,8 @@ public class GunzipUnixMenuProvider implements BrowserMenuLeafProvider, BrowserA var dir = entries.size() > 1 ? "[...]" : GunzipActionProvider.getTarget( - entries.getFirst().getRawFileEntry().getPath()) - .getFileName(); + entries.getFirst().getRawFileEntry().getPath()) + .getFileName(); return AppI18n.observable("gunzipDirectory", dir); } @@ -56,15 +55,14 @@ public class GunzipUnixMenuProvider implements BrowserMenuLeafProvider, BrowserA return false; } - return entries.stream() - .allMatch(entry -> { - var s = entry.getRawFileEntry().getPath().toString(); - if (s.endsWith(".tar.gz") || s.endsWith(".tgz") || s.equals("tar.gzip")) { - return false; - } + return entries.stream().allMatch(entry -> { + var s = entry.getRawFileEntry().getPath().toString(); + if (s.endsWith(".tar.gz") || s.endsWith(".tgz") || s.equals("tar.gzip")) { + return false; + } - return s.endsWith(".gz") || s.endsWith(".gzip"); - }) + return s.endsWith(".gz") || s.endsWith(".gzip"); + }) && model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java index d692909cc..1ab589db6 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.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.process.CommandBuilder; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import lombok.NonNull; import lombok.experimental.SuperBuilder; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java index 88871e3c9..94b964da5 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.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.process.CommandBuilder; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import lombok.NonNull; import lombok.experimental.SuperBuilder; 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 aff0d2601..aaf56bff8 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 @@ -5,7 +5,7 @@ import io.xpipe.app.browser.action.BrowserActionProvider; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ShellControl; -import io.xpipe.core.FilePath; +import io.xpipe.app.util.FilePath; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java index ae99147f7..b5d404d8d 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java @@ -7,8 +7,8 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ShellControl; import io.xpipe.app.process.ShellDialects; -import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; 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 4a006374f..4cb41c50b 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,12 +3,12 @@ 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.fs.FileKind; import io.xpipe.app.process.CommandBuilder; import io.xpipe.app.process.ProcessOutputException; import io.xpipe.app.process.ShellDialects; -import io.xpipe.core.FilePath; -import io.xpipe.core.OsType; +import io.xpipe.app.util.FilePath; +import io.xpipe.app.util.OsType; import lombok.NonNull; import lombok.experimental.SuperBuilder; 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 942815801..3421cd0cb 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 @@ -12,6 +12,7 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.GlobalTimer; import javafx.animation.*; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; @@ -51,10 +52,17 @@ public class AppMainWindowContentComp extends SimpleRegionBuilder { 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; + Color color; + if (AppPrefs.get() != null) { + var baseColor = + AppPrefs.get().theme().getValue().getEmphasisColor().get(); + if (baseColor == null) { + baseColor = Platform.getPreferences().getAccentColor(); + } + color = ColorHelper.withOpacity(baseColor, dark ? 0.7 : 0.85); + } else { + color = Color.TRANSPARENT; + } DropShadow shadow = new DropShadow(); shadow.setRadius(10); shadow.setColor(color); @@ -101,9 +109,10 @@ public class AppMainWindowContentComp extends SimpleRegionBuilder { var loadingTextCounter = new SimpleIntegerProperty(); GlobalTimer.scheduleUntil(Duration.ofMillis(500), false, () -> { - if (loaded.getValue() != null) { - return true; - } + // We want to keep this running for animating the status on shutdown + // if (loaded.getValue() != null) { + // return true; + // } loadingTextCounter.set((loadingTextCounter.get() + 1) % 4); return false; @@ -114,9 +123,16 @@ public class AppMainWindowContentComp extends SimpleRegionBuilder { if (base == null) { return null; } + + var content = loaded.getValue(); + if (content != null) { + return null; + } + return base + " " + (".".repeat(loadingTextCounter.get())) + (" ".repeat(3 - loadingTextCounter.get())); }, + loaded, AppMainWindow.getLoadingText(), loadingTextCounter); var text = new LabelComp(loadingTextAnimated); diff --git a/app/src/main/java/io/xpipe/app/comp/base/BusyButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/BusyButtonComp.java new file mode 100644 index 000000000..087fa8deb --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/BusyButtonComp.java @@ -0,0 +1,68 @@ +package io.xpipe.app.comp.base; + +import io.xpipe.app.comp.RegionBuilder; +import io.xpipe.app.core.AppFontSizes; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.platform.PlatformThread; +import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.FailableSupplier; +import io.xpipe.app.util.ThreadHelper; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.layout.Region; + +import atlantafx.base.theme.Styles; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.concurrent.atomic.AtomicReference; + +@Getter +@AllArgsConstructor +public class BusyButtonComp extends RegionBuilder