mirror of
https://github.com/xpipe-io/xpipe.git
synced 2025-04-21 03:33:39 +00:00
Compare commits
No commits in common. "master" and "15.0.2" have entirely different histories.
203 changed files with 1724 additions and 3942 deletions
74
README.md
74
README.md
|
@ -13,15 +13,14 @@ XPipe is a new type of shell connection hub and remote file manager that allows
|
|||
XPipe fully integrates with your tools such as your favourite text/code editors, terminals, shells, command-line tools and more. The platform is designed to be extensible, allowing anyone to add easily support for more tools or to implement custom functionality through a modular extension system.
|
||||
|
||||
It currently supports:
|
||||
|
||||
- [SSH](https://docs.xpipe.io/guide/ssh) connections, config files, and tunnels
|
||||
- [Docker](https://docs.xpipe.io/guide/docker), [Podman](https://docs.xpipe.io/guide/podman), [LXD](https://docs.xpipe.io/guide/lxc), and [incus](https://docs.xpipe.io/guide/lxc) containers
|
||||
- [Proxmox PVE](https://docs.xpipe.io/guide/proxmox) virtual machines and containers
|
||||
- [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines
|
||||
- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers
|
||||
- [Tailscale](https://docs.xpipe.io/guide/tailscale) and [Teleport](https://docs.xpipe.io/guide/teleport) connections
|
||||
- Windows Subsystem for Linux, Cygwin, and MSYS2 environments
|
||||
- Powershell Remote Sessions
|
||||
- [SSH](https://www.ssh.com/academy/ssh/protocol) connections, config files, and tunnels
|
||||
- [Docker](https://www.docker.com/), [Podman](https://podman.io/), [LXD](https://linuxcontainers.org/lxd/introduction/), and [incus](https://linuxcontainers.org/incus/) container instances located on any host
|
||||
- [Proxmox PVE](https://www.proxmox.com/en/proxmox-virtual-environment/overview) virtual machines and containers
|
||||
- [Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/about/), [KVM/QEMU](https://linux-kvm.org/page/Main_Page), [VMware Player/Workstation/Fusion](https://www.vmware.com/products/desktop-hypervisor/workstation-and-fusion) virtual machines
|
||||
- [Kubernetes](https://kubernetes.io/) clusters, pods, and containers
|
||||
- [Windows Subsystem for Linux](https://ubuntu.com/wsl), [Cygwin](https://www.cygwin.com/), and [MSYS2](https://www.msys2.org/) instances
|
||||
- [Powershell Remote Sessions](https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.3)
|
||||
- [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh) and [Teleport](https://goteleport.com/) connections
|
||||
- RDP and VNC connections
|
||||
|
||||
## Connection hub
|
||||
|
@ -77,6 +76,13 @@ It currently supports:
|
|||
- There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services.
|
||||
- Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems
|
||||
|
||||
## Programmatic connection control via the API
|
||||
|
||||
- The XPipe API provides programmatic access to XPipe’s features via an HTTP interface
|
||||
- Manage all your remote systems and access their file systems in your own favorite programming language
|
||||
- Either call the API directly or with the help of the [python library](https://github.com/xpipe-io/xpipe-python-api)
|
||||
- Full documentation can be either found in the application itself under the API tab or in the [OpenAPI](/openapi.yaml) spec file
|
||||
|
||||
# Downloads
|
||||
|
||||
Note that this is a desktop application that should be run on your local desktop workstation, not on any server or containers. It will be able to connect to your server infrastructure from there.
|
||||
|
@ -87,6 +93,12 @@ Installers are the easiest way to get started and come with an optional automati
|
|||
|
||||
- [Windows .msi Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-x86_64.msi)
|
||||
|
||||
You can also install XPipe by pasting the installation command into your terminal. This will perform the .msi setup for the current user automatically:
|
||||
|
||||
```
|
||||
powershell -ExecutionPolicy Bypass -Command iwr "https://github.com/xpipe-io/xpipe/raw/master/get-xpipe.ps1" -OutFile "$env:TEMP\get-xpipe.ps1" ";" "&" "$env:TEMP\get-xpipe.ps1"
|
||||
```
|
||||
|
||||
If you don't like installers, you can also use a portable version that is packaged as an archive:
|
||||
|
||||
- [Windows .zip Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-x86_64.zip)
|
||||
|
@ -95,20 +107,6 @@ Alternatively, you can also use the following package managers:
|
|||
- [choco](https://community.chocolatey.org/packages/xpipe) to install it with `choco install xpipe`.
|
||||
- [winget](https://github.com/microsoft/winget-cli) to install it with `winget install xpipe-io.xpipe --source winget`.
|
||||
|
||||
## macOS
|
||||
|
||||
Installers are the easiest way to get started and come with an optional automatic update functionality:
|
||||
|
||||
- [MacOS .pkg Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-x86_64.pkg)
|
||||
- [MacOS .pkg Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-arm64.pkg)
|
||||
|
||||
If you don't like installers, you can also use a portable version that is packaged as an archive:
|
||||
|
||||
- [MacOS .dmg Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-x86_64.dmg)
|
||||
- [MacOS .dmg Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-arm64.dmg)
|
||||
|
||||
Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-tap) to install XPipe with `brew install --cask xpipe-io/tap/xpipe`.
|
||||
|
||||
## Linux
|
||||
|
||||
You can install XPipe the fastest by pasting the installation command into your terminal. This will perform the setup automatically.
|
||||
|
@ -168,11 +166,35 @@ Alternatively, there are also AppImages available:
|
|||
Note that the portable version assumes that you have some basic packages for graphical systems already installed
|
||||
as it is not a perfect standalone version. It should however run on most systems.
|
||||
|
||||
## Docker container
|
||||
## macOS
|
||||
|
||||
XPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe.
|
||||
Installers are the easiest way to get started and come with an optional automatic update functionality:
|
||||
|
||||
Since it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured.
|
||||
- [MacOS .pkg Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-x86_64.pkg)
|
||||
- [MacOS .pkg Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-macos-arm64.pkg)
|
||||
|
||||
You also can install XPipe by pasting the installation command into your terminal. This will perform the `.pkg` installation automatically:
|
||||
|
||||
```
|
||||
bash <(curl -sL https://github.com/xpipe-io/xpipe/raw/master/get-xpipe.sh)
|
||||
```
|
||||
|
||||
If you don't like installers, you can also use a portable version that is packaged as an archive:
|
||||
|
||||
- [MacOS .dmg Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-x86_64.dmg)
|
||||
- [MacOS .dmg Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-macos-arm64.dmg)
|
||||
|
||||
Alternatively, you can also use [Homebrew](https://github.com/xpipe-io/homebrew-tap) to install XPipe with `brew install --cask xpipe-io/tap/xpipe`.
|
||||
|
||||
## Early access releases
|
||||
|
||||
Prior to major releases, there will be several Public Test Build (PTB) releases published at https://github.com/xpipe-io/xpipe-ptb to see whether everything is production ready and contain the latest new features.
|
||||
|
||||
In case you're interested in trying out the PTB versions, you can easily do so without any limitations. The regular releases and PTB releases are designed to not interfere with each other and can therefore be installed and used side by side.
|
||||
|
||||
## XPipe Webtop
|
||||
|
||||
XPipe is a desktop application first and foremost. It requires a full desktop environment to function with various installed applications such as terminals, editors, shells, CLI tools, and more. So there is no true web-based interface for XPipe. Since it might make sense however to access your XPipe environment from the web, there is also a so-called webtop docker container image for XPipe. [XPipe Webtop](https://github.com/xpipe-io/xpipe-webtop) is a web-based desktop environment that can be run in a container and accessed from a browser via KasmVNC. The desktop environment comes with XPipe and various terminals and editors preinstalled and configured.
|
||||
|
||||
# Further information
|
||||
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
Due to its nature, XPipe has to handle a lot of sensitive information. Therefore, the security, integrity, and privacy of your data has topmost priority.
|
||||
|
||||
More information about the security approach of the XPipe application can be found on the documentation website at https://docs.xpipe.io/reference/security.
|
||||
General information about the security approach of the XPipe application can be found on the website at https://xpipe.io/features#security. If you're interested in security implementation details, you can find them at https://docs.xpipe.io/security.
|
||||
|
||||
You can report security vulnerabilities in this GitHub repository in a confidential manner. We will get back to you as soon as possible if you do.
|
||||
|
|
|
@ -48,7 +48,7 @@ dependencies {
|
|||
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
|
||||
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
|
||||
|
||||
api("com.github.weisj:jsvg:1.7.1")
|
||||
api("com.github.weisj:jsvg:1.7.0")
|
||||
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
|
||||
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
|
|
|
@ -20,9 +20,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AppBeaconServer {
|
||||
|
@ -36,7 +34,6 @@ public class AppBeaconServer {
|
|||
private final boolean propertyPort;
|
||||
|
||||
private boolean running;
|
||||
private ExecutorService executor;
|
||||
private HttpServer server;
|
||||
|
||||
@Getter
|
||||
|
@ -108,11 +105,7 @@ public class AppBeaconServer {
|
|||
}
|
||||
|
||||
running = false;
|
||||
server.stop(0);
|
||||
executor.shutdown();
|
||||
try {
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ignored) {}
|
||||
server.stop(1);
|
||||
}
|
||||
|
||||
private void initAuthSecret() throws IOException {
|
||||
|
@ -134,7 +127,12 @@ public class AppBeaconServer {
|
|||
}
|
||||
|
||||
private void start() throws IOException {
|
||||
executor = Executors.newFixedThreadPool(5, r -> {
|
||||
server = HttpServer.create(
|
||||
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
|
||||
BeaconInterface.getAll().forEach(beaconInterface -> {
|
||||
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
|
||||
});
|
||||
server.setExecutor(Executors.newFixedThreadPool(5, r -> {
|
||||
Thread t = Executors.defaultThreadFactory().newThread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("http handler");
|
||||
|
@ -142,13 +140,7 @@ public class AppBeaconServer {
|
|||
ErrorEvent.fromThrowable(e).handle();
|
||||
});
|
||||
return t;
|
||||
});
|
||||
server = HttpServer.create(
|
||||
new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10);
|
||||
BeaconInterface.getAll().forEach(beaconInterface -> {
|
||||
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
|
||||
});
|
||||
server.setExecutor(executor);
|
||||
}));
|
||||
|
||||
var resourceMap = Map.of(
|
||||
"openapi.yaml", "misc/openapi.yaml",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.beacon.api.ConnectionQueryExchange;
|
||||
|
||||
|
@ -15,7 +14,43 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
|
|||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
var found = DataStorageQuery.query(msg.getCategoryFilter(), msg.getConnectionFilter(), msg.getTypeFilter());
|
||||
var catMatcher = Pattern.compile(
|
||||
toRegex("all connections/" + msg.getCategoryFilter().toLowerCase()));
|
||||
var conMatcher = Pattern.compile(toRegex(msg.getConnectionFilter().toLowerCase()));
|
||||
var typeMatcher = Pattern.compile(toRegex(msg.getTypeFilter().toLowerCase()));
|
||||
|
||||
List<DataStoreEntry> found = new ArrayList<>();
|
||||
for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {
|
||||
if (!storeEntry.getValidity().isUsable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = DataStorage.get().getStorePath(storeEntry).toString();
|
||||
if (!conMatcher.matcher(name).matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cat = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(storeEntry.getCategoryUuid())
|
||||
.orElse(null);
|
||||
if (cat == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var c = DataStorage.get().getStorePath(cat).toString();
|
||||
if (!catMatcher.matcher(c).matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!typeMatcher
|
||||
.matcher(storeEntry.getProvider().getId().toLowerCase())
|
||||
.matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
found.add(storeEntry);
|
||||
}
|
||||
|
||||
return Response.builder()
|
||||
.found(found.stream().map(entry -> entry.getUuid()).toList())
|
||||
.build();
|
||||
|
@ -25,4 +60,85 @@ public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
|
|||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
|
||||
private String toRegex(String pattern) {
|
||||
// https://stackoverflow.com/a/17369948/6477761
|
||||
StringBuilder sb = new StringBuilder(pattern.length());
|
||||
int inGroup = 0;
|
||||
int inClass = 0;
|
||||
int firstIndexInClass = -1;
|
||||
char[] arr = pattern.toCharArray();
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
char ch = arr[i];
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
if (++i >= arr.length) {
|
||||
sb.append('\\');
|
||||
} else {
|
||||
char next = arr[i];
|
||||
switch (next) {
|
||||
case ',':
|
||||
// escape not needed
|
||||
break;
|
||||
case 'Q':
|
||||
case 'E':
|
||||
// extra escape needed
|
||||
sb.append('\\');
|
||||
default:
|
||||
sb.append('\\');
|
||||
}
|
||||
sb.append(next);
|
||||
}
|
||||
break;
|
||||
case '*':
|
||||
if (inClass == 0) sb.append(".*");
|
||||
else sb.append('*');
|
||||
break;
|
||||
case '?':
|
||||
if (inClass == 0) sb.append('.');
|
||||
else sb.append('?');
|
||||
break;
|
||||
case '[':
|
||||
inClass++;
|
||||
firstIndexInClass = i + 1;
|
||||
sb.append('[');
|
||||
break;
|
||||
case ']':
|
||||
inClass--;
|
||||
sb.append(']');
|
||||
break;
|
||||
case '.':
|
||||
case '(':
|
||||
case ')':
|
||||
case '+':
|
||||
case '|':
|
||||
case '^':
|
||||
case '$':
|
||||
case '@':
|
||||
case '%':
|
||||
if (inClass == 0 || (firstIndexInClass == i && ch == '^')) sb.append('\\');
|
||||
sb.append(ch);
|
||||
break;
|
||||
case '!':
|
||||
if (firstIndexInClass == i) sb.append('^');
|
||||
else sb.append('!');
|
||||
break;
|
||||
case '{':
|
||||
inGroup++;
|
||||
sb.append('(');
|
||||
break;
|
||||
case '}':
|
||||
inGroup--;
|
||||
sb.append(')');
|
||||
break;
|
||||
case ',':
|
||||
if (inGroup > 0) sb.append('|');
|
||||
else sb.append(',');
|
||||
break;
|
||||
default:
|
||||
sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.beacon.api.DaemonFocusExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
@ -10,11 +9,7 @@ public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
|
|||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
OperationMode.switchUp(OperationMode.GUI);
|
||||
var w = AppMainWindow.getInstance();
|
||||
if (w != null) {
|
||||
w.focus();
|
||||
}
|
||||
OperationMode.switchUp(OperationMode.map(msg.getMode()));
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import atlantafx.base.layout.ModalBox;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
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.terminal.TerminalLauncherManager;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.BeaconServerException;
|
||||
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
|
||||
var found = DataStorageQuery.queryUserInput(msg.getConnection());
|
||||
if (found.isEmpty()) {
|
||||
throw new BeaconClientException("No connection found for input " + msg.getConnection());
|
||||
}
|
||||
|
||||
if (found.size() > 1) {
|
||||
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
|
||||
}
|
||||
|
||||
var e = found.getFirst();
|
||||
var isShell = e.getStore() instanceof ShellStore;
|
||||
if (!isShell) {
|
||||
throw new BeaconClientException("Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
|
||||
}
|
||||
|
||||
if (!checkPermission()) {
|
||||
return Response.builder().command(List.of()).build();
|
||||
}
|
||||
|
||||
var r = TerminalLauncherManager.externalExchange(e.ref(), msg.getArguments());
|
||||
return Response.builder().command(r).build();
|
||||
}
|
||||
|
||||
private boolean checkPermission() {
|
||||
var cache = AppCache.getBoolean("externalLaunchPermitted", false);
|
||||
if (cache) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var r = AppDialog.confirm("externalLaunch");
|
||||
if (r) {
|
||||
AppCache.update("externalLaunchPermitted", true);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
}
|
|
@ -2,14 +2,13 @@ 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 {
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
|
||||
var r = TerminalLauncherManager.launchExchange(msg.getRequest());
|
||||
return Response.builder().targetFile(r).build();
|
||||
}
|
||||
|
|
|
@ -17,9 +17,11 @@ public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {
|
|||
var term = AppPrefs.get().terminalType().getValue();
|
||||
var unicode = term.supportsUnicode();
|
||||
var escapes = term.supportsEscapes();
|
||||
var finished = TerminalLauncherManager.isCompletedSuccessfully(msg.getRequest());
|
||||
return Response.builder()
|
||||
.supportsUnicode(unicode)
|
||||
.supportsEscapeSequences(escapes)
|
||||
.alreadyFinished(finished)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -24,9 +24,6 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
|
|||
|
||||
public void closeAsync(BrowserSessionTab e) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
// This is a bit ugly
|
||||
// If we die on tab init, wait a bit with closing to avoid removal while it is still being inited/added
|
||||
ThreadHelper.sleep(100);
|
||||
closeSync(e);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -73,7 +73,6 @@ public class BrowserFullSessionComp extends SimpleComp {
|
|||
var pinnedStack = createSplitStack(rightSplit, tabs);
|
||||
|
||||
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
|
||||
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
|
||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
||||
.withOnDividerChange(d -> {
|
||||
|
@ -148,8 +147,8 @@ public class BrowserFullSessionComp extends SimpleComp {
|
|||
var rec = new Rectangle();
|
||||
rec.widthProperty().bind(struc.get().widthProperty());
|
||||
rec.heightProperty().bind(struc.get().heightProperty());
|
||||
rec.setArcHeight(11);
|
||||
rec.setArcWidth(11);
|
||||
rec.setArcHeight(7);
|
||||
rec.setArcWidth(7);
|
||||
struc.get().getChildren().getFirst().setClip(rec);
|
||||
})
|
||||
.vgrow();
|
||||
|
@ -174,7 +173,6 @@ public class BrowserFullSessionComp extends SimpleComp {
|
|||
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
|
||||
var cache = new HashMap<BrowserSessionTab, Region>();
|
||||
var splitStack = new StackComp(List.of());
|
||||
splitStack.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
splitStack.apply(struc -> {
|
||||
model.getEffectiveRightTab().subscribe((newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
|
|
@ -156,8 +156,8 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
var os = fileList.getFileSystemModel()
|
||||
.getFileSystem()
|
||||
.getShell()
|
||||
.map(shellControl -> shellControl.getOsType())
|
||||
.orElse(null);
|
||||
.orElseThrow()
|
||||
.getOsType();
|
||||
table.widthProperty().subscribe((newValue) -> {
|
||||
if (os != OsType.WINDOWS && os != OsType.MACOS) {
|
||||
ownerCol.setVisible(newValue.doubleValue() > 1000);
|
||||
|
|
|
@ -53,7 +53,7 @@ public class BrowserFileSystemSavedState {
|
|||
|
||||
public BrowserFileSystemSavedState() {
|
||||
lastDirectory = null;
|
||||
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<>(STORED)));
|
||||
recentDirectories = FXCollections.observableList(new ArrayList<>(STORED));
|
||||
}
|
||||
|
||||
static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {
|
||||
|
@ -164,7 +164,7 @@ public class BrowserFileSystemSavedState {
|
|||
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
|
||||
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.synchronizedObservableList(FXCollections.observableList(cleaned)));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
|||
home,
|
||||
model.getCurrentPath().isNull(),
|
||||
fileList,
|
||||
model.getCurrentPath().isNull().not()), false);
|
||||
model.getCurrentPath().isNull().not()));
|
||||
var r = stack.styleClass("browser-content-container").createRegion();
|
||||
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.*;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
|
@ -12,10 +11,7 @@ import java.nio.file.Path;
|
|||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -417,60 +413,23 @@ public class BrowserFileTransferOperation {
|
|||
// Initialize progress immediately prior to reading anything
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
|
||||
var killStreams = new AtomicBoolean(false);
|
||||
var exception = new AtomicReference<Exception>();
|
||||
var thread = ThreadHelper.createPlatformThread("transfer", true, () -> {
|
||||
try {
|
||||
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
|
||||
byte[] buffer = new byte[bs];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
|
||||
if (cancelled()) {
|
||||
killStreams.set(true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!checkTransferValidity()) {
|
||||
killStreams.set(true);
|
||||
break;
|
||||
}
|
||||
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
exception.set(ex);
|
||||
}
|
||||
});
|
||||
|
||||
thread.start();
|
||||
while (true) {
|
||||
var alive = thread.isAlive();
|
||||
var cancelled = cancelled();
|
||||
|
||||
if (cancelled) {
|
||||
// Assume that the transfer has stalled if it doesn't finish until then
|
||||
thread.join(1000);
|
||||
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
|
||||
byte[] buffer = new byte[bs];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
|
||||
if (cancelled()) {
|
||||
killStreams();
|
||||
break;
|
||||
}
|
||||
|
||||
if (alive) {
|
||||
Thread.sleep(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (killStreams.get()) {
|
||||
if (!checkTransferValidity()) {
|
||||
killStreams();
|
||||
}
|
||||
|
||||
var ex = exception.get();
|
||||
if (ex != null) {
|
||||
throw ex;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
|
|||
ObservableList<Entry> lastSystems;
|
||||
|
||||
public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {
|
||||
this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));
|
||||
this.lastSystems = FXCollections.observableArrayList(lastSystems);
|
||||
}
|
||||
|
||||
private static BrowserHistorySavedStateImpl INSTANCE;
|
||||
|
|
|
@ -60,7 +60,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
|||
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
|
||||
map.put(emptyDisplay, empty);
|
||||
map.put(contentDisplay, empty.not());
|
||||
var stack = new MultiContentComp(map, false);
|
||||
var stack = new MultiContentComp(map);
|
||||
return stack.createRegion();
|
||||
}
|
||||
|
||||
|
@ -165,7 +165,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
|||
.accessibleText(e.getPath())
|
||||
.disable(disable)
|
||||
.styleClass("directory-button")
|
||||
.apply(struc -> struc.get().setMaxWidth(20000))
|
||||
.apply(struc -> struc.get().setMaxWidth(2000))
|
||||
.styleClass(Styles.RIGHT_PILL)
|
||||
.hgrow()
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
|
||||
|
|
|
@ -38,7 +38,7 @@ public class BrowserOverviewComp extends SimpleComp {
|
|||
|
||||
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
|
||||
|
||||
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
|
||||
var commonPlatform = FXCollections.<FileEntry>observableArrayList();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var common = sc.getOsType().determineInterestingPaths(sc).stream()
|
||||
.filter(s -> !s.isBlank())
|
||||
|
|
|
@ -4,11 +4,8 @@ import io.xpipe.app.browser.BrowserAbstractSessionModel;
|
|||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.BrowserSessionTab;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.AppMainWindowContentComp;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.terminal.TerminalDockComp;
|
||||
|
@ -21,7 +18,6 @@ import javafx.application.Platform;
|
|||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.Optional;
|
||||
|
@ -138,13 +134,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
|||
dockModel.toggleView(aBoolean);
|
||||
});
|
||||
});
|
||||
AppDialog.getModalOverlay().addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
if (c.getList().size() > 0) {
|
||||
dockModel.toggleView(false);
|
||||
} else {
|
||||
dockModel.toggleView(viewActive.get());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshShowingState() {
|
||||
|
|
|
@ -65,14 +65,13 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var p = sourceItem.get().getProgress().getValue();
|
||||
var hideProgress = sourceItem
|
||||
.get()
|
||||
.downloadFinished()
|
||||
.get();
|
||||
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
|
||||
var progressSuffix = hideProgress
|
||||
var progressSuffix = p == null
|
||||
|| sourceItem
|
||||
.get()
|
||||
.downloadFinished()
|
||||
.get()
|
||||
? ""
|
||||
: " " + share + "%";
|
||||
: " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
|
||||
return entry.getFileName() + progressSuffix;
|
||||
},
|
||||
sourceItem.get().getProgress());
|
||||
|
@ -82,14 +81,14 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
|
||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
|
||||
.apply(struc -> struc.get().setWrapText(true))
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()));
|
||||
.hide(model.getEmpty());
|
||||
|
||||
var clearButton = new IconButtonComp("mdi2c-close", () -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.clear(true);
|
||||
});
|
||||
})
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
|
||||
.hide(model.getEmpty())
|
||||
.tooltipKey("clearTransferDescription");
|
||||
|
||||
var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
|
||||
|
@ -97,7 +96,7 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
model.transferToDownloads();
|
||||
});
|
||||
})
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()))
|
||||
.hide(model.getEmpty())
|
||||
.tooltipKey("downloadStageDescription");
|
||||
|
||||
var bottom = new HorizontalComp(
|
||||
|
|
|
@ -8,9 +8,7 @@ import io.xpipe.app.util.ShellTemp;
|
|||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.collections.FXCollections;
|
||||
|
@ -36,7 +34,6 @@ public class BrowserTransferModel {
|
|||
BrowserFullSessionModel browserSessionModel;
|
||||
ObservableList<Item> items = FXCollections.observableArrayList();
|
||||
ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
|
||||
BooleanProperty transferring = new SimpleBooleanProperty();
|
||||
|
||||
public BrowserTransferModel(BrowserFullSessionModel browserSessionModel) {
|
||||
this.browserSessionModel = browserSessionModel;
|
||||
|
@ -50,9 +47,8 @@ public class BrowserTransferModel {
|
|||
}
|
||||
if (toDownload.isPresent()) {
|
||||
downloadSingle(toDownload.get());
|
||||
} else {
|
||||
ThreadHelper.sleep(20);
|
||||
}
|
||||
ThreadHelper.sleep(20);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
|
@ -130,7 +126,6 @@ public class BrowserTransferModel {
|
|||
}
|
||||
|
||||
try {
|
||||
transferring.setValue(true);
|
||||
var op = new BrowserFileTransferOperation(
|
||||
BrowserLocalFileSystem.getLocalFileEntry(TEMP),
|
||||
List.of(item.getBrowserEntry().getRawFileEntry()),
|
||||
|
@ -155,8 +150,6 @@ public class BrowserTransferModel {
|
|||
synchronized (items) {
|
||||
items.remove(item);
|
||||
}
|
||||
} finally {
|
||||
transferring.setValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -154,15 +154,7 @@ public abstract class Comp<S extends CompStructure<?>> {
|
|||
}
|
||||
|
||||
public Comp<S> disable(ObservableValue<Boolean> o) {
|
||||
return apply(struc -> {
|
||||
var region = struc.get();
|
||||
BindingsHelper.preserve(region, o);
|
||||
o.subscribe(n -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
region.setDisable(n);
|
||||
});
|
||||
});
|
||||
});
|
||||
return apply(struc -> struc.get().disableProperty().bind(o));
|
||||
}
|
||||
|
||||
public Comp<S> padding(Insets insets) {
|
||||
|
|
|
@ -22,6 +22,7 @@ public class AnchorComp extends Comp<CompStructure<AnchorPane>> {
|
|||
for (var c : comps) {
|
||||
pane.getChildren().add(c.createRegion());
|
||||
}
|
||||
pane.setPickOnBounds(false);
|
||||
return new SimpleCompStructure<>(pane);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
|
|||
return model.getSelected().getValue().equals(entry);
|
||||
},
|
||||
model.getSelected())));
|
||||
var multi = new MultiContentComp(map, true);
|
||||
var multi = new MultiContentComp(map);
|
||||
multi.styleClass("background");
|
||||
|
||||
var pane = new BorderPane();
|
||||
|
|
|
@ -6,7 +6,6 @@ import io.xpipe.app.core.AppFontSizes;
|
|||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.resources.AppImages;
|
||||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
@ -83,7 +82,6 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
|
||||
loaded.subscribe(struc -> {
|
||||
if (struc != null) {
|
||||
TrackEvent.info("Window content node set");
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
anim.stop();
|
||||
struc.prepareAddition();
|
||||
|
@ -92,7 +90,6 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
pane.getStyleClass().remove("background");
|
||||
pane.getChildren().remove(vbox);
|
||||
struc.show();
|
||||
TrackEvent.info("Window content node shown");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -110,6 +107,14 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
}
|
||||
});
|
||||
|
||||
loaded.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
Platform.runLater(() -> {
|
||||
stage.requestFocus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return pane;
|
||||
});
|
||||
var modal = new ModalOverlayStackComp(bg, overlay);
|
||||
|
|
|
@ -42,7 +42,7 @@ public class ComboTextFieldComp extends Comp<CompStructure<ComboBox<String>>> {
|
|||
});
|
||||
});
|
||||
text.setEditable(true);
|
||||
text.setMaxWidth(20000);
|
||||
text.setMaxWidth(2000);
|
||||
text.setValue(value.getValue() != null ? value.getValue() : null);
|
||||
text.valueProperty().addListener((c, o, n) -> {
|
||||
value.setValue(n != null && n.length() > 0 ? n : null);
|
||||
|
|
|
@ -81,7 +81,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> {
|
||||
if (!AppPrefs.get().enableGitStorage().get()) {
|
||||
AppLayoutModel.get().selectSettings();
|
||||
AppPrefs.get().selectCategory("vaultSync");
|
||||
AppPrefs.get().selectCategory("sync");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
|
|||
});
|
||||
var filter = new CustomTextField();
|
||||
filter.setMinHeight(0);
|
||||
filter.setMaxHeight(20000);
|
||||
filter.setMaxHeight(2000);
|
||||
filter.getStyleClass().add("filter-comp");
|
||||
filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
|
||||
filter.rightProperty()
|
||||
|
@ -67,7 +67,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
|
|||
filterText.subscribe(val -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
clear.setVisible(val != null);
|
||||
if (!Objects.equals(filter.getText(), val) && !(val == null && "".equals(filter.getText()))) {
|
||||
if (!Objects.equals(filter.getText(), val)) {
|
||||
filter.setText(val);
|
||||
}
|
||||
});
|
||||
|
|
29
app/src/main/java/io/xpipe/app/comp/base/GrowPaneComp.java
Normal file
29
app/src/main/java/io/xpipe/app/comp/base/GrowPaneComp.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GrowPaneComp extends Comp<CompStructure<Pane>> {
|
||||
|
||||
private final List<Comp<?>> comps;
|
||||
|
||||
public GrowPaneComp(List<Comp<?>> comps) {
|
||||
this.comps = List.copyOf(comps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<Pane> createBase() {
|
||||
var pane = new BorderPane();
|
||||
for (var c : comps) {
|
||||
pane.setCenter(c.createRegion());
|
||||
}
|
||||
pane.setPickOnBounds(false);
|
||||
return new SimpleCompStructure<>(pane);
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ public class IntComboFieldComp extends Comp<CompStructure<ComboBox<String>>> {
|
|||
text.setValue(value.getValue() != null ? value.getValue().toString() : null);
|
||||
text.setItems(FXCollections.observableList(
|
||||
predefined.stream().map(integer -> "" + integer).toList()));
|
||||
text.setMaxWidth(20000);
|
||||
text.setMaxWidth(2000);
|
||||
text.getStyleClass().add("int-combo-field-comp");
|
||||
text.setSkin(new ComboBoxListViewSkin<>(text));
|
||||
text.setVisibleRowCount(Math.min(10, predefined.size()));
|
||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.comp.CompStructure;
|
|||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.control.TextField;
|
||||
|
@ -14,8 +13,6 @@ import javafx.scene.input.KeyEvent;
|
|||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
||||
|
||||
|
@ -37,38 +34,29 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
|||
|
||||
@Override
|
||||
public CompStructure<TextField> createBase() {
|
||||
var field = new TextField(value.getValue() != null ? value.getValue().toString() : null);
|
||||
var text = new TextField(value.getValue() != null ? value.getValue().toString() : null);
|
||||
|
||||
value.addListener((ChangeListener<Number>) (observableValue, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
// Check if control value is the same. Then don't set it as that might cause bugs
|
||||
if ((newValue == null && field.getText().isEmpty())
|
||||
|| Objects.equals(field.getText(), newValue != null ? newValue.toString() : null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue == null) {
|
||||
Platform.runLater(() -> {
|
||||
field.setText(null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
text.setText("");
|
||||
} else {
|
||||
if (newValue.intValue() < minValue) {
|
||||
value.setValue(minValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.intValue() < minValue) {
|
||||
value.setValue(minValue);
|
||||
return;
|
||||
}
|
||||
if (newValue.intValue() > maxValue) {
|
||||
value.setValue(maxValue);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue.intValue() > maxValue) {
|
||||
value.setValue(maxValue);
|
||||
return;
|
||||
text.setText(newValue.toString());
|
||||
}
|
||||
|
||||
field.setText(newValue.toString());
|
||||
});
|
||||
});
|
||||
|
||||
field.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
|
||||
text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> {
|
||||
if (minValue < 0) {
|
||||
if (!"-0123456789".contains(keyEvent.getCharacter())) {
|
||||
keyEvent.consume();
|
||||
|
@ -80,7 +68,7 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
|||
}
|
||||
});
|
||||
|
||||
field.textProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
text.textProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
if (newValue == null
|
||||
|| newValue.isEmpty()
|
||||
|| (minValue < 0 && "-".equals(newValue))
|
||||
|
@ -91,12 +79,12 @@ public class IntFieldComp extends Comp<CompStructure<TextField>> {
|
|||
|
||||
int intValue = Integer.parseInt(newValue);
|
||||
if (minValue > intValue || intValue > maxValue) {
|
||||
field.textProperty().setValue(oldValue);
|
||||
text.textProperty().setValue(oldValue);
|
||||
}
|
||||
|
||||
value.setValue(Integer.parseInt(field.textProperty().get()));
|
||||
value.setValue(Integer.parseInt(text.textProperty().get()));
|
||||
});
|
||||
|
||||
return new SimpleCompStructure<>(field);
|
||||
return new SimpleCompStructure<>(text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,6 @@ import io.xpipe.app.comp.CompStructure;
|
|||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.control.TextField;
|
||||
|
@ -45,12 +43,6 @@ public class LazyTextFieldComp extends Comp<CompStructure<TextField>> {
|
|||
});
|
||||
|
||||
r.focusedProperty().addListener((c, o, n) -> {
|
||||
if (n && OsType.getLocal() != OsType.WINDOWS) {
|
||||
Platform.runLater(() -> {
|
||||
r.selectEnd();
|
||||
});
|
||||
}
|
||||
|
||||
if (!n) {
|
||||
appliedValue.setValue(currentValue.getValue());
|
||||
r.setDisable(true);
|
||||
|
|
|
@ -4,17 +4,12 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
|
|||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
|
@ -27,7 +22,6 @@ import javafx.scene.layout.VBox;
|
|||
import lombok.Setter;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
|
@ -43,7 +37,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
private final boolean scrollBar;
|
||||
|
||||
@Setter
|
||||
private boolean visibilityControl = false;
|
||||
private int platformPauseInterval = -1;
|
||||
|
||||
public ListBoxViewComp(
|
||||
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
|
||||
|
@ -62,24 +56,16 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
vbox.setFocusTraversable(false);
|
||||
var scroll = new ScrollPane(vbox);
|
||||
|
||||
refresh(scroll, vbox, shown, all, cache, false);
|
||||
|
||||
var hadScene = new AtomicBoolean(false);
|
||||
scroll.sceneProperty().subscribe(scene -> {
|
||||
if (scene != null) {
|
||||
hadScene.set(true);
|
||||
refresh(scroll, vbox, shown, all, cache, true);
|
||||
}
|
||||
});
|
||||
refresh(scroll, vbox, shown, all, cache, false, false);
|
||||
|
||||
shown.addListener((ListChangeListener<? super T>) (c) -> {
|
||||
Platform.runLater(() -> {
|
||||
if (scroll.getScene() == null && hadScene.get()) {
|
||||
return;
|
||||
}
|
||||
refresh(scroll, vbox, c.getList(), all, cache, true, true);
|
||||
});
|
||||
|
||||
refresh(scroll, vbox, c.getList(), all, cache, true);
|
||||
});
|
||||
all.addListener((ListChangeListener<? super T>) c -> {
|
||||
synchronized (cache) {
|
||||
cache.keySet().retainAll(c.getList());
|
||||
}
|
||||
});
|
||||
|
||||
if (scrollBar) {
|
||||
|
@ -106,84 +92,50 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
scroll.setFitToWidth(true);
|
||||
scroll.getStyleClass().add("list-box-view-comp");
|
||||
|
||||
registerVisibilityListeners(scroll, vbox);
|
||||
|
||||
return new SimpleCompStructure<>(scroll);
|
||||
}
|
||||
|
||||
private void registerVisibilityListeners(ScrollPane scroll, VBox vbox) {
|
||||
if (!visibilityControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dirty = new SimpleBooleanProperty();
|
||||
var animationTimer = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
if (!dirty.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateVisibilities(scroll, vbox);
|
||||
dirty.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
scroll.vvalueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
dirty.set(true);
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
scroll.heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||
dirty.set(true);
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
vbox.heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||
dirty.set(true);
|
||||
Platform.runLater(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
});
|
||||
|
||||
// We can't directly listen to any parent element changing visibility, so this is a compromise
|
||||
if (AppLayoutModel.get() != null) {
|
||||
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
|
||||
dirty.set(true);
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
});
|
||||
}
|
||||
BrowserFullSessionModel.DEFAULT.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
if (StoreViewState.get() != null) {
|
||||
StoreViewState.get().getSortMode().addListener((observable, oldValue, newValue) -> {
|
||||
// This is very ugly, but it just takes multiple iterations for the order to apply
|
||||
Platform.runLater(() -> {
|
||||
Platform.runLater(() -> {
|
||||
Platform.runLater(() -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
vbox.sceneProperty().addListener((observable, oldValue, newValue) -> {
|
||||
dirty.set(true);
|
||||
|
||||
if (newValue != null) {
|
||||
animationTimer.start();
|
||||
} else {
|
||||
animationTimer.stop();
|
||||
}
|
||||
|
||||
Node c = vbox;
|
||||
do {
|
||||
c.boundsInParentProperty().addListener((change, oldBounds,newBounds) -> {
|
||||
dirty.set(true);
|
||||
while ((c = c.getParent()) != null) {
|
||||
c.boundsInParentProperty().addListener((observable1, oldValue1, newValue1) -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
// Don't listen to root node changes, that seemingly can cause exceptions
|
||||
} while ((c = c.getParent()) != null && c.getParent() != null);
|
||||
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
if (newValue != null) {
|
||||
newValue.heightProperty().addListener((observable1, oldValue1, newValue1) -> {
|
||||
dirty.set(true);
|
||||
updateVisibilities(scroll, vbox);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new SimpleCompStructure<>(scroll);
|
||||
}
|
||||
|
||||
private boolean isVisible(ScrollPane pane, VBox box, Node node) {
|
||||
|
@ -226,20 +178,9 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
}
|
||||
|
||||
private void updateVisibilities(ScrollPane scroll, VBox vbox) {
|
||||
if (!visibilityControl) {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for (Node child : vbox.getChildren()) {
|
||||
var v = isVisible(scroll, vbox, child);
|
||||
child.setVisible(v);
|
||||
if (v) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 10) {
|
||||
// System.out.println("Visible: " + count);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,42 +190,44 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
List<? extends T> shown,
|
||||
List<? extends T> all,
|
||||
Map<T, Region> cache,
|
||||
boolean asynchronous,
|
||||
boolean refreshVisibilities) {
|
||||
Runnable update = () -> {
|
||||
synchronized (cache) {
|
||||
var set = new HashSet<T>();
|
||||
// These lists might diverge on updates, so add both
|
||||
synchronized (shown) {
|
||||
set.addAll(shown);
|
||||
}
|
||||
synchronized (all) {
|
||||
set.addAll(all);
|
||||
}
|
||||
// These lists might diverge on updates
|
||||
set.addAll(shown);
|
||||
set.addAll(all);
|
||||
// Clear cache of unused values
|
||||
cache.keySet().removeIf(t -> !set.contains(t));
|
||||
}
|
||||
|
||||
// Use copy to prevent concurrent modifications and to not synchronize to long
|
||||
List<T> shownCopy;
|
||||
synchronized (shown) {
|
||||
shownCopy = new ArrayList<>(shown);
|
||||
}
|
||||
List<Region> newShown = shownCopy.stream().map(v -> {
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
if (comp != null) {
|
||||
var r = comp.createRegion();
|
||||
if (visibilityControl) {
|
||||
r.setVisible(false);
|
||||
final long[] lastPause = {System.currentTimeMillis()};
|
||||
// Create copy to reduce chances of concurrent modification
|
||||
var shownCopy = new ArrayList<>(shown);
|
||||
var newShown = shownCopy.stream()
|
||||
.map(v -> {
|
||||
var elapsed = System.currentTimeMillis() - lastPause[0];
|
||||
if (platformPauseInterval != -1 && elapsed > platformPauseInterval) {
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
lastPause[0] = System.currentTimeMillis();
|
||||
}
|
||||
cache.put(v, r);
|
||||
} else {
|
||||
cache.put(v, null);
|
||||
}
|
||||
}
|
||||
|
||||
return cache.get(v);
|
||||
}).filter(region -> region != null).toList();
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
if (comp != null) {
|
||||
var r = comp.createRegion();
|
||||
r.setVisible(false);
|
||||
cache.put(v, r);
|
||||
} else {
|
||||
cache.put(v, null);
|
||||
}
|
||||
}
|
||||
|
||||
return cache.get(v);
|
||||
})
|
||||
.filter(region -> region != null)
|
||||
.toList();
|
||||
|
||||
if (listView.getChildren().equals(newShown)) {
|
||||
return;
|
||||
|
@ -304,6 +247,11 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
updateVisibilities(scroll, listView);
|
||||
}
|
||||
};
|
||||
update.run();
|
||||
|
||||
if (asynchronous) {
|
||||
Platform.runLater(update);
|
||||
} else {
|
||||
PlatformThread.runLaterIfNeeded(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package io.xpipe.app.comp.base;
|
|||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
@ -16,11 +15,9 @@ import java.util.Map;
|
|||
|
||||
public class MultiContentComp extends SimpleComp {
|
||||
|
||||
private final boolean log;
|
||||
private final Map<Comp<?>, ObservableValue<Boolean>> content;
|
||||
|
||||
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content, boolean log) {
|
||||
this.log = log;
|
||||
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
|
||||
this.content = FXCollections.observableMap(content);
|
||||
}
|
||||
|
||||
|
@ -37,14 +34,7 @@ public class MultiContentComp extends SimpleComp {
|
|||
});
|
||||
|
||||
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> e : content.entrySet()) {
|
||||
var name = e.getKey().getClass().getSimpleName();
|
||||
if (log) {
|
||||
TrackEvent.trace("Creating content tab region for element " + name);
|
||||
}
|
||||
var r = e.getKey().createRegion();
|
||||
if (log) {
|
||||
TrackEvent.trace("Created content tab region for element " + name);
|
||||
}
|
||||
e.getValue().subscribe(val -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
r.setManaged(val);
|
||||
|
@ -52,9 +42,6 @@ public class MultiContentComp extends SimpleComp {
|
|||
});
|
||||
});
|
||||
m.put(e.getKey(), r);
|
||||
if (log) {
|
||||
TrackEvent.trace("Added content tab region for element " + name);
|
||||
}
|
||||
}
|
||||
|
||||
return stack;
|
||||
|
|
|
@ -10,8 +10,6 @@ import javafx.beans.property.Property;
|
|||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
|
@ -70,32 +68,23 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
|
||||
@Override
|
||||
public Structure createBase() {
|
||||
var field = new PasswordField();
|
||||
field.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
|
||||
if (e.isControlDown() && e.getCode() == KeyCode.BACK_SPACE) {
|
||||
var sel = field.getSelection();
|
||||
if (sel.getEnd() > 0) {
|
||||
field.setText(field.getText().substring(sel.getEnd()));
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
field.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
|
||||
field.textProperty().addListener((c, o, n) -> {
|
||||
var text = new PasswordField();
|
||||
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
|
||||
text.textProperty().addListener((c, o, n) -> {
|
||||
value.setValue(n != null && n.length() > 0 ? encrypt(n.toCharArray()) : null);
|
||||
});
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
// Check if control value is the same. Then don't set it as that might cause bugs
|
||||
if ((n == null && field.getText().isEmpty())
|
||||
|| Objects.equals(field.getText(), n != null ? n.getSecretValue() : null)) {
|
||||
if ((n == null && text.getText().isEmpty())
|
||||
|| Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.setText(n != null ? n.getSecretValue() : null);
|
||||
text.setText(n != null ? n.getSecretValue() : null);
|
||||
});
|
||||
});
|
||||
HBox.setHgrow(field, Priority.ALWAYS);
|
||||
HBox.setHgrow(text, Priority.ALWAYS);
|
||||
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyPassword(value.getValue());
|
||||
|
@ -104,7 +93,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
.tooltipKey("copyPassword")
|
||||
.createRegion();
|
||||
|
||||
var ig = new InputGroup(field);
|
||||
var ig = new InputGroup(text);
|
||||
ig.setFillHeight(true);
|
||||
ig.getStyleClass().add("secret-field-comp");
|
||||
if (allowCopy) {
|
||||
|
@ -114,10 +103,10 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
|
||||
ig.focusedProperty().addListener((c, o, n) -> {
|
||||
if (n) {
|
||||
field.requestFocus();
|
||||
text.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
return new Structure(ig, field);
|
||||
return new Structure(ig, text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,9 +76,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
var shortcut = e.combination();
|
||||
b.apply(new TooltipAugment<>(e.name(), shortcut));
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
|
||||
AppFontSizes.xl(struc.get());
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
@ -125,7 +123,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
AppFontSizes.xl(struc.get());
|
||||
});
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
|
|
|
@ -24,6 +24,7 @@ public class StackComp extends Comp<CompStructure<StackPane>> {
|
|||
pane.getChildren().add(c.createRegion());
|
||||
}
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
pane.setPickOnBounds(false);
|
||||
return new SimpleCompStructure<>(pane);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import io.xpipe.app.comp.Comp;
|
|||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
|
@ -58,11 +57,6 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return OsType.getLocal() == OsType.WINDOWS ? 38 : 37;
|
||||
}
|
||||
|
||||
protected Region createContent() {
|
||||
var grid = new GridPane();
|
||||
grid.setHgap(8);
|
||||
|
|
|
@ -82,8 +82,7 @@ public class OsLogoComp extends SimpleComp {
|
|||
}
|
||||
|
||||
return ICONS.entrySet().stream()
|
||||
.filter(e -> name.toLowerCase().contains(e.getKey()) ||
|
||||
name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
|
||||
.filter(e -> name.toLowerCase().contains(e.getKey()))
|
||||
.findAny()
|
||||
.map(e -> e.getValue())
|
||||
.orElse("os/linux.svg");
|
||||
|
|
|
@ -21,11 +21,6 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return 57;
|
||||
}
|
||||
|
||||
private Label createSummary() {
|
||||
var summary = new Label();
|
||||
summary.textProperty().bind(getWrapper().getShownSummary());
|
||||
|
|
|
@ -14,7 +14,6 @@ import io.xpipe.app.util.ClipboardHelper;
|
|||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
@ -80,11 +79,7 @@ public class StoreCategoryComp extends SimpleComp {
|
|||
.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
struc.get().setFocusTraversable(false);
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
HBox.setMargin(struc.get(), new Insets(0, 0, 2.3, 0));
|
||||
} else if (OsType.getLocal() == OsType.MACOS) {
|
||||
HBox.setMargin(struc.get(), new Insets(0, 0, 1.8, 0));
|
||||
}
|
||||
HBox.setMargin(struc.get(), new Insets(0, 0, 2.6, 0));
|
||||
})
|
||||
.disable(Bindings.isEmpty(category.getChildren().getList()))
|
||||
.styleClass("expand-button")
|
||||
|
@ -170,7 +165,6 @@ public class StoreCategoryComp extends SimpleComp {
|
|||
new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper), false);
|
||||
children.styleClass("children");
|
||||
children.minHeight(0);
|
||||
children.setVisibilityControl(true);
|
||||
|
||||
var hide = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
|
|
|
@ -24,7 +24,6 @@ import javafx.beans.property.SimpleStringProperty;
|
|||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
@ -200,7 +199,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
selected),
|
||||
() -> {});
|
||||
button.apply(struc -> {
|
||||
struc.get().setMaxWidth(20000);
|
||||
struc.get().setMaxWidth(2000);
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
Comp<?> graphic = PrettyImageHelper.ofFixedSize(
|
||||
Bindings.createStringBinding(
|
||||
|
@ -225,14 +224,6 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
}
|
||||
event.consume();
|
||||
});
|
||||
struc.get().setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.SECONDARY) {
|
||||
return;
|
||||
}
|
||||
|
||||
selected.setValue(mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
.styleClass("choice-comp");
|
||||
|
||||
|
@ -250,7 +241,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
StackPane.setMargin(icon, new Insets(10));
|
||||
pane.setPickOnBounds(false);
|
||||
StackPane.setAlignment(icon, Pos.CENTER_RIGHT);
|
||||
pane.setMaxWidth(20000);
|
||||
pane.setMaxWidth(2000);
|
||||
r.prefWidthProperty().bind(pane.widthProperty());
|
||||
r.maxWidthProperty().bind(pane.widthProperty());
|
||||
return pane;
|
||||
|
|
|
@ -183,10 +183,6 @@ public class StoreCreationComp extends DialogComp {
|
|||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e) {
|
||||
showEdit(e, dataStoreEntry -> {});
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
|
||||
show(
|
||||
e.getName(),
|
||||
e.getProvider(),
|
||||
|
@ -201,14 +197,9 @@ public class StoreCreationComp extends DialogComp {
|
|||
if (e.getStore().equals(newE.getStore())) {
|
||||
e.setName(newE.getName());
|
||||
} else {
|
||||
var madeValid = !e.getValidity().isUsable() && newE.getValidity().isUsable();
|
||||
DataStorage.get().updateEntry(e, newE);
|
||||
if (madeValid) {
|
||||
StoreViewState.get().toggleStoreListUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
consumer.accept(e);
|
||||
});
|
||||
},
|
||||
true,
|
||||
|
|
|
@ -68,10 +68,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
}
|
||||
}
|
||||
|
||||
public static StoreEntryComp customSection(StoreSection e) {
|
||||
public static StoreEntryComp customSection(StoreSection e, boolean topLevel) {
|
||||
var prov = e.getWrapper().getEntry().getProvider();
|
||||
if (prov != null) {
|
||||
return prov.customEntryComp(e, e.getDepth() == 1);
|
||||
return prov.customEntryComp(e, topLevel);
|
||||
} else {
|
||||
var forceCondensed = AppPrefs.get() != null
|
||||
&& AppPrefs.get().condenseConnectionDisplay().get();
|
||||
|
@ -81,8 +81,6 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
public abstract boolean isFullSize();
|
||||
|
||||
public abstract int getHeight();
|
||||
|
||||
@Override
|
||||
protected final Region createSimple() {
|
||||
var r = createContent();
|
||||
|
@ -359,7 +357,9 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
getWrapper().moveTo(storeCategoryWrapper.getCategory());
|
||||
event.consume();
|
||||
});
|
||||
if (storeCategoryWrapper.getParent() == null) {
|
||||
if (storeCategoryWrapper.getParent() == null
|
||||
|| storeCategoryWrapper.equals(
|
||||
getWrapper().getCategory().getValue())) {
|
||||
m.setDisable(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,11 +27,11 @@ public class StoreEntryListComp extends SimpleComp {
|
|||
.getAllChildren()
|
||||
.getList(),
|
||||
(StoreSection e) -> {
|
||||
var custom = StoreSection.customSection(e).hgrow();
|
||||
var custom = StoreSection.customSection(e, true).hgrow();
|
||||
return custom;
|
||||
},
|
||||
true);
|
||||
content.setVisibilityControl(true);
|
||||
content.setPlatformPauseInterval(50);
|
||||
content.apply(struc -> {
|
||||
// Reset scroll
|
||||
StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {
|
||||
|
@ -142,6 +142,6 @@ public class StoreEntryListComp extends SimpleComp {
|
|||
map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);
|
||||
map.put(new StoreIdentitiesIntroComp(), showIdentitiesIntro);
|
||||
|
||||
return new MultiContentComp(map, false).createRegion();
|
||||
return new MultiContentComp(map).createRegion();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,23 @@ import java.util.function.Function;
|
|||
|
||||
public class StoreEntryListOverviewComp extends SimpleComp {
|
||||
|
||||
private final Property<StoreSortMode> sortMode;
|
||||
|
||||
public StoreEntryListOverviewComp() {
|
||||
this.sortMode = new SimpleObjectProperty<>();
|
||||
StoreViewState.get().getActiveCategory().subscribe(val -> {
|
||||
sortMode.setValue(val.getSortMode().getValue());
|
||||
});
|
||||
sortMode.addListener((observable, oldValue, newValue) -> {
|
||||
var cat = StoreViewState.get().getActiveCategory().getValue();
|
||||
if (cat == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cat.getSortMode().setValue(newValue);
|
||||
});
|
||||
}
|
||||
|
||||
private Region createGroupListHeader() {
|
||||
var label = new Label();
|
||||
var name = BindingsHelper.flatMap(
|
||||
|
@ -125,7 +142,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
}
|
||||
|
||||
private Comp<?> createAlphabeticalSortButton() {
|
||||
var sortMode = StoreViewState.get().getSortMode();
|
||||
var icon = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) {
|
||||
|
@ -166,7 +182,6 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
}
|
||||
|
||||
private Comp<?> createDateSortButton() {
|
||||
var sortMode = StoreViewState.get().getSortMode();
|
||||
var icon = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (sortMode.getValue() == StoreSortMode.DATE_ASC) {
|
||||
|
|
|
@ -32,6 +32,7 @@ public class StoreEntryWrapper {
|
|||
private final Property<String> name;
|
||||
private final DataStoreEntry entry;
|
||||
private final Property<Instant> lastAccess;
|
||||
private final Property<Instant> lastAccessApplied = new SimpleObjectProperty<>();
|
||||
private final BooleanProperty disabled = new SimpleBooleanProperty();
|
||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
|
||||
|
@ -103,6 +104,10 @@ public class StoreEntryWrapper {
|
|||
setupListeners();
|
||||
}
|
||||
|
||||
public void applyLastAccess() {
|
||||
this.lastAccessApplied.setValue(lastAccess.getValue());
|
||||
}
|
||||
|
||||
public void moveTo(DataStoreCategory category) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
DataStorage.get().moveEntryToCategory(entry, category);
|
||||
|
@ -125,7 +130,8 @@ public class StoreEntryWrapper {
|
|||
|
||||
public void delete() {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
DataStorage.get().deleteWithChildren(this.entry);
|
||||
DataStorage.get().deleteChildren(this.entry);
|
||||
DataStorage.get().deleteStoreEntry(this.entry);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,10 @@ import io.xpipe.app.core.AppI18n;
|
|||
import io.xpipe.app.icon.SystemIcon;
|
||||
import io.xpipe.app.icon.SystemIconCache;
|
||||
import io.xpipe.app.icon.SystemIconManager;
|
||||
import io.xpipe.app.resources.AppImages;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
|
@ -108,29 +106,19 @@ public class StoreIconChoiceComp extends SimpleComp {
|
|||
}
|
||||
|
||||
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
|
||||
var available = icons.stream()
|
||||
.filter(systemIcon -> AppImages.hasNormalImage("icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
|
||||
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
|
||||
.toList();
|
||||
table.getPlaceholder().setVisible(available.size() == 0);
|
||||
var filtered = available;
|
||||
if (filterString != null && !filterString.isBlank() && filterString.length() >= 2) {
|
||||
filtered = available.stream().filter(icon -> containsString(icon.getId(), filterString)).toList();
|
||||
}
|
||||
var data = partitionList(filtered, columns);
|
||||
table.getItems().setAll(data);
|
||||
var displayedIcons = filterString == null || filterString.isBlank() || filterString.length() < 2
|
||||
? icons.stream()
|
||||
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
|
||||
.toList()
|
||||
: icons.stream()
|
||||
.filter(icon -> containsString(icon.getId(), filterString))
|
||||
.toList();
|
||||
|
||||
var selectMatch = filtered.size() == 1 || filtered.stream().anyMatch(systemIcon -> systemIcon.getId().equals(filterString));
|
||||
// Table updates seem to not always be instant, sometimes the column is not there yet
|
||||
if (selectMatch && table.getColumns().size() > 0) {
|
||||
table.getSelectionModel().select(0, table.getColumns().getFirst());
|
||||
selected.setValue(filtered.getFirst());
|
||||
} else {
|
||||
selected.setValue(null);
|
||||
}
|
||||
var data = partitionList(displayedIcons, columns);
|
||||
table.getItems().setAll(data);
|
||||
}
|
||||
|
||||
private <T> List<List<T>> partitionList(List<T> list, int size) {
|
||||
private <T> Collection<List<T>> partitionList(List<T> list, int size) {
|
||||
List<List<T>> partitions = new ArrayList<>();
|
||||
if (list.size() == 0) {
|
||||
return partitions;
|
||||
|
|
|
@ -86,7 +86,7 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
|
|||
var syncButton = new Button(null, new FontIcon("mdi2p-play-circle"));
|
||||
syncButton.textProperty().bind(AppI18n.observable("setupSync"));
|
||||
syncButton.setOnAction(event -> {
|
||||
AppPrefs.get().selectCategory("vaultSync");
|
||||
AppPrefs.get().selectCategory("sync");
|
||||
event.consume();
|
||||
});
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ public class StoreIntroComp extends SimpleComp {
|
|||
|
||||
var importButton = new Button(null, new FontIcon("mdi2g-git"));
|
||||
importButton.textProperty().bind(AppI18n.observable("importConnections"));
|
||||
importButton.setOnAction(event -> AppPrefs.get().selectCategory("vaultSync"));
|
||||
importButton.setOnAction(event -> AppPrefs.get().selectCategory("sync"));
|
||||
var importPane = new StackPane(importButton);
|
||||
importPane.setAlignment(Pos.CENTER);
|
||||
|
||||
|
|
|
@ -53,12 +53,12 @@ public class StoreSection {
|
|||
}
|
||||
}
|
||||
|
||||
public static Comp<?> customSection(StoreSection e) {
|
||||
public static Comp<?> customSection(StoreSection e, boolean topLevel) {
|
||||
var prov = e.getWrapper().getEntry().getProvider();
|
||||
if (prov != null) {
|
||||
return prov.customSectionComp(e);
|
||||
return prov.customSectionComp(e, topLevel);
|
||||
} else {
|
||||
return new StoreSectionComp(e);
|
||||
return new StoreSectionComp(e, topLevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ public class StoreSection {
|
|||
|
||||
var current = mappedSortMode.getValue();
|
||||
if (current != null) {
|
||||
return current.comparator().compare(o1, o2);
|
||||
return current.comparator().compare(current.representative(o1), current.representative(o2));
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
@ -133,8 +133,7 @@ public class StoreSection {
|
|||
(showInCategory(category.getValue(), section.getWrapper()));
|
||||
},
|
||||
category,
|
||||
filterString,
|
||||
updateObservable);
|
||||
filterString);
|
||||
return new StoreSection(null, ordered, shown, 0);
|
||||
}
|
||||
|
||||
|
@ -210,8 +209,7 @@ public class StoreSection {
|
|||
category,
|
||||
filterString,
|
||||
e.getPersistentState(),
|
||||
e.getCache(),
|
||||
updateObservable);
|
||||
e.getCache());
|
||||
return new StoreSection(e, cached, filtered, depth);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.comp.base.IconButtonComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
private static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
|
||||
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
|
||||
private static final PseudoClass TOP = PseudoClass.getPseudoClass("top");
|
||||
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
|
||||
|
||||
protected final StoreSection section;
|
||||
|
||||
public StoreSectionBaseComp(StoreSection section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
protected ObservableBooleanValue effectiveExpanded(ObservableBooleanValue expanded) {
|
||||
return section.getWrapper() != null ? Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return expanded.get()
|
||||
&& section.getShownChildren().getList().size() > 0;
|
||||
},
|
||||
expanded,
|
||||
section.getShownChildren().getList()) : new SimpleBooleanProperty(true);
|
||||
}
|
||||
|
||||
protected void addPseudoClassListeners(VBox vbox, ObservableBooleanValue expanded) {
|
||||
var observable = effectiveExpanded(expanded);
|
||||
BindingsHelper.preserve(this, observable);
|
||||
observable.subscribe(val -> {
|
||||
vbox.pseudoClassStateChanged(EXPANDED, val);
|
||||
});
|
||||
|
||||
vbox.pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||
vbox.pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
|
||||
vbox.pseudoClassStateChanged(ROOT, section.getDepth() == 0);
|
||||
vbox.pseudoClassStateChanged(SUB, section.getDepth() > 1);
|
||||
vbox.pseudoClassStateChanged(TOP, section.getDepth() == 1);
|
||||
|
||||
if (section.getWrapper() != null) {
|
||||
if (section.getDepth() == 1) {
|
||||
section.getWrapper().getColor().subscribe(val -> {
|
||||
var newList = new ArrayList<>(vbox.getStyleClass());
|
||||
newList.removeIf(s -> Arrays.stream(DataColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));
|
||||
newList.remove("gray");
|
||||
newList.add("color-box");
|
||||
if (val != null) {
|
||||
newList.add(val.getId());
|
||||
} else {
|
||||
newList.add("gray");
|
||||
}
|
||||
vbox.getStyleClass().setAll(newList);
|
||||
});
|
||||
}
|
||||
|
||||
section.getWrapper().getPerUser().subscribe(val -> {
|
||||
vbox.pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void addVisibilityListeners(VBox root, HBox hbox) {
|
||||
var children = new ArrayList<>(hbox.getChildren());
|
||||
hbox.getChildren().clear();
|
||||
root.visibleProperty().subscribe((newValue) -> {
|
||||
if (newValue) {
|
||||
hbox.getChildren().addAll(children);
|
||||
} else {
|
||||
hbox.getChildren().removeAll(children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected ListBoxViewComp<StoreSection> createChildrenList(Function<StoreSection, Comp<?>> function, ObservableBooleanValue hide) {
|
||||
var content = new ListBoxViewComp<>(
|
||||
section.getShownChildren().getList(),
|
||||
section.getAllChildren().getList(),
|
||||
(StoreSection e) -> {
|
||||
return function.apply(e).grow(true, false);
|
||||
},
|
||||
section.getWrapper() == null);
|
||||
content.setVisibilityControl(true);
|
||||
content.minHeight(0);
|
||||
content.hgrow();
|
||||
content.styleClass("children-content");
|
||||
content.hide(hide);
|
||||
return content;
|
||||
}
|
||||
|
||||
protected Comp<CompStructure<Button>> createExpandButton(Runnable action, int width, ObservableBooleanValue expanded) {
|
||||
var icon = Bindings.createObjectBinding(() -> new LabelGraphic.IconGraphic(
|
||||
expanded.get() && section.getShownChildren().getList().size() > 0 ?
|
||||
"mdal-keyboard_arrow_down" :
|
||||
"mdal-keyboard_arrow_right"), expanded, section.getShownChildren().getList());
|
||||
var expandButton = new IconButtonComp(icon,
|
||||
action);
|
||||
expandButton
|
||||
.minWidth(width)
|
||||
.prefWidth(width)
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Expand " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
|
||||
.styleClass("expand-button")
|
||||
.maxHeight(100);
|
||||
return expandButton;
|
||||
}
|
||||
|
||||
protected Comp<CompStructure<Button>> createQuickAccessButton(int width, Consumer<StoreSection> r) {
|
||||
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, r)
|
||||
.styleClass("quick-access-button")
|
||||
.minWidth(width)
|
||||
.prefWidth(width)
|
||||
.maxHeight(100)
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Access " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(quickAccessDisabled);
|
||||
return quickAccessButton;
|
||||
}
|
||||
}
|
|
@ -12,14 +12,11 @@ import io.xpipe.app.util.LabelGraphic;
|
|||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -27,24 +24,103 @@ import java.util.Arrays;
|
|||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StoreSectionComp extends StoreSectionBaseComp {
|
||||
public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
public StoreSectionComp(StoreSection section) {
|
||||
super(section);
|
||||
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
|
||||
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
|
||||
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
|
||||
private final StoreSection section;
|
||||
private final boolean topLevel;
|
||||
|
||||
public StoreSectionComp(StoreSection section, boolean topLevel) {
|
||||
this.section = section;
|
||||
this.topLevel = topLevel;
|
||||
}
|
||||
|
||||
private Comp<CompStructure<Button>> createQuickAccessButton() {
|
||||
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
Consumer<StoreSection> quickAccessAction = w -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
w.getWrapper().executeDefaultAction();
|
||||
});
|
||||
};
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
|
||||
.vgrow()
|
||||
.styleClass("quick-access-button")
|
||||
.apply(struc -> struc.get().setMinWidth(30))
|
||||
.apply(struc -> struc.get().setPrefWidth(30))
|
||||
.maxHeight(100)
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Access " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(quickAccessDisabled)
|
||||
.focusTraversableForAccessibility()
|
||||
.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
|
||||
return quickAccessButton;
|
||||
}
|
||||
|
||||
private Comp<CompStructure<Button>> createExpandButton() {
|
||||
var expandButton = new IconButtonComp(
|
||||
Bindings.createObjectBinding(
|
||||
() -> new LabelGraphic.IconGraphic(
|
||||
section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren()
|
||||
.getList()
|
||||
.size()
|
||||
> 0
|
||||
? "mdal-keyboard_arrow_down"
|
||||
: "mdal-keyboard_arrow_right"),
|
||||
section.getWrapper().getExpanded(),
|
||||
section.getShownChildren().getList()),
|
||||
() -> {
|
||||
section.getWrapper().toggleExpanded();
|
||||
});
|
||||
expandButton
|
||||
.apply(struc -> struc.get().setMinWidth(30))
|
||||
.apply(struc -> struc.get().setPrefWidth(30))
|
||||
.focusTraversableForAccessibility()
|
||||
.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE))
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Expand " + section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
|
||||
.styleClass("expand-button")
|
||||
.maxHeight(100)
|
||||
.vgrow();
|
||||
return expandButton;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
var entryButton = StoreEntryComp.customSection(section);
|
||||
entryButton.hgrow();
|
||||
entryButton.apply(struc -> {
|
||||
struc.get().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
var entryButton = StoreEntryComp.customSection(section, topLevel);
|
||||
var quickAccessButton = createQuickAccessButton();
|
||||
var expandButton = createExpandButton();
|
||||
var buttonList = new ArrayList<Comp<?>>();
|
||||
if (entryButton.isFullSize()) {
|
||||
buttonList.add(quickAccessButton);
|
||||
}
|
||||
buttonList.add(expandButton);
|
||||
var buttons = new VerticalComp(buttonList);
|
||||
var topEntryList = new HorizontalComp(List.of(buttons, entryButton.hgrow()));
|
||||
topEntryList.apply(struc -> {
|
||||
var mainButton = struc.get().getChildren().get(1);
|
||||
mainButton.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode() == KeyCode.SPACE) {
|
||||
section.getWrapper().toggleExpanded();
|
||||
event.consume();
|
||||
}
|
||||
if (event.getCode() == KeyCode.RIGHT) {
|
||||
var ref = (VBox) ((HBox) struc.get().getParent()).getChildren().getFirst();
|
||||
var ref = (VBox) struc.get().getChildren().getFirst();
|
||||
if (entryButton.isFullSize()) {
|
||||
var btn = (Button) ref.getChildren().getFirst();
|
||||
btn.fire();
|
||||
|
@ -54,45 +130,72 @@ public class StoreSectionComp extends StoreSectionBaseComp {
|
|||
});
|
||||
});
|
||||
|
||||
var quickAccessButton = createQuickAccessButton(30, c -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
c.getWrapper().executeDefaultAction();
|
||||
});
|
||||
});
|
||||
quickAccessButton.vgrow();
|
||||
quickAccessButton.focusTraversableForAccessibility();
|
||||
quickAccessButton.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
|
||||
|
||||
var expandButton = createExpandButton(() -> section.getWrapper().toggleExpanded(), 30, section.getWrapper().getExpanded());
|
||||
expandButton.vgrow();
|
||||
expandButton.focusTraversableForAccessibility();
|
||||
expandButton.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE));
|
||||
var buttonList = new ArrayList<Comp<?>>();
|
||||
if (entryButton.isFullSize()) {
|
||||
buttonList.add(quickAccessButton);
|
||||
}
|
||||
buttonList.add(expandButton);
|
||||
var buttons = new VerticalComp(buttonList);
|
||||
var topEntryList = new HorizontalComp(List.of(buttons, entryButton));
|
||||
topEntryList.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
|
||||
topEntryList.minHeight(entryButton.getHeight());
|
||||
topEntryList.maxHeight(entryButton.getHeight());
|
||||
topEntryList.prefHeight(entryButton.getHeight());
|
||||
|
||||
var effectiveExpanded = effectiveExpanded(section.getWrapper().getExpanded());
|
||||
var content = createChildrenList(c -> StoreSection.customSection(c), Bindings.not(effectiveExpanded));
|
||||
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
||||
// section is actually expanded
|
||||
var listSections = section.getShownChildren()
|
||||
.filtered(
|
||||
storeSection -> section.getAllChildren().getList().size() <= 20
|
||||
|| section.getWrapper().getExpanded().get(),
|
||||
section.getWrapper().getExpanded(),
|
||||
section.getAllChildren().getList());
|
||||
var content = new ListBoxViewComp<>(
|
||||
listSections.getList(),
|
||||
section.getAllChildren().getList(),
|
||||
(StoreSection e) -> {
|
||||
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
|
||||
},
|
||||
false);
|
||||
content.minHeight(0).hgrow();
|
||||
|
||||
var expanded = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren().getList().size() > 0;
|
||||
},
|
||||
section.getWrapper().getExpanded(),
|
||||
section.getShownChildren().getList());
|
||||
var full = new VerticalComp(List.of(
|
||||
topEntryList,
|
||||
Comp.separator().hide(Bindings.not(effectiveExpanded)),
|
||||
content));
|
||||
full.styleClass("store-entry-section-comp");
|
||||
full.apply(struc -> {
|
||||
Comp.separator().hide(expanded.not()),
|
||||
content.styleClass("children-content")
|
||||
.hide(Bindings.or(
|
||||
Bindings.not(section.getWrapper().getExpanded()),
|
||||
Bindings.size(section.getShownChildren().getList())
|
||||
.isEqualTo(0)))));
|
||||
return full.styleClass("store-entry-section-comp")
|
||||
.apply(struc -> {
|
||||
struc.get().setFillWidth(true);
|
||||
var hbox = ((HBox) struc.get().getChildren().getFirst());
|
||||
addPseudoClassListeners(struc.get(), section.getWrapper().getExpanded());
|
||||
addVisibilityListeners(struc.get(), hbox);
|
||||
});
|
||||
return full.createStructure();
|
||||
expanded.subscribe(val -> {
|
||||
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
||||
});
|
||||
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
|
||||
struc.get().pseudoClassStateChanged(ROOT, topLevel);
|
||||
struc.get().pseudoClassStateChanged(SUB, !topLevel);
|
||||
|
||||
section.getWrapper().getColor().subscribe(val -> {
|
||||
if (!topLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newList = new ArrayList<>(struc.get().getStyleClass());
|
||||
newList.removeIf(s -> Arrays.stream(DataColor.values())
|
||||
.anyMatch(
|
||||
dataStoreColor -> dataStoreColor.getId().equals(s)));
|
||||
newList.remove("gray");
|
||||
newList.add("color-box");
|
||||
if (val != null) {
|
||||
newList.add(val.getId());
|
||||
} else {
|
||||
newList.add("gray");
|
||||
}
|
||||
struc.get().getStyleClass().setAll(newList);
|
||||
});
|
||||
|
||||
section.getWrapper().getPerUser().subscribe(val -> {
|
||||
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val);
|
||||
});
|
||||
})
|
||||
.createStructure();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,18 +12,23 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StoreSectionMiniComp extends StoreSectionBaseComp {
|
||||
public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
private final BooleanProperty expanded;
|
||||
public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded");
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth");
|
||||
private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root");
|
||||
private static final PseudoClass TOP = PseudoClass.getPseudoClass("top");
|
||||
private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub");
|
||||
|
||||
private final StoreSection section;
|
||||
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment;
|
||||
private final Consumer<StoreSection> action;
|
||||
|
||||
|
@ -31,61 +36,142 @@ public class StoreSectionMiniComp extends StoreSectionBaseComp {
|
|||
StoreSection section,
|
||||
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment,
|
||||
Consumer<StoreSection> action) {
|
||||
super(section);
|
||||
this.section = section;
|
||||
this.augment = augment;
|
||||
this.action = action;
|
||||
this.expanded = new SimpleBooleanProperty(section.getWrapper() == null || section.getWrapper().getExpanded().getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
var list = new ArrayList<Comp<?>>();
|
||||
BooleanProperty expanded;
|
||||
if (section.getWrapper() != null) {
|
||||
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {
|
||||
action.accept(section);
|
||||
});
|
||||
root.hgrow();
|
||||
root.maxWidth(2000);
|
||||
root.styleClass("item");
|
||||
root.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
struc.get().setGraphic(PrettyImageHelper.ofFixedSize(
|
||||
var root = new ButtonComp(section.getWrapper().getShownName(), () -> {})
|
||||
.apply(struc -> {
|
||||
struc.get()
|
||||
.setGraphic(PrettyImageHelper.ofFixedSize(
|
||||
section.getWrapper().getIconFile(), 16, 16)
|
||||
.createRegion());
|
||||
struc.get().setMnemonicParsing(false);
|
||||
});
|
||||
})
|
||||
.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
})
|
||||
.apply(struc -> {
|
||||
struc.get().setOnAction(event -> {
|
||||
action.accept(section);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
.grow(true, false)
|
||||
.apply(struc -> struc.get().setMnemonicParsing(false))
|
||||
.styleClass("item");
|
||||
augment.accept(section, root);
|
||||
|
||||
var expandButton = createExpandButton(() -> expanded.set(!expanded.get()), 20, expanded);
|
||||
expandButton.focusTraversable();
|
||||
expanded =
|
||||
new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren().getList().size() > 0);
|
||||
var button = new IconButtonComp(
|
||||
Bindings.createObjectBinding(
|
||||
() -> new LabelGraphic.IconGraphic(
|
||||
expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"),
|
||||
expanded),
|
||||
() -> {
|
||||
expanded.set(!expanded.get());
|
||||
})
|
||||
.apply(struc -> struc.get().setMinWidth(20))
|
||||
.apply(struc -> struc.get().setPrefWidth(20))
|
||||
.focusTraversable()
|
||||
.accessibleText(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return "Expand "
|
||||
+ section.getWrapper().getName().getValue();
|
||||
},
|
||||
section.getWrapper().getName()))
|
||||
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0))
|
||||
.grow(false, true)
|
||||
.styleClass("expand-button");
|
||||
|
||||
var quickAccessButton = createQuickAccessButton(20, action);
|
||||
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
Consumer<StoreSection> quickAccessAction = action;
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
|
||||
.vgrow()
|
||||
.styleClass("quick-access-button")
|
||||
.maxHeight(100)
|
||||
.disable(quickAccessDisabled);
|
||||
|
||||
var buttonList = new ArrayList<Comp<?>>();
|
||||
buttonList.add(expandButton);
|
||||
buttonList.add(button);
|
||||
buttonList.add(root);
|
||||
if (section.getDepth() == 1) {
|
||||
buttonList.add(quickAccessButton);
|
||||
}
|
||||
var h = new HorizontalComp(buttonList);
|
||||
h.apply(struc -> struc.get().setFillHeight(true));
|
||||
h.prefHeight(28);
|
||||
list.add(h);
|
||||
list.add(new HorizontalComp(buttonList).apply(struc -> struc.get().setFillHeight(true)));
|
||||
} else {
|
||||
expanded = new SimpleBooleanProperty(true);
|
||||
}
|
||||
|
||||
var content = createChildrenList(c -> new StoreSectionMiniComp(c, this.augment, this.action), Bindings.not(expanded));
|
||||
list.add(content);
|
||||
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
||||
// section is actually expanded
|
||||
var listSections = section.getWrapper() != null
|
||||
? section.getShownChildren()
|
||||
.filtered(
|
||||
storeSection ->
|
||||
section.getAllChildren().getList().size() <= 20 || expanded.get(),
|
||||
expanded,
|
||||
section.getAllChildren().getList())
|
||||
: section.getShownChildren();
|
||||
var content = new ListBoxViewComp<>(
|
||||
listSections.getList(),
|
||||
section.getAllChildren().getList(),
|
||||
(StoreSection e) -> {
|
||||
return new StoreSectionMiniComp(e, this.augment, this.action);
|
||||
},
|
||||
section.getWrapper() == null)
|
||||
.minHeight(0)
|
||||
.hgrow();
|
||||
|
||||
var full = new VerticalComp(list);
|
||||
full.styleClass("store-section-mini-comp");
|
||||
full.apply(struc -> {
|
||||
list.add(content.styleClass("children-content")
|
||||
.hide(Bindings.or(
|
||||
Bindings.not(expanded),
|
||||
Bindings.size(section.getAllChildren().getList()).isEqualTo(0))));
|
||||
|
||||
var vert = new VerticalComp(list);
|
||||
return vert.styleClass("store-section-mini-comp")
|
||||
.apply(struc -> {
|
||||
struc.get().setFillWidth(true);
|
||||
addPseudoClassListeners(struc.get(), expanded);
|
||||
expanded.subscribe(val -> {
|
||||
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
||||
});
|
||||
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||
struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0);
|
||||
struc.get().pseudoClassStateChanged(ROOT, section.getDepth() == 0);
|
||||
struc.get().pseudoClassStateChanged(TOP, section.getDepth() == 1);
|
||||
struc.get().pseudoClassStateChanged(SUB, section.getDepth() > 1);
|
||||
})
|
||||
.apply(struc -> {
|
||||
if (section.getWrapper() != null) {
|
||||
var hbox = ((HBox) struc.get().getChildren().getFirst());
|
||||
addVisibilityListeners(struc.get(), hbox);
|
||||
section.getWrapper().getColor().subscribe(val -> {
|
||||
if (section.getDepth() != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataColor.values())
|
||||
.anyMatch(dataStoreColor ->
|
||||
dataStoreColor.getId().equals(s)));
|
||||
struc.get().getStyleClass().remove("gray");
|
||||
struc.get().getStyleClass().add("color-box");
|
||||
if (val != null) {
|
||||
struc.get().getStyleClass().add(val.getId());
|
||||
} else {
|
||||
struc.get().getStyleClass().add("gray");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return full.createStructure();
|
||||
})
|
||||
.createStructure();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface StoreSortMode {
|
||||
|
||||
StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() {
|
||||
@Override
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
|
@ -20,6 +27,11 @@ public interface StoreSortMode {
|
|||
}
|
||||
};
|
||||
StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() {
|
||||
@Override
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "alphabetical-asc";
|
||||
|
@ -32,10 +44,10 @@ public interface StoreSortMode {
|
|||
.reversed();
|
||||
}
|
||||
};
|
||||
StoreSortMode DATE_DESC = new StoreSortMode.DateSortMode() {
|
||||
StoreSortMode DATE_DESC = new StoreSortMode() {
|
||||
|
||||
protected Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccess().getValue();
|
||||
private Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccessApplied().getValue();
|
||||
if (la == null) {
|
||||
return Instant.MAX;
|
||||
}
|
||||
|
@ -44,19 +56,35 @@ public interface StoreSortMode {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int compare(Instant s1, Instant s2) {
|
||||
return s1.compareTo(s2);
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return Stream.concat(
|
||||
s.getShownChildren().getList().stream()
|
||||
.filter(section -> section.getWrapper()
|
||||
.getEntry()
|
||||
.getValidity()
|
||||
.isUsable())
|
||||
.map(this::representative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "date-desc";
|
||||
}
|
||||
};
|
||||
StoreSortMode DATE_ASC = new StoreSortMode.DateSortMode() {
|
||||
|
||||
protected Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccess().getValue();
|
||||
@Override
|
||||
public Comparator<StoreSection> comparator() {
|
||||
return Comparator.comparing(e -> {
|
||||
return date(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
StoreSortMode DATE_ASC = new StoreSortMode() {
|
||||
|
||||
private Instant date(StoreSection s) {
|
||||
var la = s.getWrapper().getLastAccessApplied().getValue();
|
||||
if (la == null) {
|
||||
return Instant.MIN;
|
||||
}
|
||||
|
@ -65,16 +93,32 @@ public interface StoreSortMode {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected int compare(Instant s1, Instant s2) {
|
||||
return s2.compareTo(s1);
|
||||
public StoreSection representative(StoreSection s) {
|
||||
return Stream.concat(
|
||||
s.getShownChildren().getList().stream()
|
||||
.filter(section -> section.getWrapper()
|
||||
.getEntry()
|
||||
.getValidity()
|
||||
.isUsable())
|
||||
.map(this::representative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "date-asc";
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public Comparator<StoreSection> comparator() {
|
||||
return Comparator.<StoreSection, Instant>comparing(e -> {
|
||||
return date(e);
|
||||
})
|
||||
.reversed();
|
||||
}
|
||||
};
|
||||
List<StoreSortMode> ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC);
|
||||
|
||||
static Optional<StoreSortMode> fromId(String id) {
|
||||
|
@ -87,54 +131,9 @@ public interface StoreSortMode {
|
|||
return DATE_ASC;
|
||||
}
|
||||
|
||||
StoreSection representative(StoreSection s);
|
||||
|
||||
String getId();
|
||||
|
||||
Comparator<StoreSection> comparator();
|
||||
|
||||
abstract class DateSortMode implements StoreSortMode {
|
||||
|
||||
private int entriesListOberservableIndex = -1;
|
||||
private final Map<StoreSection, StoreSection> cachedRepresentatives = new IdentityHashMap<>();
|
||||
|
||||
private StoreSection computeRepresentative(StoreSection s) {
|
||||
return Stream.concat(
|
||||
s.getShownChildren().getList().stream()
|
||||
.filter(section -> section.getWrapper()
|
||||
.getEntry()
|
||||
.getValidity()
|
||||
.isUsable())
|
||||
.map(this::getRepresentative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
private StoreSection getRepresentative(StoreSection s) {
|
||||
if (StoreViewState.get().getEntriesListUpdateObservable().get() != entriesListOberservableIndex) {
|
||||
cachedRepresentatives.clear();
|
||||
entriesListOberservableIndex = StoreViewState.get().getEntriesListUpdateObservable().get();
|
||||
}
|
||||
|
||||
if (cachedRepresentatives.containsKey(s)) {
|
||||
return cachedRepresentatives.get(s);
|
||||
}
|
||||
|
||||
var r = computeRepresentative(s);
|
||||
cachedRepresentatives.put(s, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
protected abstract Instant date(StoreSection s);
|
||||
|
||||
protected abstract int compare(Instant s1, Instant s2);
|
||||
|
||||
@Override
|
||||
public Comparator<StoreSection> comparator() {
|
||||
return (o1, o2) -> {
|
||||
var r1 = getRepresentative(o1);
|
||||
var r2 = getRepresentative(o2);
|
||||
return DateSortMode.this.compare(date(r1), date(r2));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,6 @@ public class StoreViewState {
|
|||
@Getter
|
||||
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
|
||||
|
||||
@Getter
|
||||
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
|
||||
|
||||
@Getter
|
||||
private StoreSection currentTopLevelSection;
|
||||
|
||||
|
@ -121,23 +118,15 @@ public class StoreViewState {
|
|||
.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
|
||||
.map(StoreEntryWrapper::new)
|
||||
.toList()));
|
||||
allEntries.getList().forEach(e -> e.applyLastAccess());
|
||||
categories
|
||||
.getList()
|
||||
.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
|
||||
.map(StoreCategoryWrapper::new)
|
||||
.toList()));
|
||||
|
||||
sortMode.addListener((observable, oldValue, newValue) -> {
|
||||
var cat = getActiveCategory().getValue();
|
||||
if (cat == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cat.getSortMode().setValue(newValue);
|
||||
});
|
||||
activeCategory.addListener((observable, oldValue, newValue) -> {
|
||||
DataStorage.get().setSelectedCategory(newValue.getCategory());
|
||||
sortMode.setValue(newValue.getSortMode().getValue());
|
||||
});
|
||||
var selected = AppCache.getNonNull("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
|
||||
activeCategory.setValue(categories.getList().stream()
|
||||
|
@ -152,6 +141,7 @@ public class StoreViewState {
|
|||
}
|
||||
|
||||
public void updateDisplay() {
|
||||
allEntries.getList().forEach(e -> e.applyLastAccess());
|
||||
toggleStoreListUpdate();
|
||||
}
|
||||
|
||||
|
@ -190,6 +180,7 @@ public class StoreViewState {
|
|||
var l = Arrays.stream(entry)
|
||||
.map(StoreEntryWrapper::new)
|
||||
.peek(storeEntryWrapper -> storeEntryWrapper.update())
|
||||
.peek(wrapper -> wrapper.applyLastAccess())
|
||||
.toList();
|
||||
|
||||
// Don't update anything if we have already reset
|
||||
|
@ -211,7 +202,6 @@ public class StoreViewState {
|
|||
.getUuid())))
|
||||
.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
|
||||
}
|
||||
l.forEach(storeEntryWrapper -> storeEntryWrapper.update());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.issue.TrackEvent;
|
|||
import io.xpipe.app.update.GitHubUpdater;
|
||||
import io.xpipe.app.update.PortableUpdater;
|
||||
import io.xpipe.app.update.UpdateHandler;
|
||||
import io.xpipe.app.update.WebtopUpdater;
|
||||
import io.xpipe.app.util.LocalExec;
|
||||
import io.xpipe.app.util.Translatable;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
@ -29,7 +28,7 @@ public enum AppDistributionType implements Translatable {
|
|||
HOMEBREW("homebrew", true, () -> new PortableUpdater(true)),
|
||||
APT_REPO("apt", true, () -> new PortableUpdater(true)),
|
||||
RPM_REPO("rpm", true, () -> new PortableUpdater(true)),
|
||||
WEBTOP("webtop", true, () -> new WebtopUpdater()),
|
||||
WEBTOP("webtop", true, () -> new PortableUpdater(false)),
|
||||
CHOCO("choco", true, () -> new PortableUpdater(true));
|
||||
|
||||
private static AppDistributionType type;
|
||||
|
|
|
@ -20,6 +20,10 @@ public class AppFont {
|
|||
// Load ikonli fonts
|
||||
TrackEvent.info("Loading ikonli fonts ...");
|
||||
new FontIcon("mdi2s-stop");
|
||||
new FontIcon("mdi2m-magnify");
|
||||
new FontIcon("mdi2d-database-plus");
|
||||
new FontIcon("mdi2p-professional-hexagon");
|
||||
new FontIcon("mdi2c-chevron-double-right");
|
||||
|
||||
TrackEvent.info("Loading bundled fonts ...");
|
||||
AppResources.with(
|
||||
|
|
|
@ -56,7 +56,10 @@ public class AppInstance {
|
|||
try {
|
||||
var inputs = AppProperties.get().getArguments().getOpenArgs();
|
||||
// Assume that we want to open the GUI if we launched again
|
||||
client.get().performRequest(DaemonFocusExchange.Request.builder().build());
|
||||
client.get()
|
||||
.performRequest(DaemonFocusExchange.Request.builder()
|
||||
.mode(XPipeDaemonMode.GUI)
|
||||
.build());
|
||||
if (!inputs.isEmpty()) {
|
||||
client.get()
|
||||
.performRequest(DaemonOpenExchange.Request.builder()
|
||||
|
|
|
@ -125,15 +125,14 @@ public class AppLayoutModel {
|
|||
new LabelGraphic.IconGraphic("mdi2b-book-open-variant"),
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.DOCS),
|
||||
null),
|
||||
new Entry(
|
||||
AppI18n.observable("webtop"),
|
||||
new LabelGraphic.IconGraphic("mdi2d-desktop-mac"),
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
|
||||
null)));
|
||||
if (AppDistributionType.get() != AppDistributionType.WEBTOP) {
|
||||
l.add(new Entry(
|
||||
AppI18n.observable("webtop"),
|
||||
new LabelGraphic.IconGraphic("mdi2d-desktop-mac"),
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
|
||||
null));
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,6 @@ public class AppProperties {
|
|||
boolean autoAcceptEula;
|
||||
UUID uuid;
|
||||
boolean initialLaunch;
|
||||
boolean restarted;
|
||||
/**
|
||||
* Unique identifier that resets on every XPipe restart.
|
||||
*/
|
||||
|
@ -130,9 +129,6 @@ public class AppProperties {
|
|||
autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula"))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
restarted = Optional.ofNullable(System.getProperty("io.xpipe.app.restarted"))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
|
||||
// We require the user dir from here
|
||||
AppUserDirectoryCheck.check(dataDir);
|
||||
|
|
|
@ -19,7 +19,6 @@ import javafx.application.ColorScheme;
|
|||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
@ -84,40 +83,33 @@ public class AppTheme {
|
|||
|
||||
public static void init() {
|
||||
if (init) {
|
||||
TrackEvent.trace("Theme init requested again");
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppPrefs.get() == null) {
|
||||
TrackEvent.trace("Theme init prior to prefs init, setting theme to default");
|
||||
Theme.getDefaultLightTheme().apply();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var lastSystemDark = AppCache.getBoolean("lastDarkTheme", false);
|
||||
var nowDark = isDarkMode();
|
||||
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
|
||||
AppCache.update("lastDarkTheme", nowDark);
|
||||
if (AppPrefs.get().theme().getValue() == null || lastSystemDark != nowDark) {
|
||||
TrackEvent.trace("Updating theme to system theme");
|
||||
setDefault();
|
||||
}
|
||||
|
||||
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
|
||||
TrackEvent.withTrace("Platform preference changed").tag("change", change.toString()).handle();
|
||||
});
|
||||
|
||||
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
|
||||
if (change.getKey().equals("GTK.theme_name")) {
|
||||
Platform.runLater(() -> {
|
||||
updateThemeToThemeName(change.getValueRemoved(), change.getValueAdded());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Platform.getPreferences().colorSchemeProperty().addListener((observableValue, colorScheme, t1) -> {
|
||||
Platform.runLater(() -> {
|
||||
updateThemeToColorScheme(t1);
|
||||
if (t1 == ColorScheme.DARK
|
||||
&& !AppPrefs.get().theme().getValue().isDark()) {
|
||||
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
|
||||
}
|
||||
|
||||
if (t1 != ColorScheme.DARK
|
||||
&& AppPrefs.get().theme().getValue().isDark()) {
|
||||
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (IllegalStateException ex) {
|
||||
|
@ -138,52 +130,12 @@ public class AppTheme {
|
|||
init = true;
|
||||
}
|
||||
|
||||
private static void updateThemeToThemeName(Object oldName, Object newName) {
|
||||
if (OsType.getLocal() == OsType.LINUX && newName != null) {
|
||||
var toDark = (oldName == null || !oldName.toString().contains("-dark")) &&
|
||||
newName.toString().contains("-dark");
|
||||
var toLight = (oldName == null || oldName.toString().contains("-dark")) &&
|
||||
!newName.toString().contains("-dark");
|
||||
if (toDark) {
|
||||
updateThemeToColorScheme(ColorScheme.DARK);
|
||||
} else if (toLight) {
|
||||
updateThemeToColorScheme(ColorScheme.LIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDarkMode() {
|
||||
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
|
||||
if (nowDark) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var gtkTheme = Platform.getPreferences().get("GTK.theme_name");
|
||||
return gtkTheme != null && gtkTheme.toString().contains("-dark");
|
||||
}
|
||||
|
||||
private static void updateThemeToColorScheme(ColorScheme colorScheme) {
|
||||
if (colorScheme == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (colorScheme == ColorScheme.DARK
|
||||
&& !AppPrefs.get().theme().getValue().isDark()) {
|
||||
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
|
||||
}
|
||||
|
||||
if (colorScheme != ColorScheme.DARK
|
||||
&& AppPrefs.get().theme().getValue().isDark()) {
|
||||
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
|
||||
}
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
if (!init) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nowDark = isDarkMode();
|
||||
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
|
||||
AppCache.update("lastDarkTheme", nowDark);
|
||||
}
|
||||
|
||||
|
@ -210,26 +162,19 @@ public class AppTheme {
|
|||
}
|
||||
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
var window = AppMainWindow.getInstance();
|
||||
if (window == null) {
|
||||
if (AppMainWindow.getInstance() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackEvent.debug("Setting theme " + newTheme.getId() + " for scene");
|
||||
|
||||
// Don't animate transition in performance mode
|
||||
if (AppPrefs.get() == null || AppPrefs.get().performanceMode().get()) {
|
||||
newTheme.apply();
|
||||
return;
|
||||
}
|
||||
|
||||
var stage = window.getStage();
|
||||
var scene = stage.getScene();
|
||||
var window = AppMainWindow.getInstance().getStage();
|
||||
var scene = window.getScene();
|
||||
Pane root = (Pane) scene.getRoot();
|
||||
Image snapshot = scene.snapshot(null);
|
||||
ImageView imageView = new ImageView(snapshot);
|
||||
root.getChildren().add(imageView);
|
||||
|
||||
newTheme.apply();
|
||||
TrackEvent.debug("Set theme " + newTheme.getId() + " for scene");
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// Animate!
|
||||
|
@ -355,7 +300,7 @@ public class AppTheme {
|
|||
AppFontSizes.forOs(AppFontSizes.BASE_11, AppFontSizes.BASE_10, AppFontSizes.BASE_11),
|
||||
() -> ColorHelper.withOpacity(
|
||||
Platform.getPreferences().getAccentColor().desaturate().desaturate(), 0.2),
|
||||
91);
|
||||
115);
|
||||
|
||||
// Adjust this to create your own theme
|
||||
public static final Theme CUSTOM = new DerivedTheme(
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package io.xpipe.app.core.check;
|
||||
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AppBundledToolsCheck {
|
||||
|
||||
private static boolean getResult() {
|
||||
var fc = new ProcessBuilder("where", "ssh")
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(ProcessBuilder.Redirect.DISCARD);
|
||||
try {
|
||||
var proc = fc.start();
|
||||
proc.waitFor(2, TimeUnit.SECONDS);
|
||||
return proc.exitValue() == 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void check() {
|
||||
if (AppPrefs.get().useBundledTools().get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OsType.getLocal().equals(OsType.WINDOWS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getResult()) {
|
||||
AppPrefs.get().useBundledTools.set(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -56,6 +56,9 @@ public class BaseMode extends OperationMode {
|
|||
TrackEvent.info("Initializing base mode components ...");
|
||||
AppMainWindow.loadingText("initializingApp");
|
||||
LicenseProvider.get().init();
|
||||
// We no longer need this
|
||||
// AppCertutilCheck.check();
|
||||
AppBundledToolsCheck.check();
|
||||
AppHomebrewCoreutilsCheck.check();
|
||||
AppAvCheck.check();
|
||||
AppJavaOptionsCheck.check();
|
||||
|
@ -91,7 +94,6 @@ public class BaseMode extends OperationMode {
|
|||
AppPrefs.setLocalDefaultsIfNeeded();
|
||||
PlatformInit.init(true);
|
||||
AppMainWindow.addUpdateTitleListener();
|
||||
TrackEvent.info("Shell initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
shellLoaded.await();
|
||||
|
@ -104,16 +106,15 @@ public class BaseMode extends OperationMode {
|
|||
DataStorage.init();
|
||||
storageLoaded.countDown();
|
||||
StoreViewState.init();
|
||||
AppMainWindow.loadingText("loadingUserInterface");
|
||||
AppLayoutModel.init();
|
||||
PlatformInit.init(true);
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
AppGreetingsDialog.showIfNeeded();
|
||||
AppMainWindow.loadingText("initializingApp");
|
||||
});
|
||||
imagesLoaded.await();
|
||||
browserLoaded.await();
|
||||
iconsLoaded.await();
|
||||
TrackEvent.info("Waiting for startup dialogs to close");
|
||||
AppDialog.waitForAllDialogsClose();
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
try {
|
||||
|
@ -123,7 +124,6 @@ public class BaseMode extends OperationMode {
|
|||
}
|
||||
});
|
||||
UpdateChangelogAlert.showIfNeeded();
|
||||
TrackEvent.info("Connection storage initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
AppFileWatcher.init();
|
||||
|
@ -131,7 +131,6 @@ public class BaseMode extends OperationMode {
|
|||
BlobManager.init();
|
||||
TerminalView.init();
|
||||
TerminalLauncherManager.init();
|
||||
TrackEvent.info("File/Watcher initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
PlatformInit.init(true);
|
||||
|
@ -140,16 +139,13 @@ public class BaseMode extends OperationMode {
|
|||
storageLoaded.await();
|
||||
SystemIconManager.init();
|
||||
iconsLoaded.countDown();
|
||||
TrackEvent.info("Platform initialization thread completed");
|
||||
},
|
||||
() -> {
|
||||
BrowserIconManager.loadIfNecessary();
|
||||
shellLoaded.await();
|
||||
BrowserLocalFileSystem.init();
|
||||
storageLoaded.await();
|
||||
BrowserFullSessionModel.init();
|
||||
browserLoaded.countDown();
|
||||
TrackEvent.info("Browser initialization thread completed");
|
||||
});
|
||||
ActionProvider.initProviders();
|
||||
DataStoreProviders.init();
|
||||
|
|
|
@ -180,8 +180,6 @@ public abstract class OperationMode {
|
|||
|
||||
var startupMode = getStartupMode();
|
||||
switchToSyncOrThrow(map(startupMode));
|
||||
// If it doesn't find time, the JVM will not gc the startup workload
|
||||
System.gc();
|
||||
inStartup = false;
|
||||
AppOpenArguments.init();
|
||||
}
|
||||
|
@ -258,7 +256,7 @@ public abstract class OperationMode {
|
|||
var exec = XPipeInstallation.createExternalAsyncLaunchCommand(
|
||||
loc,
|
||||
XPipeDaemonMode.GUI,
|
||||
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir + "\" \"-Dio.xpipe.app.restarted=true\"",
|
||||
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir + "\"",
|
||||
true);
|
||||
LocalShell.getShell().executeSimpleCommand(exec);
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ public class AppDialog {
|
|||
var transition = new PauseTransition(Duration.millis(200));
|
||||
transition.setOnFinished(e -> {
|
||||
if (wait) {
|
||||
PlatformThread.exitNestedEventLoop(key);
|
||||
Platform.exitNestedEventLoop(key, null);
|
||||
}
|
||||
});
|
||||
transition.play();
|
||||
|
@ -95,7 +95,7 @@ public class AppDialog {
|
|||
}
|
||||
});
|
||||
if (wait) {
|
||||
PlatformThread.enterNestedEventLoop(key);
|
||||
Platform.enterNestedEventLoop(key);
|
||||
waitForDialogClose(o);
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +108,6 @@ public class AppDialog {
|
|||
public static Comp<?> dialogText(String s) {
|
||||
return Comp.of(() -> {
|
||||
var text = new Text(s);
|
||||
text.getStyleClass().add("dialog-text");
|
||||
text.setWrappingWidth(450);
|
||||
var sp = new StackPane(text);
|
||||
return sp;
|
||||
|
@ -119,7 +118,6 @@ public class AppDialog {
|
|||
public static Comp<?> dialogText(ObservableValue<String> s) {
|
||||
return Comp.of(() -> {
|
||||
var text = new Text();
|
||||
text.getStyleClass().add("dialog-text");
|
||||
text.textProperty().bind(s);
|
||||
text.setWrappingWidth(450);
|
||||
var sp = new StackPane(text);
|
||||
|
|
|
@ -138,10 +138,8 @@ public class AppMainWindow {
|
|||
}
|
||||
|
||||
public static synchronized void initContent() {
|
||||
TrackEvent.info("Window content node creation started");
|
||||
var content = new AppLayoutComp();
|
||||
var s = content.createStructure();
|
||||
TrackEvent.info("Window content node structure created");
|
||||
loadedContent.setValue(s);
|
||||
}
|
||||
|
||||
|
@ -152,13 +150,6 @@ public class AppMainWindow {
|
|||
}
|
||||
}
|
||||
|
||||
public void focus() {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
stage.setIconified(false);
|
||||
stage.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
private static String createTitle() {
|
||||
var t = LicenseProvider.get().licenseTitle();
|
||||
var base =
|
||||
|
@ -368,13 +359,8 @@ public class AppMainWindow {
|
|||
}
|
||||
TrackEvent.debug("Window loaded saved bounds");
|
||||
} else if (!AppProperties.get().isShowcase()) {
|
||||
if (AppDistributionType.get() == AppDistributionType.WEBTOP) {
|
||||
stage.setWidth(1000);
|
||||
stage.setHeight(600);
|
||||
} else {
|
||||
stage.setWidth(1280);
|
||||
stage.setHeight(720);
|
||||
}
|
||||
stage.setWidth(1280);
|
||||
stage.setHeight(720);
|
||||
} else {
|
||||
stage.setX(312);
|
||||
stage.setY(149);
|
||||
|
|
|
@ -89,10 +89,6 @@ public class AppWindowBounds {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (allScreenBounds.getWidth() == 0 || allScreenBounds.getHeight() == 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Alerts do not have a custom x/y set, but we are able to handle that
|
||||
|
||||
boolean changed = false;
|
||||
|
|
|
@ -124,13 +124,10 @@ public class ModifiedStage extends Stage {
|
|||
var transition = new PauseTransition(Duration.millis(300));
|
||||
transition.setOnFinished(e -> {
|
||||
applyModes(stage);
|
||||
// We only need to update the frame by resizing on Windows
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
stage.setWidth(stage.getWidth() - 1);
|
||||
Platform.runLater(() -> {
|
||||
stage.setWidth(stage.getWidth() + 1);
|
||||
});
|
||||
}
|
||||
stage.setWidth(stage.getWidth() - 1);
|
||||
Platform.runLater(() -> {
|
||||
stage.setWidth(stage.getWidth() + 1);
|
||||
});
|
||||
});
|
||||
transition.play();
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ package io.xpipe.app.ext;
|
|||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.util.FailableConsumer;
|
||||
|
@ -22,7 +21,6 @@ public interface ActionProvider {
|
|||
List<ActionProvider> ALL_STANDALONE = new ArrayList<>();
|
||||
|
||||
static void initProviders() {
|
||||
TrackEvent.trace("Starting action provider initialization");
|
||||
for (ActionProvider actionProvider : ALL) {
|
||||
try {
|
||||
actionProvider.init();
|
||||
|
@ -30,7 +28,6 @@ public interface ActionProvider {
|
|||
ErrorEvent.fromThrowable(t).handle();
|
||||
}
|
||||
}
|
||||
TrackEvent.trace("Finished action provider initialization");
|
||||
}
|
||||
|
||||
default void init() throws Exception {}
|
||||
|
|
|
@ -45,17 +45,13 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
var d = shellControl.getShellDialect().getDumbMode();
|
||||
if (!d.supportsAnyPossibleInteraction()) {
|
||||
shellControl.close();
|
||||
try {
|
||||
d.throwIfUnsupported();
|
||||
} catch (Exception e) {
|
||||
throw ErrorEvent.expected(e);
|
||||
}
|
||||
d.throwIfUnsupported();
|
||||
}
|
||||
|
||||
if (!shellControl.getTtyState().isPreservesOutput()
|
||||
|| !shellControl.getTtyState().isSupportsInput()) {
|
||||
throw ErrorEvent.expected(new UnsupportedOperationException(
|
||||
"Shell has a PTY allocated and as a result does not support file system operations. For more information see " + Hyperlinks.DOCS_TTY));
|
||||
throw new UnsupportedOperationException(
|
||||
"Shell has a PTY allocated and as a result does not support file system operations. For more information see " + Hyperlinks.DOCS_TTY);
|
||||
}
|
||||
|
||||
shellControl.checkLicenseOrThrow();
|
||||
|
|
|
@ -93,8 +93,8 @@ public interface DataStoreProvider {
|
|||
return StoreEntryComp.create(s, null, preferLarge);
|
||||
}
|
||||
|
||||
default StoreSectionComp customSectionComp(StoreSection section) {
|
||||
return new StoreSectionComp(section);
|
||||
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
|
||||
return new StoreSectionComp(section, topLevel);
|
||||
}
|
||||
|
||||
default boolean shouldShowScan() {
|
||||
|
|
|
@ -7,7 +7,6 @@ import com.github.weisj.jsvg.SVGDocument;
|
|||
import com.github.weisj.jsvg.SVGRenderingHints;
|
||||
import com.github.weisj.jsvg.attributes.ViewBox;
|
||||
import com.github.weisj.jsvg.parser.SVGLoader;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.awt.*;
|
||||
|
@ -22,14 +21,6 @@ import javax.imageio.ImageIO;
|
|||
|
||||
public class SystemIconCache {
|
||||
|
||||
private static enum ImageColorScheme {
|
||||
|
||||
TRANSPARENT,
|
||||
MIXED,
|
||||
LIGHT,
|
||||
DARK
|
||||
}
|
||||
|
||||
private static final Path DIRECTORY =
|
||||
AppProperties.get().getDataDir().resolve("cache").resolve("icons").resolve("raster");
|
||||
private static final int[] sizes = new int[] {16, 24, 40, 80};
|
||||
|
@ -59,31 +50,11 @@ public class SystemIconCache {
|
|||
Files.createDirectories(target);
|
||||
|
||||
for (var icon : e.getValue().getIcons()) {
|
||||
var dark = icon.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK;
|
||||
if (refreshChecksum(icon.getFile(), target, icon.getName(), dark)) {
|
||||
if (refreshChecksum(icon.getFile(), target, icon.getName(), icon.isDark())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var scheme = rasterizeSizes(icon.getFile(), target, icon.getName(), dark);
|
||||
if (scheme == ImageColorScheme.TRANSPARENT) {
|
||||
var message = "Failed to rasterize icon icon " + icon.getFile().getFileName().toString() + ": Rasterized image is transparent";
|
||||
ErrorEvent.fromMessage(message).omit().expected().handle();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scheme != ImageColorScheme.DARK || icon.getColorSchemeData() != SystemIconSourceFile.ColorSchemeData.DEFAULT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasExplicitDark = e.getValue().getIcons().stream().anyMatch(
|
||||
systemIconSourceFile -> systemIconSourceFile.getSource().equals(icon.getSource()) &&
|
||||
systemIconSourceFile.getName().equals(icon.getName()) &&
|
||||
systemIconSourceFile.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK);
|
||||
if (hasExplicitDark) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rasterizeSizesInverted(icon.getFile(), target, icon.getName(), true);
|
||||
rasterizeSizes(icon.getFile(), target, icon.getName(), icon.isDark());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -106,60 +77,28 @@ public class SystemIconCache {
|
|||
}
|
||||
}
|
||||
|
||||
private static ImageColorScheme rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
|
||||
TrackEvent.trace("Rasterizing image " + path.getFileName().toString());
|
||||
private static boolean rasterizeSizes(Path path, Path dir, String name, boolean dark) throws IOException {
|
||||
try {
|
||||
ImageColorScheme c = null;
|
||||
for (var size : sizes) {
|
||||
var image = rasterize(path, size);
|
||||
if (image == null) {
|
||||
continue;
|
||||
}
|
||||
if (c == null) {
|
||||
c = determineColorScheme(image);
|
||||
if (c == ImageColorScheme.TRANSPARENT) {
|
||||
return ImageColorScheme.TRANSPARENT;
|
||||
}
|
||||
}
|
||||
write(dir, name, dark, size, image);
|
||||
rasterize(path, dir, name, dark, size);
|
||||
}
|
||||
return c;
|
||||
} catch (Exception ex) {
|
||||
var message = "Failed to rasterize icon icon " + path.getFileName().toString() + ": " + ex.getMessage();
|
||||
ErrorEvent.fromThrowable(ex).description(message).omit().expected().handle();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark) throws IOException {
|
||||
try {
|
||||
ImageColorScheme c = null;
|
||||
for (var size : sizes) {
|
||||
var image = rasterize(path, size);
|
||||
if (image == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var invert = invert(image);
|
||||
write(dir, name, dark, size, invert);
|
||||
}
|
||||
return c;
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
if (ex instanceof IOException) {
|
||||
throw ex;
|
||||
}
|
||||
|
||||
ErrorEvent.fromThrowable(ex).omit().expected().handle();
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static BufferedImage rasterize(Path path, int px) throws IOException {
|
||||
private static void rasterize(Path path, Path dir, String name, boolean dark, int px) throws IOException {
|
||||
SVGLoader loader = new SVGLoader();
|
||||
URL svgUrl = path.toUri().toURL();
|
||||
SVGDocument svgDocument = loader.load(svgUrl);
|
||||
if (svgDocument == null) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage image = new BufferedImage(px, px, BufferedImage.TYPE_INT_ARGB);
|
||||
|
@ -170,67 +109,8 @@ public class SystemIconCache {
|
|||
g.setRenderingHint(SVGRenderingHints.KEY_SOFT_CLIPPING, SVGRenderingHints.VALUE_SOFT_CLIPPING_ON);
|
||||
svgDocument.render((Component) null, g, new ViewBox(0, 0, px, px));
|
||||
g.dispose();
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image) throws IOException {
|
||||
var out = dir.resolve(name + "-" + px + (dark ? "-dark" : "") + ".png");
|
||||
ImageIO.write(image, "png", out.toFile());
|
||||
return image;
|
||||
}
|
||||
|
||||
private static BufferedImage invert(BufferedImage image) {
|
||||
var buffer = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
int clr = image.getRGB(x, y);
|
||||
int alpha = (clr >> 24) & 0xff;
|
||||
int red = (clr & 0x00ff0000) >> 16;
|
||||
int green = (clr & 0x0000ff00) >> 8;
|
||||
int blue = clr & 0x000000ff;
|
||||
buffer.setRGB(x, y, new Color(255- red, 255- green, 255- blue, alpha).getRGB());
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static ImageColorScheme determineColorScheme(BufferedImage image) {
|
||||
var transparent = true;
|
||||
var counter = 0;
|
||||
var mean = 0.0;
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
int clr = image.getRGB(x, y);
|
||||
int alpha = (clr >> 24) & 0xff;
|
||||
int red = (clr & 0x00ff0000) >> 16;
|
||||
int green = (clr & 0x0000ff00) >> 8;
|
||||
int blue = clr & 0x000000ff;
|
||||
|
||||
if (alpha > 0) {
|
||||
transparent = false;
|
||||
}
|
||||
|
||||
if (alpha < 200) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mean += red + green + blue;
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
if (transparent) {
|
||||
return ImageColorScheme.TRANSPARENT;
|
||||
}
|
||||
|
||||
mean /= counter * 3;
|
||||
if (mean < 50) {
|
||||
return ImageColorScheme.DARK;
|
||||
} else if (mean > 205) {
|
||||
return ImageColorScheme.LIGHT;
|
||||
} else {
|
||||
return ImageColorScheme.MIXED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,18 +80,7 @@ public class SystemIconManager {
|
|||
});
|
||||
}
|
||||
|
||||
private static void reloadImages() {
|
||||
AppImages.remove(s -> s.startsWith("icons/"));
|
||||
try {
|
||||
for (var source : getEffectiveSources()) {
|
||||
AppImages.loadRasterImages(SystemIconCache.getDirectory(source), "icons/" + source.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearInvalidImages() {
|
||||
public static void reloadImages() {
|
||||
AppImages.remove(s -> s.startsWith("icons/"));
|
||||
try {
|
||||
for (var source : getEffectiveSources()) {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package io.xpipe.app.icon;
|
||||
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
|
@ -92,14 +90,7 @@ public interface SystemIconSource {
|
|||
@Override
|
||||
public void refresh() throws Exception {
|
||||
try (var sc =
|
||||
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
|
||||
var present = sc.view().findProgram("git").isPresent();
|
||||
if (!present) {
|
||||
var msg = "Git command-line tools are not available in the PATH but are required to use icons from a git repository. For more details, see https://git-scm.com/downloads.";
|
||||
ErrorEvent.fromMessage(msg).expected().handle();
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
|
||||
var dir = SystemIconManager.getPoolPath().resolve(id);
|
||||
if (!Files.exists(dir)) {
|
||||
sc.command(CommandBuilder.of()
|
||||
|
|
|
@ -31,53 +31,25 @@ public class SystemIconSourceData {
|
|||
}
|
||||
|
||||
var files = Files.walk(dir).toList();
|
||||
List<Path> flatFiles = files.stream()
|
||||
.filter(path -> Files.isRegularFile(path))
|
||||
.filter(path -> path.toString().endsWith(".svg"))
|
||||
.map(path -> {
|
||||
var name = FilenameUtils.getBaseName(path.getFileName().toString());
|
||||
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
|
||||
var cleanedPath = path.getParent().resolve(cleanedName + ".svg");
|
||||
return cleanedPath;
|
||||
}).toList();
|
||||
for (var file : flatFiles) {
|
||||
var name = FilenameUtils.getBaseName(file.getFileName().toString());
|
||||
var displayName = name.toLowerCase(Locale.ROOT);
|
||||
var baseFile = file.getParent().resolve(name + ".svg");
|
||||
var hasBaseVariant = Files.exists(baseFile);
|
||||
var darkModeFile = file.getParent().resolve(name + "-light.svg");
|
||||
var hasDarkModeVariant = Files.exists(darkModeFile);
|
||||
var lightModeFile = file.getParent().resolve(name + "-dark.svg");
|
||||
var hasLightModeVariant = Files.exists(lightModeFile);
|
||||
|
||||
if (hasBaseVariant && hasDarkModeVariant) {
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasBaseVariant && hasLightModeVariant) {
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DARK));
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasBaseVariant) {
|
||||
if (hasLightModeVariant) {
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
if (hasDarkModeVariant) {
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
|
||||
}
|
||||
} else {
|
||||
if (hasDarkModeVariant) {
|
||||
sourceFiles.add(
|
||||
new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
}
|
||||
for (var file : files) {
|
||||
if (file.getFileName().toString().endsWith(".svg")) {
|
||||
var name = FilenameUtils.getBaseName(file.getFileName().toString());
|
||||
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
|
||||
var hasLightVariant = Files.exists(file.getParent().resolve(cleanedName + "-light.svg"));
|
||||
var hasDarkVariant = Files.exists(file.getParent().resolve(cleanedName + "-dark.svg"));
|
||||
if (hasLightVariant && !hasDarkVariant && name.endsWith("-light")) {
|
||||
var s = new SystemIconSourceFile(source, cleanedName.toLowerCase(Locale.ROOT), file, true);
|
||||
sourceFiles.add(s);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
if (hasLightVariant && hasDarkVariant && (name.endsWith("-dark") || name.endsWith("-light"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var s = new SystemIconSourceFile(source, cleanedName.toLowerCase(Locale.ROOT), file, false);
|
||||
sourceFiles.add(s);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
|
|
|
@ -7,13 +7,8 @@ import java.nio.file.Path;
|
|||
@Value
|
||||
public class SystemIconSourceFile {
|
||||
|
||||
public static enum ColorSchemeData {
|
||||
DARK,
|
||||
DEFAULT;
|
||||
}
|
||||
|
||||
SystemIconSource source;
|
||||
String name;
|
||||
Path file;
|
||||
ColorSchemeData colorSchemeData;
|
||||
boolean dark;
|
||||
}
|
||||
|
|
|
@ -39,14 +39,6 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
return hasEmail || hasText;
|
||||
}
|
||||
|
||||
private static boolean doesExceedCommentSize(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return text.length() > 5000;
|
||||
}
|
||||
|
||||
private static Throwable adjustCopy(Throwable throwable, boolean clear) {
|
||||
if (throwable == null) {
|
||||
return null;
|
||||
|
@ -147,16 +139,6 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
atts.forEach(attachment -> s.addAttachment(attachment));
|
||||
}
|
||||
|
||||
if (doesExceedCommentSize(ee.getUserReport())) {
|
||||
try {
|
||||
var report = Files.createTempFile("report", ".txt");
|
||||
Files.writeString(report, ee.getUserReport());
|
||||
s.addAttachment(new Attachment(report.toString()));
|
||||
} catch (Exception ex) {
|
||||
AppLogs.get().logException("Unable to create report file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
s.setTag(
|
||||
"hasLicense",
|
||||
String.valueOf(
|
||||
|
@ -194,7 +176,7 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
AppPrefs.get() != null
|
||||
? String.valueOf(AppPrefs.get().useLocalFallbackShell().get())
|
||||
: "unknown");
|
||||
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : "false");
|
||||
s.setTag("initial", AppProperties.get() != null ? AppProperties.get().isInitialLaunch() + "" : null);
|
||||
|
||||
var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null;
|
||||
if (ee.getDescription() != null
|
||||
|
@ -249,11 +231,7 @@ public class SentryErrorHandler implements ErrorHandler {
|
|||
if (hasEmail) {
|
||||
fb.setEmail(email);
|
||||
}
|
||||
if (doesExceedCommentSize(text)) {
|
||||
fb.setComments("<Attachment>");
|
||||
} else {
|
||||
fb.setComments(text);
|
||||
}
|
||||
fb.setComments(text);
|
||||
Sentry.captureUserFeedback(fb);
|
||||
}
|
||||
Sentry.flush(3000);
|
||||
|
|
|
@ -32,8 +32,8 @@ public class AboutCategory extends AppPrefsCategory {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new TileButtonComp("documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS);
|
||||
new TileButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
|
||||
Hyperlinks.open(Hyperlinks.SLACK);
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
|
@ -45,6 +45,13 @@ public class AboutCategory extends AppPrefsCategory {
|
|||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new TileButtonComp("securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS_SECURITY);
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS_PRIVACY);
|
||||
|
@ -59,8 +66,7 @@ public class AboutCategory extends AppPrefsCategory {
|
|||
.styleClass("open-source-notices");
|
||||
var modal = ModalOverlay.of("openSourceNotices", comp);
|
||||
modal.show();
|
||||
})
|
||||
.grow(true, false))
|
||||
}))
|
||||
.addComp(
|
||||
new TileButtonComp("eula", "eulaDescription", "mdi2c-card-text-outline", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS_EULA);
|
||||
|
|
|
@ -9,7 +9,6 @@ import io.xpipe.app.icon.SystemIconSource;
|
|||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.terminal.ExternalTerminalType;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.util.ModuleHelper;
|
||||
|
||||
|
@ -58,6 +57,8 @@ public class AppPrefs {
|
|||
mapVaultShared(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class, false);
|
||||
public final BooleanProperty performanceMode =
|
||||
mapLocal(new SimpleBooleanProperty(), "performanceMode", Boolean.class, false);
|
||||
public final BooleanProperty useBundledTools =
|
||||
mapLocal(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class, true);
|
||||
public final ObjectProperty<AppTheme.Theme> theme =
|
||||
mapLocal(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class, false);
|
||||
final BooleanProperty useSystemFont =
|
||||
|
@ -117,8 +118,6 @@ public class AppPrefs {
|
|||
mapLocal(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class, false);
|
||||
final StringProperty customEditorCommand =
|
||||
mapLocal(new SimpleStringProperty(""), "customEditorCommand", String.class, false);
|
||||
final BooleanProperty customEditorCommandInTerminal =
|
||||
mapLocal(new SimpleBooleanProperty(false), "customEditorCommandInTerminal", Boolean.class, false);
|
||||
final BooleanProperty automaticallyCheckForUpdates =
|
||||
mapLocal(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class, false);
|
||||
final BooleanProperty encryptAllVaultData =
|
||||
|
@ -250,8 +249,8 @@ public class AppPrefs {
|
|||
new AboutCategory(),
|
||||
new SystemCategory(),
|
||||
new AppearanceCategory(),
|
||||
new VaultCategory(),
|
||||
new SyncCategory(),
|
||||
new VaultCategory(),
|
||||
new TerminalCategory(),
|
||||
new EditorCategory(),
|
||||
new RdpCategory(),
|
||||
|
@ -273,7 +272,7 @@ public class AppPrefs {
|
|||
INSTANCE = new AppPrefs();
|
||||
PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler));
|
||||
INSTANCE.loadLocal();
|
||||
INSTANCE.adjustLocalValues();
|
||||
INSTANCE.fixInvalidLocalValues();
|
||||
INSTANCE.vaultStorageHandler = new AppPrefsStorageHandler(
|
||||
INSTANCE.storageDirectory().getValue().resolve("preferences.json"));
|
||||
}
|
||||
|
@ -327,6 +326,10 @@ public class AppPrefs {
|
|||
return performanceMode;
|
||||
}
|
||||
|
||||
public ObservableBooleanValue useBundledTools() {
|
||||
return useBundledTools;
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> useSystemFont() {
|
||||
return useSystemFont;
|
||||
}
|
||||
|
@ -407,10 +410,6 @@ public class AppPrefs {
|
|||
return customEditorCommand;
|
||||
}
|
||||
|
||||
public ObservableBooleanValue customEditorCommandInTerminal() {
|
||||
return customEditorCommandInTerminal;
|
||||
}
|
||||
|
||||
public final ReadOnlyIntegerProperty editorReloadTimeout() {
|
||||
return editorReloadTimeout;
|
||||
}
|
||||
|
@ -532,7 +531,7 @@ public class AppPrefs {
|
|||
}
|
||||
}
|
||||
|
||||
private void adjustLocalValues() {
|
||||
private void fixInvalidLocalValues() {
|
||||
// You can set the directory to empty in the settings
|
||||
if (storageDirectory.get() == null || storageDirectory.get().toString().isBlank()) {
|
||||
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
|
||||
|
@ -544,11 +543,6 @@ public class AppPrefs {
|
|||
ErrorEvent.fromThrowable(e).expected().build().handle();
|
||||
storageDirectory.setValue(DEFAULT_STORAGE_DIR);
|
||||
}
|
||||
|
||||
if (AppProperties.get().isInitialLaunch()) {
|
||||
var f = PlatformState.determineDefaultScalingFactor();
|
||||
uiScale.setValue(f.isPresent() ? f.getAsInt() : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSharedRemote() {
|
||||
|
|
|
@ -52,6 +52,7 @@ public class AppPrefsComp extends SimpleComp {
|
|||
split.setFillHeight(true);
|
||||
split.getStyleClass().add("prefs");
|
||||
var stack = new StackPane(split);
|
||||
stack.setPickOnBounds(false);
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,25 +14,24 @@ public class ConnectionsCategory extends AppPrefsCategory {
|
|||
@Override
|
||||
protected Comp<?> create() {
|
||||
var prefs = AppPrefs.get();
|
||||
var connectionsBuilder = new OptionsBuilder().pref(prefs.condenseConnectionDisplay).addToggle(prefs.condenseConnectionDisplay).pref(
|
||||
prefs.showChildCategoriesInParentCategory).addToggle(prefs.showChildCategoriesInParentCategory).pref(
|
||||
prefs.openConnectionSearchWindowOnConnectionCreation).addToggle(prefs.openConnectionSearchWindowOnConnectionCreation).pref(
|
||||
prefs.requireDoubleClickForConnections).addToggle(prefs.requireDoubleClickForConnections);
|
||||
var localShellBuilder = new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell);
|
||||
// Change order to prioritize fallback shell on macOS
|
||||
var options = OsType.getLocal() == OsType.MACOS ? new OptionsBuilder()
|
||||
.addTitle("localShell")
|
||||
.sub(localShellBuilder)
|
||||
var options = new OptionsBuilder()
|
||||
.addTitle("connections")
|
||||
.sub(connectionsBuilder) :
|
||||
new OptionsBuilder()
|
||||
.addTitle("connections")
|
||||
.sub(connectionsBuilder)
|
||||
.sub(new OptionsBuilder()
|
||||
.pref(prefs.condenseConnectionDisplay)
|
||||
.addToggle(prefs.condenseConnectionDisplay)
|
||||
.pref(prefs.showChildCategoriesInParentCategory)
|
||||
.addToggle(prefs.showChildCategoriesInParentCategory)
|
||||
.pref(prefs.openConnectionSearchWindowOnConnectionCreation)
|
||||
.addToggle(prefs.openConnectionSearchWindowOnConnectionCreation)
|
||||
.pref(prefs.requireDoubleClickForConnections)
|
||||
.addToggle(prefs.requireDoubleClickForConnections))
|
||||
.addTitle("localShell")
|
||||
.sub(localShellBuilder);
|
||||
.sub(new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell));
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
options.addTitle("sshConfiguration")
|
||||
.sub(new OptionsBuilder()
|
||||
.pref(prefs.useBundledTools)
|
||||
.addToggle(prefs.useBundledTools)
|
||||
.addComp(prefs.getCustomComp("x11WslInstance")));
|
||||
}
|
||||
return options.buildComp();
|
||||
|
|
|
@ -47,13 +47,9 @@ public class EditorCategory extends AppPrefsCategory {
|
|||
prefs.externalEditor, PrefsChoiceValue.getSupported(ExternalEditorType.class), false))
|
||||
.nameAndDescription("customEditorCommand")
|
||||
.addComp(new TextFieldComp(prefs.customEditorCommand, true)
|
||||
.apply(struc -> struc.get().setPromptText("myeditor $FILE")))
|
||||
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
|
||||
.addComp(terminalTest)
|
||||
.nameAndDescription("customEditorCommandInTerminal")
|
||||
.addToggle(prefs.customEditorCommandInTerminal)
|
||||
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))
|
||||
)
|
||||
.apply(struc -> struc.get().setPromptText("myeditor $FILE"))
|
||||
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM)))
|
||||
.addComp(terminalTest))
|
||||
.buildComp();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package io.xpipe.app.prefs;
|
|||
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
|
@ -37,53 +36,6 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
}
|
||||
};
|
||||
|
||||
WindowsType CURSOR_WINDOWS = new WindowsType("app.cursor", "Cursor", true) {
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
|
||||
.resolve("Programs")
|
||||
.resolve("cursor")
|
||||
.resolve("Cursor.exe"));
|
||||
}
|
||||
};
|
||||
|
||||
WindowsType WINDSURF_WINDOWS = new WindowsType("app.windsurf", "windsurf.cmd", false) {
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
|
||||
.resolve("Programs")
|
||||
.resolve("Windsurf")
|
||||
.resolve("bin")
|
||||
.resolve("windsurf.cmd"));
|
||||
}
|
||||
};
|
||||
|
||||
// Cli is broken, keep inactive
|
||||
WindowsType THEIAIDE_WINDOWS = new WindowsType("app.theiaide", "Theiaide", true) {
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
|
||||
.resolve("Programs")
|
||||
.resolve("TheiaIDE")
|
||||
.resolve("TheiaIDE.exe"));
|
||||
}
|
||||
};
|
||||
|
||||
WindowsType TRAE_WINDOWS = new WindowsType("app.trae", "trae.cmd", false) {
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
|
||||
.resolve("Programs")
|
||||
.resolve("Trae")
|
||||
.resolve("bin")
|
||||
.resolve("trae.cmd"));
|
||||
}
|
||||
};
|
||||
|
||||
WindowsType VSCODE_WINDOWS = new WindowsType("app.vscode", "code.cmd", false) {
|
||||
|
||||
@Override
|
||||
|
@ -136,8 +88,6 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
}
|
||||
};
|
||||
|
||||
LinuxPathType WINDSURF_LINUX = new LinuxPathType("app.windsurf", "windsurf");
|
||||
|
||||
LinuxPathType ZED_LINUX = new LinuxPathType("app.zed", "zed");
|
||||
|
||||
ExternalEditorType ZED_MACOS = new MacOsEditor("app.zed", "Zed");
|
||||
|
@ -160,9 +110,6 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
ExternalEditorType SUBLIME_MACOS = new MacOsEditor("app.sublime", "Sublime Text");
|
||||
ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code");
|
||||
ExternalEditorType VSCODIUM_MACOS = new MacOsEditor("app.vscodium", "VSCodium");
|
||||
ExternalEditorType CURSOR_MACOS = new MacOsEditor("app.cursor", "Cursor");
|
||||
ExternalEditorType WINDSURF_MACOS = new MacOsEditor("app.windsurf", "Windsurf");
|
||||
ExternalEditorType TRAE_MACOS = new MacOsEditor("app.trae", "Trae");
|
||||
ExternalEditorType CUSTOM = new ExternalEditorType() {
|
||||
|
||||
@Override
|
||||
|
@ -172,13 +119,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
throw ErrorEvent.expected(new IllegalStateException("No custom editor command specified"));
|
||||
}
|
||||
|
||||
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
|
||||
var command = CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString()));
|
||||
if (AppPrefs.get().customEditorCommandInTerminal().get()) {
|
||||
TerminalLauncher.openDirect(file.toString(), sc -> command.buildFull(sc), AppPrefs.get().terminalType.get());
|
||||
} else {
|
||||
ExternalApplicationHelper.startAsync(command);
|
||||
}
|
||||
var format =
|
||||
customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
|
||||
ExternalApplicationHelper.startAsync(CommandBuilder.of()
|
||||
.add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -192,11 +136,11 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
ExternalEditorType WEBSTORM = new GenericPathType("app.webstorm", "webstorm", false);
|
||||
ExternalEditorType CLION = new GenericPathType("app.clion", "clion", false);
|
||||
List<ExternalEditorType> WINDOWS_EDITORS =
|
||||
List.of(CURSOR_WINDOWS, WINDSURF_WINDOWS, TRAE_WINDOWS, VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
|
||||
List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
|
||||
List<LinuxPathType> LINUX_EDITORS =
|
||||
List.of(ExternalEditorType.WINDSURF_LINUX, VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
|
||||
List.of(VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
|
||||
List<ExternalEditorType> MACOS_EDITORS =
|
||||
List.of(CURSOR_MACOS, WINDSURF_MACOS, TRAE_MACOS, BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
|
||||
List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
|
||||
List<ExternalEditorType> CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION);
|
||||
|
||||
@SuppressWarnings("TrivialFunctionalExpressionUsage")
|
||||
|
|
|
@ -10,12 +10,7 @@ import io.xpipe.core.util.SecretValue;
|
|||
import lombok.Value;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
@ -36,7 +31,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var adaptedRdpConfig = getAdaptedConfig(configuration);
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig);
|
||||
var file = writeConfig(adaptedRdpConfig);
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of().add(executable).addFile(file.toString()));
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
|
@ -99,16 +94,16 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
|
||||
@Override
|
||||
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
|
||||
var config = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
|
||||
var config = writeConfig(configuration.getConfig());
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
.addFile(config.toString())
|
||||
.discardAllOutput());
|
||||
.discardOutput());
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
// Startup is slow
|
||||
ThreadHelper.sleep(10000);
|
||||
FileUtils.deleteQuietly(config.toFile());
|
||||
Files.delete(config);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -118,33 +113,27 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
}
|
||||
};
|
||||
|
||||
ExternalRdpClientType REMMINA = new RemminaRdpType();
|
||||
|
||||
ExternalRdpClientType X_FREE_RDP = new PathCheckType("app.xfreeRdp", "xfreerdp", true) {
|
||||
ExternalRdpClientType REMMINA = new PathCheckType("app.remmina", "remmina", true) {
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
|
||||
var b = CommandBuilder.of().addFile(file.toString()).add("/cert-ignore");
|
||||
if (configuration.getPassword() != null) {
|
||||
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
|
||||
b.add("/p:'" + escapedPw + "'");
|
||||
}
|
||||
launch(configuration.getTitle(), b);
|
||||
var file = writeConfig(configuration.getConfig());
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(
|
||||
CommandBuilder.of().add(executable).add("-c").addFile(file.toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPasswordPassing() {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
ExternalRdpClientType MICROSOFT_REMOTE_DESKTOP_MACOS_APP =
|
||||
new MacOsType("app.microsoftRemoteDesktopApp", "Microsoft Remote Desktop") {
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
|
||||
var file = writeConfig(configuration.getConfig());
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
@ -162,7 +151,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
|
||||
var file = writeConfig(configuration.getConfig());
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
@ -178,7 +167,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
|
||||
ExternalRdpClientType CUSTOM = new CustomType();
|
||||
List<ExternalRdpClientType> WINDOWS_CLIENTS = List.of(MSTSC, DEVOLUTIONS);
|
||||
List<ExternalRdpClientType> LINUX_CLIENTS = List.of(REMMINA, X_FREE_RDP);
|
||||
List<ExternalRdpClientType> LINUX_CLIENTS = List.of(REMMINA);
|
||||
List<ExternalRdpClientType> MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP, WINDOWS_APP_MACOS);
|
||||
|
||||
@SuppressWarnings("TrivialFunctionalExpressionUsage")
|
||||
|
@ -225,9 +214,9 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
|
||||
boolean supportsPasswordPassing();
|
||||
|
||||
default Path writeRdpConfigFile(String title, RdpConfig input) throws Exception {
|
||||
var name = OsType.getLocal().makeFileSystemCompatible(title);
|
||||
var file = LocalShell.getShell().getSystemTemporaryDirectory().join(name + ".rdp");
|
||||
default Path writeConfig(RdpConfig input) throws Exception {
|
||||
var file =
|
||||
LocalShell.getShell().getSystemTemporaryDirectory().join("exec-" + ScriptHelper.getScriptId() + ".rdp");
|
||||
var string = input.toString();
|
||||
Files.writeString(file.toLocalPath(), string);
|
||||
return file.toLocalPath();
|
||||
|
@ -297,7 +286,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
.add(ExternalApplicationHelper.replaceFileArgument(
|
||||
format,
|
||||
"FILE",
|
||||
writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()).toString())));
|
||||
writeConfig(configuration.getConfig()).toString())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -310,87 +299,4 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class RemminaRdpType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType {
|
||||
|
||||
public RemminaRdpType() {super("app.remmina", "remmina", true);}
|
||||
|
||||
private List<String> toStrip() {
|
||||
return List.of("auto connect", "password 51", "prompt for credentials", "smart sizing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
RdpConfig c = configuration.getConfig();
|
||||
var l = new HashSet<>(c.getContent().keySet());
|
||||
toStrip().forEach(l::remove);
|
||||
if (l.size() == 2 && l.contains("username") && l.contains("full address")) {
|
||||
var encrypted = encryptPassword(configuration.getPassword());
|
||||
if (encrypted.isPresent()) {
|
||||
var file = writeRemminaConfigFile(configuration, encrypted.get());
|
||||
launch(configuration.getTitle(), CommandBuilder.of().add("-c").addFile(file.toString()));
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
ThreadHelper.sleep(5000);
|
||||
FileUtils.deleteQuietly(file.toFile());
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), c);
|
||||
launch(configuration.getTitle(), CommandBuilder.of().add("-c").addFile(file.toString()));
|
||||
}
|
||||
|
||||
private Optional<String> encryptPassword(SecretValue password) throws Exception {
|
||||
if (password == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref").readStdoutIfPossible();
|
||||
if (prefSecretBase64.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
var paddedPassword = password.getSecretValue();
|
||||
paddedPassword = paddedPassword + "\0".repeat(8 - paddedPassword.length() % 8);
|
||||
var prefSecret = Base64.getDecoder().decode(prefSecretBase64.get());
|
||||
var key = Arrays.copyOfRange(prefSecret, 0, 24);
|
||||
var iv = Arrays.copyOfRange(prefSecret, 24, prefSecret.length);
|
||||
|
||||
var cipher = Cipher.getInstance("DESede/CBC/Nopadding");
|
||||
var keySpec = new SecretKeySpec(key, "DESede");
|
||||
var ivspec = new IvParameterSpec(iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivspec);
|
||||
byte[] encryptedText = cipher.doFinal(paddedPassword.getBytes(StandardCharsets.UTF_8));
|
||||
var base64Encrypted = Base64.getEncoder().encodeToString(encryptedText);
|
||||
return Optional.ofNullable(base64Encrypted);
|
||||
}
|
||||
}
|
||||
|
||||
private Path writeRemminaConfigFile(LaunchConfiguration configuration, String password) throws Exception {
|
||||
var name = OsType.getLocal().makeFileSystemCompatible(configuration.getTitle());
|
||||
var file = LocalShell.getShell().getSystemTemporaryDirectory().join(name + ".remmina");
|
||||
var string = """
|
||||
[remmina]
|
||||
protocol=RDP
|
||||
name=%s
|
||||
username=%s
|
||||
server=%s
|
||||
password=%s
|
||||
cert_ignore=1
|
||||
""".formatted(configuration.getTitle(),
|
||||
configuration.getConfig().get("username").orElseThrow().getValue(),
|
||||
configuration.getConfig().get("full address").orElseThrow().getValue(),
|
||||
password
|
||||
);
|
||||
Files.writeString(file.toLocalPath(), string);
|
||||
return file.toLocalPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPasswordPassing() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import javafx.beans.property.SimpleStringProperty;
|
|||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -112,13 +111,8 @@ public class IconsCategory extends AppPrefsCategory {
|
|||
return;
|
||||
}
|
||||
|
||||
var path = Path.of(dir.get());
|
||||
if (Files.isRegularFile(path)) {
|
||||
throw new IllegalArgumentException("A custom icon directory requires to be a directory of .svg files, not a single file");
|
||||
}
|
||||
|
||||
var source = SystemIconSource.Directory.builder()
|
||||
.path(path)
|
||||
.path(Path.of(dir.get()))
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
if (!sources.contains(source)) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import io.xpipe.app.comp.base.IntegratedTextAreaComp;
|
|||
import io.xpipe.app.comp.base.LabelComp;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
|
@ -116,7 +115,6 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
.minHeight(120);
|
||||
var templates = Comp.of(() -> {
|
||||
var cb = new MenuButton();
|
||||
AppFontSizes.base(cb);
|
||||
cb.textProperty().bind(BindingsHelper.flatMap(prefs.passwordManager, externalPasswordManager -> {
|
||||
return externalPasswordManager != null
|
||||
? AppI18n.observable(externalPasswordManager.getId())
|
||||
|
@ -147,7 +145,6 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
new TextFieldComp(testPasswordManagerValue)
|
||||
.apply(struc -> struc.get().setPromptText("Enter password key"))
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.prefWidth(400)
|
||||
.apply(struc -> struc.get().setOnKeyPressed(event -> {
|
||||
if (event.getCode() == KeyCode.ENTER) {
|
||||
test.run();
|
||||
|
@ -156,17 +153,14 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
})),
|
||||
new ButtonComp(null, new FontIcon("mdi2p-play"), test).styleClass(Styles.RIGHT_PILL)));
|
||||
testInput.apply(struc -> {
|
||||
struc.get().setFillHeight(true);
|
||||
var first = ((Region) struc.get().getChildren().get(0));
|
||||
var second = ((Region) struc.get().getChildren().get(1));
|
||||
second.minHeightProperty().bind(first.heightProperty());
|
||||
second.maxHeightProperty().bind(first.heightProperty());
|
||||
second.prefHeightProperty().bind(first.heightProperty());
|
||||
});
|
||||
|
||||
var testPasswordManager = new HorizontalComp(List.of(
|
||||
testInput, Comp.hspacer(25), new LabelComp(testPasswordManagerResult).apply(struc -> struc.get()
|
||||
.setOpacity(0.8))))
|
||||
.setOpacity(0.5))))
|
||||
.padding(new Insets(10, 0, 0, 0))
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
|
||||
.apply(struc -> struc.get().setFillHeight(true));
|
||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.comp.base.*;
|
|||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.storage.DataStorageSyncHandler;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
|
@ -25,7 +24,15 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
|
||||
@Override
|
||||
protected String getId() {
|
||||
return "vaultSync";
|
||||
return "sync";
|
||||
}
|
||||
|
||||
private static void showHelpAlert() {
|
||||
var md = AppI18n.get().getMarkdownDocumentation("vault");
|
||||
var markdown = new MarkdownComp(md, s -> s, true).prefWidth(600);
|
||||
var modal = ModalOverlay.of(markdown);
|
||||
modal.addButton(ModalButton.ok());
|
||||
AppDialog.show(modal);
|
||||
}
|
||||
|
||||
public Comp<?> create() {
|
||||
|
@ -54,7 +61,7 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
|
||||
var remoteRepo = new TextFieldComp(prefs.storageGitRemote).hgrow();
|
||||
var helpButton = new ButtonComp(AppI18n.observable("help"), new FontIcon("mdi2h-help-circle-outline"), () -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS_SYNC);
|
||||
showHelpAlert();
|
||||
});
|
||||
var remoteRow = new HorizontalComp(List.of(remoteRepo, helpButton)).spacing(10);
|
||||
remoteRow.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
|
||||
|
|
|
@ -15,7 +15,6 @@ import io.xpipe.app.terminal.ExternalTerminalType;
|
|||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.app.util.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
|
@ -59,10 +58,6 @@ public class TerminalCategory extends AppPrefsCategory {
|
|||
var feature = LicenseProvider.get().getFeature("logging");
|
||||
if (newValue && !feature.isSupported()) {
|
||||
try {
|
||||
// Disable it again so people don't forget that they left it on
|
||||
Platform.runLater(() -> {
|
||||
prefs.enableTerminalLogging.set(false);
|
||||
});
|
||||
feature.throwIfUnsupported();
|
||||
} catch (LicenseRequiredException ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
|
|
|
@ -54,7 +54,7 @@ public class UpdateCheckComp extends SimpleComp {
|
|||
}
|
||||
|
||||
if (updateReady.getValue()) {
|
||||
var prefix = !AppDistributionType.get().getUpdateHandler().supportsDirectInstallation()
|
||||
var prefix = AppDistributionType.get() == AppDistributionType.PORTABLE
|
||||
? AppI18n.get("updateReadyPortable")
|
||||
: AppI18n.get("updateReady");
|
||||
var version = "Version "
|
||||
|
|
|
@ -74,7 +74,7 @@ public class VaultCategory extends AppPrefsCategory {
|
|||
.hide(new SimpleBooleanProperty(uh.getUserCount() > 1))
|
||||
.nameAndDescription("syncTeamVaults")
|
||||
.addComp(new ButtonComp(AppI18n.observable("enableGitSync"), () -> AppPrefs.get()
|
||||
.selectCategory("vaultSync")))
|
||||
.selectCategory("sync")))
|
||||
.licenseRequirement("team")
|
||||
.disable(!LicenseProvider.get().getFeature("team").isSupported())
|
||||
.hide(new SimpleBooleanProperty(
|
||||
|
|
|
@ -451,7 +451,6 @@ public abstract class DataStorage {
|
|||
newChildren = l.stream()
|
||||
.filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null)
|
||||
.toList();
|
||||
e.getProvider().onChildrenRefresh(e);
|
||||
} else {
|
||||
newChildren = null;
|
||||
}
|
||||
|
@ -519,35 +518,14 @@ public abstract class DataStorage {
|
|||
.toList());
|
||||
|
||||
toUpdate.removeIf(pair -> {
|
||||
// Children classes might not be the same, the same goes for state classes
|
||||
// This can happen when there are multiple child classes and the ids got switched around
|
||||
var storeClassMatch = pair.getKey()
|
||||
.getStore()
|
||||
.getClass()
|
||||
.equals(pair.getValue().get().getStore().getClass());
|
||||
if (!storeClassMatch) {
|
||||
if (pair.getKey().getStorePersistentState() != null
|
||||
&& pair.getValue().get().getStorePersistentState() != null) {
|
||||
return pair.getKey()
|
||||
.getStorePersistentState()
|
||||
.equals(pair.getValue().get().getStorePersistentState());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
|
||||
.merge(pair.getValue().getStore().asNeeded());
|
||||
var mergedStoreChanged = pair.getKey().getStore() != merged;
|
||||
|
||||
if (pair.getKey().getStorePersistentState() == null || pair.getValue().get().getStorePersistentState() == null) {
|
||||
return !mergedStoreChanged;
|
||||
}
|
||||
|
||||
var stateClassMatch = pair.getKey()
|
||||
.getStorePersistentState()
|
||||
.getClass()
|
||||
.equals(pair.getValue().get().getStorePersistentState().getClass());
|
||||
if (!stateClassMatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var stateChange = !pair.getKey()
|
||||
.getStorePersistentState()
|
||||
.equals(pair.getValue().get().getStorePersistentState());
|
||||
return !mergedStoreChanged && !stateChange;
|
||||
});
|
||||
|
||||
if (toRemove.isEmpty() && toAdd.isEmpty() && toUpdate.isEmpty()) {
|
||||
|
@ -569,18 +547,31 @@ public abstract class DataStorage {
|
|||
}
|
||||
addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new));
|
||||
toUpdate.forEach(pair -> {
|
||||
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
|
||||
.merge(pair.getValue().getStore().asNeeded());
|
||||
if (merged != pair.getKey().getStore()) {
|
||||
pair.getKey().setStoreInternal(merged, false);
|
||||
}
|
||||
// Update state by merging
|
||||
if (pair.getKey().getStorePersistentState() != null
|
||||
&& pair.getValue().get().getStorePersistentState() != null) {
|
||||
var classMatch = pair.getKey()
|
||||
.getStorePersistentState()
|
||||
.getClass()
|
||||
.equals(pair.getValue().get().getStorePersistentState().getClass());
|
||||
// Children classes might not be the same, the same goes for state classes
|
||||
// This can happen when there are multiple child classes and the ids got switched around
|
||||
if (classMatch) {
|
||||
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
|
||||
.merge(pair.getValue().getStore().asNeeded());
|
||||
if (merged != pair.getKey().getStore()) {
|
||||
pair.getKey().setStoreInternal(merged, false);
|
||||
}
|
||||
|
||||
var s = pair.getKey().getStorePersistentState();
|
||||
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
|
||||
pair.getKey().setStorePersistentState(mergedState);
|
||||
var s = pair.getKey().getStorePersistentState();
|
||||
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState());
|
||||
pair.getKey().setStorePersistentState(mergedState);
|
||||
}
|
||||
}
|
||||
});
|
||||
refreshEntries();
|
||||
saveAsync();
|
||||
e.getProvider().onChildrenRefresh(e);
|
||||
toAdd.forEach(
|
||||
dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.get()));
|
||||
toUpdate.forEach(dataStoreEntryRef ->
|
||||
|
@ -600,6 +591,25 @@ public abstract class DataStorage {
|
|||
}
|
||||
}
|
||||
|
||||
public void deleteChildren(DataStoreEntry e) {
|
||||
var c = getDeepStoreChildren(e);
|
||||
if (c.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
c.forEach(entry -> entry.finalizeEntry());
|
||||
this.storeEntriesSet.removeAll(c);
|
||||
synchronized (identityStoreEntryMapCache) {
|
||||
identityStoreEntryMapCache.remove(e.getStore());
|
||||
}
|
||||
synchronized (storeEntryMapCache) {
|
||||
storeEntryMapCache.remove(e.getStore());
|
||||
}
|
||||
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
|
||||
refreshEntries();
|
||||
saveAsync();
|
||||
}
|
||||
|
||||
public void deleteWithChildren(DataStoreEntry... entries) {
|
||||
List<DataStoreEntry> toDelete = Arrays.stream(entries)
|
||||
.flatMap(entry -> {
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
package io.xpipe.app.storage;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.beacon.api.ConnectionQueryExchange;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class DataStorageQuery {
|
||||
|
||||
public static List<DataStoreEntry> queryUserInput(String connection) {
|
||||
var found = query("**", "**" + connection + "*", "*");
|
||||
if (found.size() > 1) {
|
||||
var narrow = found.stream().filter(dataStoreEntry -> dataStoreEntry.getName().equalsIgnoreCase(connection)).toList();
|
||||
if (narrow.size() == 1) {
|
||||
return narrow;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public static List<DataStoreEntry> query(String categoryFilter, String connectionFilter, String typeFilter) {
|
||||
if (DataStorage.get() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var catMatcher = Pattern.compile(
|
||||
toRegex("all connections/" + categoryFilter.toLowerCase()));
|
||||
var conMatcher = Pattern.compile(toRegex(connectionFilter.toLowerCase()));
|
||||
var typeMatcher = Pattern.compile(toRegex(typeFilter.toLowerCase()));
|
||||
|
||||
List<DataStoreEntry> found = new ArrayList<>();
|
||||
for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) {
|
||||
if (!storeEntry.getValidity().isUsable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = DataStorage.get().getStorePath(storeEntry).toString();
|
||||
if (!conMatcher.matcher(name).matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cat = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(storeEntry.getCategoryUuid())
|
||||
.orElse(null);
|
||||
if (cat == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var c = DataStorage.get().getStorePath(cat).toString();
|
||||
if (!catMatcher.matcher(c).matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!typeMatcher
|
||||
.matcher(storeEntry.getProvider().getId().toLowerCase())
|
||||
.matches()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
found.add(storeEntry);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private static String toRegex(String pattern) {
|
||||
pattern = pattern.replaceAll("\\*\\*", "#");
|
||||
// https://stackoverflow.com/a/17369948/6477761
|
||||
StringBuilder sb = new StringBuilder(pattern.length());
|
||||
int inGroup = 0;
|
||||
int inClass = 0;
|
||||
int firstIndexInClass = -1;
|
||||
char[] arr = pattern.toCharArray();
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
char ch = arr[i];
|
||||
switch (ch) {
|
||||
case '\\':
|
||||
if (++i >= arr.length) {
|
||||
sb.append('\\');
|
||||
} else {
|
||||
char next = arr[i];
|
||||
switch (next) {
|
||||
case ',':
|
||||
// escape not needed
|
||||
break;
|
||||
case 'Q':
|
||||
case 'E':
|
||||
// extra escape needed
|
||||
sb.append('\\');
|
||||
default:
|
||||
sb.append('\\');
|
||||
}
|
||||
sb.append(next);
|
||||
}
|
||||
break;
|
||||
case '*':
|
||||
if (inClass == 0) sb.append("[^/]*");
|
||||
else sb.append('*');
|
||||
break;
|
||||
case '#':
|
||||
if (inClass == 0) sb.append(".*");
|
||||
else sb.append('*');
|
||||
break;
|
||||
case '?':
|
||||
if (inClass == 0) sb.append('.');
|
||||
else sb.append('?');
|
||||
break;
|
||||
case '[':
|
||||
inClass++;
|
||||
firstIndexInClass = i + 1;
|
||||
sb.append('[');
|
||||
break;
|
||||
case ']':
|
||||
inClass--;
|
||||
sb.append(']');
|
||||
break;
|
||||
case '.':
|
||||
case '(':
|
||||
case ')':
|
||||
case '+':
|
||||
case '|':
|
||||
case '^':
|
||||
case '$':
|
||||
case '@':
|
||||
case '%':
|
||||
if (inClass == 0 || (firstIndexInClass == i && ch == '^')) sb.append('\\');
|
||||
sb.append(ch);
|
||||
break;
|
||||
case '!':
|
||||
if (firstIndexInClass == i) sb.append('^');
|
||||
else sb.append('!');
|
||||
break;
|
||||
case '{':
|
||||
inGroup++;
|
||||
sb.append('(');
|
||||
break;
|
||||
case '}':
|
||||
inGroup--;
|
||||
sb.append(')');
|
||||
break;
|
||||
case ',':
|
||||
if (inGroup > 0) sb.append('|');
|
||||
else sb.append(',');
|
||||
break;
|
||||
default:
|
||||
sb.append(ch);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -34,10 +34,7 @@ public class DataStoreEntryRef<T extends DataStore> {
|
|||
}
|
||||
|
||||
public void checkComplete() throws Throwable {
|
||||
var store = getStore();
|
||||
if (store != null) {
|
||||
getStore().checkComplete();
|
||||
}
|
||||
getStore().checkComplete();
|
||||
}
|
||||
|
||||
public DataStoreEntry get() {
|
||||
|
@ -45,7 +42,7 @@ public class DataStoreEntryRef<T extends DataStore> {
|
|||
}
|
||||
|
||||
public T getStore() {
|
||||
return entry.getStore() != null ? entry.getStore().asNeeded() : null;
|
||||
return entry.getStore().asNeeded();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.core.AppDistributionType;
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.prefs.ExternalApplicationType;
|
||||
|
@ -59,11 +58,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
// };
|
||||
|
||||
static ExternalTerminalType determineFallbackTerminalToOpen(ExternalTerminalType type) {
|
||||
if (type != XSHELL && type != MOBAXTERM && type != SECURECRT && type != TERMIUS && !(type instanceof WaveTerminalType)) {
|
||||
if (type == XSHELL || type == MOBAXTERM || type == SECURECRT) {
|
||||
return ProcessControlProvider.get().getEffectiveLocalDialect() == ShellDialects.CMD ? CMD : POWERSHELL;
|
||||
}
|
||||
|
||||
if (type != TERMIUS && type instanceof WaveTerminalType) {
|
||||
return type;
|
||||
}
|
||||
|
||||
// Fallback to an available default
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {
|
||||
// This should not be termius or wave as all others take precedence
|
||||
|
@ -411,69 +413,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return CommandBuilder.of().add("-c").addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
ExternalTerminalType COSMIC_TERM = new SimplePathType("app.cosmicTerm", "cosmic-term", true) {
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://github.com/pop-os/cosmic-term";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-e")
|
||||
.addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
ExternalTerminalType UXTERM = new SimplePathType("app.uxterm", "uxterm", true) {
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://invisible-island.net/xterm/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-title")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
.add("-e")
|
||||
.addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm", true) {
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
|
@ -640,6 +579,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
};
|
||||
ExternalTerminalType WARP = new WarpTerminalType();
|
||||
ExternalTerminalType CUSTOM = new CustomTerminalType();
|
||||
List<ExternalTerminalType> WINDOWS_TERMINALS = List.of(
|
||||
WindowsTerminalType.WINDOWS_TERMINAL_CANARY,
|
||||
|
@ -647,7 +587,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
WindowsTerminalType.WINDOWS_TERMINAL,
|
||||
AlacrittyTerminalType.ALACRITTY_WINDOWS,
|
||||
WezTerminalType.WEZTERM_WINDOWS,
|
||||
WarpTerminalType.WINDOWS,
|
||||
CMD,
|
||||
PWSH,
|
||||
POWERSHELL,
|
||||
|
@ -673,17 +612,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
TILIX,
|
||||
GUAKE,
|
||||
TILDA,
|
||||
COSMIC_TERM,
|
||||
UXTERM,
|
||||
XTERM,
|
||||
DEEPIN_TERMINAL,
|
||||
FOOT,
|
||||
Q_TERMINAL,
|
||||
WarpTerminalType.LINUX,
|
||||
TERMIUS,
|
||||
WaveTerminalType.WAVE_LINUX);
|
||||
List<ExternalTerminalType> MACOS_TERMINALS = List.of(
|
||||
WarpTerminalType.MACOS,
|
||||
WARP,
|
||||
ITERM2,
|
||||
KittyTerminalType.KITTY_MACOS,
|
||||
TabbyTerminalType.TABBY_MAC_OS,
|
||||
|
@ -728,10 +664,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return existing;
|
||||
}
|
||||
|
||||
if (existing == null && AppDistributionType.get() == AppDistributionType.WEBTOP) {
|
||||
return ExternalTerminalType.KONSOLE;
|
||||
}
|
||||
|
||||
var r = ALL.stream()
|
||||
.filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM))
|
||||
.filter(terminalType -> terminalType.isAvailable())
|
||||
|
|
|
@ -76,7 +76,7 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
.addFile(file.toString())
|
||||
.add("run")
|
||||
.addFile(configuration.getScriptFile())
|
||||
.discardAllOutput());
|
||||
.discardOutput());
|
||||
} else {
|
||||
// This is probably not going to work as it does not launch a bat file
|
||||
LocalShell.getShell()
|
||||
|
@ -84,7 +84,7 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
.addFile(file.toString())
|
||||
.add("run")
|
||||
.add(configuration.getDialectLaunchCommand())
|
||||
.discardAllOutput());
|
||||
.discardOutput());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.app.util.SecretManager;
|
||||
import io.xpipe.app.util.SecretQueryProgress;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.BeaconServerException;
|
||||
import io.xpipe.core.process.*;
|
||||
import io.xpipe.core.process.ProcessControl;
|
||||
import io.xpipe.core.process.TerminalInitScriptConfig;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.SequencedMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public class TerminalLauncherManager {
|
||||
|
@ -78,9 +73,6 @@ public class TerminalLauncherManager {
|
|||
synchronized (entries) {
|
||||
req = entries.get(request);
|
||||
}
|
||||
if (req == null) {
|
||||
return;
|
||||
}
|
||||
var byPid = ProcessHandle.of(pid);
|
||||
if (byPid.isEmpty()) {
|
||||
throw new BeaconClientException("Unable to find terminal child process " + pid);
|
||||
|
@ -92,44 +84,33 @@ public class TerminalLauncherManager {
|
|||
req.setPid(shell.pid());
|
||||
}
|
||||
|
||||
public static void waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
|
||||
public static Path waitExchange(UUID request) throws BeaconClientException, BeaconServerException {
|
||||
TerminalLaunchRequest req;
|
||||
synchronized (entries) {
|
||||
req = entries.get(request);
|
||||
}
|
||||
if (req == null) {
|
||||
return;
|
||||
throw new BeaconClientException("Unknown launch request " + request);
|
||||
}
|
||||
|
||||
if (req.isSetupCompleted()) {
|
||||
submitAsync(req.getRequest(), req.getProcessControl(), req.getConfig(), req.getWorkingDirectory());
|
||||
}
|
||||
try {
|
||||
req.waitForCompletion();
|
||||
return req.waitForCompletion();
|
||||
} finally {
|
||||
req.setSetupCompleted(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static Path launchExchange(UUID request) throws BeaconClientException, BeaconServerException {
|
||||
public static Path launchExchange(UUID request) throws BeaconClientException {
|
||||
synchronized (entries) {
|
||||
var e = entries.values().stream()
|
||||
.filter(entry -> entry.getRequest().equals(request))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (e == null) {
|
||||
// It seems like that some terminals might enter a restart loop to try to start an older process again
|
||||
// This would spam XPipe continuously with launch requests if we returned an error here
|
||||
// Therefore, we just return a new local shell session
|
||||
TrackEvent.withTrace("Unknown launch request").tag("request", request.toString()).handle();
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var defaultShell = ProcessControlProvider.get().getEffectiveLocalDialect();
|
||||
var shellExec = defaultShell.getExecutableName();
|
||||
var script = ScriptHelper.createExecScript(sc, shellExec);
|
||||
return Path.of(script.toString());
|
||||
} catch (Exception ex) {
|
||||
throw new BeaconServerException(ex);
|
||||
}
|
||||
throw new BeaconClientException("Unknown launch request " + request);
|
||||
}
|
||||
|
||||
if (!(e.getResult() instanceof TerminalLaunchResult.ResultSuccess)) {
|
||||
|
@ -139,41 +120,4 @@ public class TerminalLauncherManager {
|
|||
return ((TerminalLaunchResult.ResultSuccess) e.getResult()).getTargetScript();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static List<String> externalExchange(DataStoreEntryRef<ShellStore> ref, List<String> arguments) throws BeaconClientException, BeaconServerException {
|
||||
var request = UUID.randomUUID();
|
||||
ShellControl session;
|
||||
try {
|
||||
session = ref.getStore().getOrStartSession();
|
||||
} catch (Exception e) {
|
||||
throw new BeaconServerException(e);
|
||||
}
|
||||
|
||||
ProcessControl control;
|
||||
if (arguments.size() > 0) {
|
||||
control = session.command(CommandBuilder.of().addAll(arguments));
|
||||
} else {
|
||||
control = session;
|
||||
}
|
||||
|
||||
var config = new TerminalInitScriptConfig(ref.get().getName(), false, TerminalInitFunction.none());
|
||||
submitAsync(request, control, config, null);
|
||||
waitExchange(request);
|
||||
var script = launchExchange(request);
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var runCommand = ProcessControlProvider.get().getEffectiveLocalDialect().getOpenScriptCommand(script.toString()).buildBaseParts(sc);
|
||||
var cleaned = runCommand.stream().map(s -> {
|
||||
if (s.startsWith("\"") && s.endsWith("\"")) {
|
||||
s = s.substring(1, s.length() - 1);
|
||||
} else if (s.startsWith("'") && s.endsWith("'")) {
|
||||
s = s.substring(1, s.length() - 1);
|
||||
}
|
||||
return s;
|
||||
}).toList();
|
||||
return cleaned;
|
||||
} catch (Exception e) {
|
||||
throw new BeaconServerException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,140 +1,57 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.prefs.ExternalApplicationHelper;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import io.xpipe.core.process.TerminalInitFunction;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
public class WarpTerminalType extends ExternalTerminalType.MacOsType {
|
||||
|
||||
public interface WarpTerminalType extends ExternalTerminalType, TrackableTerminalType {
|
||||
|
||||
static WarpTerminalType WINDOWS = new Windows();
|
||||
static WarpTerminalType LINUX = new Linux();
|
||||
static WarpTerminalType MACOS = new MacOs();
|
||||
|
||||
class Windows implements WarpTerminalType {
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
if (!configuration.isPreferTabs()) {
|
||||
DesktopHelper.openUrl("warp://action/new_window?path=" + configuration.getScriptFile());
|
||||
} else {
|
||||
DesktopHelper.openUrl("warp://action/new_tab?path=" + configuration.getScriptFile());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return WindowsRegistry.local().keyExists(WindowsRegistry.HKEY_CURRENT_USER, "Software\\Classes\\warp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "app.warp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
// Warp always opens the new separate window, so we don't want to use it in the file browser for docking
|
||||
// Just say that we don't support new windows, that way it doesn't dock
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Linux implements WarpTerminalType {
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
if (!configuration.isPreferTabs()) {
|
||||
DesktopHelper.openUrl("warp://action/new_window?path=" + configuration.getScriptFile());
|
||||
} else {
|
||||
DesktopHelper.openUrl("warp://action/new_tab?path=" + configuration.getScriptFile());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return Files.exists(Path.of("/opt/warpdotdev"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "app.warp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
// Warp always opens the new separate window, so we don't want to use it in the file browser for docking
|
||||
// Just say that we don't support new windows, that way it doesn't dock
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
}
|
||||
|
||||
class MacOs extends MacOsType implements WarpTerminalType {
|
||||
|
||||
public MacOs() {
|
||||
super("app.warp", "Warp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
public WarpTerminalType() {
|
||||
super("app.warp", "Warp");
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getWebsite() {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.warp.dev/";
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean isRecommended() {
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean useColoredTitle() {
|
||||
public boolean useColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean shouldClear() {
|
||||
public boolean shouldClear() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
default TerminalInitFunction additionalInitCommands() {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalInitFunction additionalInitCommands() {
|
||||
return TerminalInitFunction.of(sc -> {
|
||||
if (sc.getShellDialect() == ShellDialects.ZSH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"zsh\"}}\\x9c'";
|
||||
|
|
|
@ -20,11 +20,6 @@ public class GitHubUpdater extends UpdateHandler {
|
|||
super(startBackgroundThread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsDirectInstallation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModalButton> createActions() {
|
||||
var list = new ArrayList<ModalButton>();
|
||||
|
|
|
@ -17,11 +17,6 @@ public class PortableUpdater extends UpdateHandler {
|
|||
super(thread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsDirectInstallation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModalButton> createActions() {
|
||||
var list = new ArrayList<ModalButton>();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue