mirror of
https://github.com/xpipe-io/xpipe.git
synced 2025-04-17 09:43:37 +00:00
Rework
This commit is contained in:
parent
03d222e5f5
commit
88a3d63885
239 changed files with 3748 additions and 3422 deletions
|
@ -49,13 +49,9 @@ dependencies {
|
|||
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
|
||||
|
||||
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'
|
||||
api 'info.picocli:picocli:4.7.6'
|
||||
api ('org.kohsuke:github-api:1.326') {
|
||||
exclude group: 'org.apache.commons', module: 'commons-lang3'
|
||||
}
|
||||
api 'org.apache.commons:commons-lang3:3.17.0'
|
||||
api 'io.sentry:sentry:7.20.0'
|
||||
api 'commons-io:commons-io:2.18.0'
|
||||
|
|
|
@ -112,7 +112,8 @@ public class AppBeaconServer {
|
|||
executor.shutdown();
|
||||
try {
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ignored) {}
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void initAuthSecret() throws IOException {
|
||||
|
|
|
@ -2,15 +2,10 @@ 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;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -22,14 +22,14 @@ public class FsReadExchangeImpl extends FsReadExchange {
|
|||
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
|
||||
var fs = new ConnectionFileSystem(shell.getControl());
|
||||
|
||||
if (!fs.fileExists(msg.getPath().toString())) {
|
||||
if (!fs.fileExists(msg.getPath())) {
|
||||
throw new BeaconClientException("File does not exist");
|
||||
}
|
||||
|
||||
var size = fs.getFileSize(msg.getPath().toString());
|
||||
var size = fs.getFileSize(msg.getPath());
|
||||
if (size > 100_000_000) {
|
||||
var file = BlobManager.get().newBlobFile();
|
||||
try (var in = fs.openInput(msg.getPath().toString())) {
|
||||
try (var in = fs.openInput(msg.getPath())) {
|
||||
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
|
||||
try (var fileOut =
|
||||
Files.newOutputStream(file.resolve(msg.getPath().getFileName()))) {
|
||||
|
@ -45,7 +45,7 @@ public class FsReadExchangeImpl extends FsReadExchange {
|
|||
}
|
||||
} else {
|
||||
byte[] bytes;
|
||||
try (var in = fs.openInput(msg.getPath().toString())) {
|
||||
try (var in = fs.openInput(msg.getPath())) {
|
||||
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
|
||||
bytes = fixedIn.readAllBytes();
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
|
|
|
@ -21,9 +21,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange {
|
|||
data = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
data = shell.getControl().getShellDialect().prepareScriptContent(data);
|
||||
var file = ScriptHelper.getExecScriptFile(shell.getControl());
|
||||
shell.getControl().view().writeScriptFile(file, data);
|
||||
file = ScriptHelper.fixScriptPermissions(shell.getControl(), file);
|
||||
var file = ScriptHelper.createExecScript(shell.getControl(), data);
|
||||
return Response.builder().path(file).build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ public class FsWriteExchangeImpl extends FsWriteExchange {
|
|||
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
|
||||
var fs = new ConnectionFileSystem(shell.getControl());
|
||||
try (var in = BlobManager.get().getBlob(msg.getBlob());
|
||||
var os = fs.openOutput(msg.getPath().toString(), in.available())) {
|
||||
var os = fs.openOutput(msg.getPath(), in.available())) {
|
||||
in.transferTo(os);
|
||||
}
|
||||
return Response.builder().build();
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
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;
|
||||
|
@ -14,6 +11,8 @@ import io.xpipe.beacon.BeaconClientException;
|
|||
import io.xpipe.beacon.BeaconServerException;
|
||||
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
|
||||
|
@ -26,13 +25,15 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx
|
|||
}
|
||||
|
||||
if (found.size() > 1) {
|
||||
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
|
||||
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");
|
||||
throw new BeaconClientException(
|
||||
"Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
|
||||
}
|
||||
|
||||
if (!checkPermission()) {
|
||||
|
|
|
@ -177,7 +177,7 @@ public class BrowserFileChooserSessionComp extends DialogComp {
|
|||
.setAll(c.getList().stream()
|
||||
.map(s -> {
|
||||
var field = new TextField(
|
||||
s.getRawFileEntry().getPath());
|
||||
s.getRawFileEntry().getPath().toString());
|
||||
field.setEditable(false);
|
||||
field.getStyleClass().add("chooser-selection");
|
||||
HBox.setHgrow(field, Priority.ALWAYS);
|
||||
|
|
|
@ -7,7 +7,7 @@ import io.xpipe.app.util.BooleanScope;
|
|||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.FileReference;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
|
@ -78,7 +78,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
|||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy) {
|
||||
if (store == null) {
|
||||
return;
|
||||
|
@ -96,7 +96,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
|||
sessionEntries.add(model);
|
||||
}
|
||||
if (path != null) {
|
||||
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
|
||||
model.initWithGivenDirectory(path.apply(model).toDirectory());
|
||||
} else {
|
||||
model.initWithDefaultDirectory();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import io.xpipe.app.storage.DataStorage;
|
|||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
|
@ -199,7 +199,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
|||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy) {
|
||||
if (store == null) {
|
||||
return;
|
||||
|
@ -212,7 +212,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
|||
|
||||
public BrowserFileSystemTabModel openFileSystemSync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy,
|
||||
boolean select)
|
||||
throws Exception {
|
||||
|
@ -232,7 +232,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
|||
}
|
||||
}
|
||||
if (path != null) {
|
||||
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
|
||||
model.initWithGivenDirectory(path.apply(model).toDirectory());
|
||||
} else {
|
||||
model.initWithDefaultDirectory();
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
public class BrowserAlerts {
|
||||
|
||||
public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) {
|
||||
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
|
||||
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
|
||||
map.put(new ButtonType(AppI18n.get("cancel"), ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
|
||||
if (multiple) {
|
||||
|
@ -96,7 +96,7 @@ public class BrowserAlerts {
|
|||
var names = namesHeader + "\n"
|
||||
+ source.stream()
|
||||
.limit(10)
|
||||
.map(entry -> "- " + new FilePath(entry.getPath()).getFileName())
|
||||
.map(entry -> "- " + entry.getPath().getFileName())
|
||||
.collect(Collectors.joining("\n"));
|
||||
if (source.size() > 10) {
|
||||
names += "\n+ " + (source.size() - 10) + " ...";
|
||||
|
|
|
@ -64,9 +64,9 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
|||
});
|
||||
}
|
||||
|
||||
var elements = FileNames.splitHierarchy(val);
|
||||
var elements = val.splitHierarchy();
|
||||
var modifiedElements = new ArrayList<>(elements);
|
||||
if (val.startsWith("/")) {
|
||||
if (val.toString().startsWith("/")) {
|
||||
modifiedElements.addFirst("/");
|
||||
}
|
||||
Breadcrumbs.BreadCrumbItem<String> items =
|
||||
|
|
|
@ -4,7 +4,6 @@ import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
|||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
@ -74,6 +73,6 @@ public class BrowserEntry {
|
|||
}
|
||||
|
||||
public String getFileName() {
|
||||
return FileNames.getFileName(getRawFileEntry().getPath());
|
||||
return getRawFileEntry().getPath().getFileName();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import io.xpipe.core.process.OsType;
|
|||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileInfo;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
@ -63,8 +62,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
filenameCol.textProperty().bind(AppI18n.observable("name"));
|
||||
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
|
||||
param.getValue() != null
|
||||
? FileNames.getFileName(
|
||||
param.getValue().getRawFileEntry().getPath())
|
||||
? param.getValue().getRawFileEntry().getPath().getFileName()
|
||||
: null));
|
||||
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
|
||||
filenameCol.setSortType(ASCENDING);
|
||||
|
|
|
@ -294,7 +294,8 @@ public class BrowserFileListCompEntry {
|
|||
return;
|
||||
}
|
||||
|
||||
model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath());
|
||||
model.getFileSystemModel()
|
||||
.cdAsync(item.getRawFileEntry().getPath().toString());
|
||||
}
|
||||
};
|
||||
DROP_TIMER.schedule(activeTask, 1200);
|
||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.prefs.AppPrefs;
|
|||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -66,8 +65,9 @@ public final class BrowserFileListModel {
|
|||
List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
|
||||
? all.getValue().stream()
|
||||
.filter(entry -> {
|
||||
var name = FileNames.getFileName(
|
||||
entry.getRawFileEntry().getPath())
|
||||
var name = entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.getFileName()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
var filterString =
|
||||
fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
|
||||
|
@ -99,8 +99,8 @@ public final class BrowserFileListModel {
|
|||
return old;
|
||||
}
|
||||
|
||||
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName());
|
||||
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
|
||||
var fullPath = fileSystemModel.getCurrentPath().get().join(old.getFileName());
|
||||
var newFullPath = fileSystemModel.getCurrentPath().get().join(newName);
|
||||
|
||||
// This check will fail on case-insensitive file systems when changing the case of the file
|
||||
// So skip it in this case
|
||||
|
@ -144,7 +144,7 @@ public final class BrowserFileListModel {
|
|||
public void onDoubleClick(BrowserEntry entry) {
|
||||
var r = entry.getRawFileEntry().resolved();
|
||||
if (r.getKind() == FileKind.DIRECTORY) {
|
||||
fileSystemModel.cdAsync(r.getPath());
|
||||
fileSystemModel.cdAsync(r.getPath().toString());
|
||||
}
|
||||
|
||||
if (AppPrefs.get().editFilesWithDoubleClick().get() && r.getKind() == FileKind.FILE) {
|
||||
|
|
|
@ -10,7 +10,6 @@ import io.xpipe.core.process.ElevationFunction;
|
|||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileInfo;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -72,7 +71,7 @@ public class BrowserFileOpener {
|
|||
var key = calculateKey(entry);
|
||||
FileBridge.get()
|
||||
.openIO(
|
||||
FileNames.getFileName(file),
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
|
@ -93,7 +92,7 @@ public class BrowserFileOpener {
|
|||
var key = calculateKey(entry);
|
||||
FileBridge.get()
|
||||
.openIO(
|
||||
FileNames.getFileName(file),
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
|
@ -119,7 +118,7 @@ public class BrowserFileOpener {
|
|||
var key = calculateKey(entry);
|
||||
FileBridge.get()
|
||||
.openIO(
|
||||
FileNames.getFileName(file),
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
|
|
|
@ -35,10 +35,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
|
|||
var graphic = new HorizontalComp(List.of(
|
||||
icon,
|
||||
new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList()), model)));
|
||||
var l = new Button(entry.getPath(), graphic.createRegion());
|
||||
var l = new Button(entry.getPath().toString(), graphic.createRegion());
|
||||
l.setGraphicTextGap(1);
|
||||
l.setOnAction(event -> {
|
||||
model.cdAsync(entry.getPath());
|
||||
model.cdAsync(entry.getPath().toString());
|
||||
event.consume();
|
||||
});
|
||||
l.setAlignment(Pos.CENTER_LEFT);
|
||||
|
|
|
@ -2,10 +2,7 @@ package io.xpipe.app.browser.file;
|
|||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import io.xpipe.core.store.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
@ -67,7 +64,7 @@ public class BrowserFileSystemHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static String resolveDirectoryPath(BrowserFileSystemTabModel model, String path, boolean allowRewrite)
|
||||
public static FilePath resolveDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean allowRewrite)
|
||||
throws Exception {
|
||||
if (path == null) {
|
||||
return null;
|
||||
|
@ -82,23 +79,23 @@ public class BrowserFileSystemHelper {
|
|||
return path;
|
||||
}
|
||||
|
||||
var resolved = shell.get()
|
||||
var resolved = FilePath.of(shell.get()
|
||||
.getShellDialect()
|
||||
.resolveDirectory(shell.get(), path)
|
||||
.readStdoutOrThrow();
|
||||
.resolveDirectory(shell.get(), path.toString())
|
||||
.readStdoutOrThrow());
|
||||
|
||||
if (!FileNames.isAbsolute(resolved)) {
|
||||
if (!resolved.isAbsolute()) {
|
||||
throw new IllegalArgumentException(String.format("Directory %s is not absolute", resolved));
|
||||
}
|
||||
|
||||
if (allowRewrite && model.getFileSystem().fileExists(path)) {
|
||||
return FileNames.toDirectory(FileNames.getParent(path));
|
||||
if (allowRewrite && model.getFileSystem().fileExists(resolved)) {
|
||||
return resolved.getParent().toDirectory();
|
||||
}
|
||||
|
||||
return FileNames.toDirectory(resolved);
|
||||
return resolved.toDirectory();
|
||||
}
|
||||
|
||||
public static void validateDirectoryPath(BrowserFileSystemTabModel model, String path, boolean verifyExists)
|
||||
public static void validateDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean verifyExists)
|
||||
throws Exception {
|
||||
if (path == null) {
|
||||
return;
|
||||
|
@ -125,7 +122,7 @@ public class BrowserFileSystemHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
|
||||
public static FileEntry getRemoteWrapper(FileSystem fileSystem, FilePath file) throws Exception {
|
||||
return new FileEntry(
|
||||
fileSystem,
|
||||
file,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
|
@ -12,33 +14,33 @@ import java.util.Objects;
|
|||
public final class BrowserFileSystemHistory {
|
||||
|
||||
private final IntegerProperty cursor = new SimpleIntegerProperty(-1);
|
||||
private final List<String> history = new ArrayList<>();
|
||||
private final List<FilePath> history = new ArrayList<>();
|
||||
private final BooleanBinding canGoBack =
|
||||
Bindings.createBooleanBinding(() -> cursor.get() > 0 && history.size() > 1, cursor);
|
||||
private final BooleanBinding canGoForth =
|
||||
Bindings.createBooleanBinding(() -> cursor.get() < history.size() - 1, cursor);
|
||||
|
||||
public List<String> getForwardHistory(int max) {
|
||||
var l = new ArrayList<String>();
|
||||
public List<FilePath> getForwardHistory(int max) {
|
||||
var l = new ArrayList<FilePath>();
|
||||
for (var i = cursor.get() + 1; i < Math.min(history.size(), cursor.get() + max); i++) {
|
||||
l.add(history.get(i));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
public List<String> getBackwardHistory(int max) {
|
||||
var l = new ArrayList<String>();
|
||||
public List<FilePath> getBackwardHistory(int max) {
|
||||
var l = new ArrayList<FilePath>();
|
||||
for (var i = cursor.get() - 1; i >= Math.max(0, cursor.get() - max); i--) {
|
||||
l.add(history.get(i));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
public String getCurrent() {
|
||||
public FilePath getCurrent() {
|
||||
return history.size() > 0 ? history.get(cursor.get()) : null;
|
||||
}
|
||||
|
||||
public void updateCurrent(String s) {
|
||||
public void updateCurrent(FilePath s) {
|
||||
var lastString = getCurrent();
|
||||
if (cursor.get() != -1 && Objects.equals(lastString, s)) {
|
||||
return;
|
||||
|
@ -52,11 +54,11 @@ public final class BrowserFileSystemHistory {
|
|||
cursor.set(history.size() - 1);
|
||||
}
|
||||
|
||||
public String back() {
|
||||
public FilePath back() {
|
||||
return back(1);
|
||||
}
|
||||
|
||||
public String back(int i) {
|
||||
public FilePath back(int i) {
|
||||
if (!canGoBack.get()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -64,7 +66,7 @@ public final class BrowserFileSystemHistory {
|
|||
return history.get(cursor.get());
|
||||
}
|
||||
|
||||
public String forth(int i) {
|
||||
public FilePath forth(int i) {
|
||||
if (!canGoForth.get()) {
|
||||
return history.getLast();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
@ -41,12 +41,12 @@ public class BrowserFileSystemSavedState {
|
|||
@Setter
|
||||
private BrowserFileSystemTabModel model;
|
||||
|
||||
private String lastDirectory;
|
||||
private FilePath lastDirectory;
|
||||
|
||||
@NonNull
|
||||
private ObservableList<RecentEntry> recentDirectories;
|
||||
|
||||
public BrowserFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
|
||||
public BrowserFileSystemSavedState(FilePath lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
|
||||
this.lastDirectory = lastDirectory;
|
||||
this.recentDirectories = recentDirectories;
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ public class BrowserFileSystemSavedState {
|
|||
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
|
||||
}
|
||||
|
||||
public void cd(String dir, boolean delay) {
|
||||
public void cd(FilePath dir, boolean delay) {
|
||||
if (dir == null) {
|
||||
lastDirectory = null;
|
||||
return;
|
||||
|
@ -107,9 +107,9 @@ public class BrowserFileSystemSavedState {
|
|||
}
|
||||
}
|
||||
|
||||
private synchronized void updateRecent(String dir) {
|
||||
var without = FileNames.removeTrailingSlash(dir);
|
||||
var with = FileNames.toDirectory(dir);
|
||||
private synchronized void updateRecent(FilePath dir) {
|
||||
var without = dir.removeTrailingSlash();
|
||||
var with = dir.toDirectory();
|
||||
recentDirectories.removeIf(recentEntry ->
|
||||
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
|
||||
|
||||
|
@ -161,7 +161,7 @@ public class BrowserFileSystemSavedState {
|
|||
recentDirectories = List.of();
|
||||
}
|
||||
var cleaned = recentDirectories.stream()
|
||||
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
|
||||
.map(recentEntry -> new RecentEntry(recentEntry.directory.toDirectory(), recentEntry.time))
|
||||
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
|
||||
|
@ -173,7 +173,7 @@ public class BrowserFileSystemSavedState {
|
|||
@Builder
|
||||
public static class RecentEntry {
|
||||
|
||||
String directory;
|
||||
FilePath directory;
|
||||
Instant time;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import io.xpipe.app.comp.base.*;
|
|||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
|
@ -49,7 +50,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
|||
private Region createContent() {
|
||||
var root = new VBox();
|
||||
var overview = new Button(null, new FontIcon("mdi2m-monitor"));
|
||||
overview.setOnAction(e -> model.cdAsync(null));
|
||||
overview.setOnAction(e -> model.cdAsync((FilePath) null));
|
||||
new TooltipAugment<>("overview", new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN))
|
||||
.augment(overview);
|
||||
overview.disableProperty().bind(model.getInOverview());
|
||||
|
@ -158,14 +159,14 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
|||
root, new KeyCodeCombination(KeyCode.UP, KeyCombination.ALT_DOWN), true, keyEvent -> {
|
||||
var p = model.getCurrentParentDirectory();
|
||||
if (p != null) {
|
||||
model.cdAsync(p.getPath());
|
||||
model.cdAsync(p.getPath().toString());
|
||||
}
|
||||
keyEvent.consume();
|
||||
});
|
||||
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.BACK_SPACE), false, keyEvent -> {
|
||||
var p = model.getCurrentParentDirectory();
|
||||
if (p != null) {
|
||||
model.cdAsync(p.getPath());
|
||||
model.cdAsync(p.getPath().toString());
|
||||
}
|
||||
keyEvent.consume();
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
|
||||
private final Property<String> filter = new SimpleStringProperty();
|
||||
private final BrowserFileListModel fileList;
|
||||
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
|
||||
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
||||
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
|
||||
|
@ -193,7 +193,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
return null;
|
||||
}
|
||||
|
||||
var parent = FileNames.getParent(currentPath.get());
|
||||
var parent = currentPath.get().getParent();
|
||||
if (parent == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -213,6 +213,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
return new FileEntry(fileSystem, currentPath.get(), null, 0, null, FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
public void cdAsync(FilePath path) {
|
||||
cdAsync(path != null ? path.toString() : null);
|
||||
}
|
||||
|
||||
public void cdAsync(String path) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
|
@ -260,7 +264,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
}
|
||||
|
||||
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
|
||||
if (Objects.equals(path, currentPath.get())) {
|
||||
var cps = currentPath.get() != null ? currentPath.get().toString() : null;
|
||||
if (Objects.equals(path, cps)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
@ -273,7 +278,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
startIfNeeded();
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
// Fix common issues with paths
|
||||
|
@ -288,12 +293,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
if (evaluatedPath == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Handle commands typed into navigation bar
|
||||
if (customInput
|
||||
&& evaluatedPath != null
|
||||
&& !evaluatedPath.isBlank()
|
||||
&& !FileNames.isAbsolute(evaluatedPath)
|
||||
&& fileSystem.getShell().isPresent()) {
|
||||
|
@ -324,34 +332,34 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
openTerminalAsync(name, directory, cc, true);
|
||||
}
|
||||
});
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
// Evaluate optional links
|
||||
String resolvedPath;
|
||||
FilePath resolvedPath;
|
||||
try {
|
||||
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, evaluatedPath, customInput);
|
||||
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, FilePath.of(evaluatedPath), customInput);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
if (!Objects.equals(path, resolvedPath)) {
|
||||
return Optional.ofNullable(resolvedPath);
|
||||
if (!Objects.equals(path, resolvedPath.toString())) {
|
||||
return Optional.ofNullable(resolvedPath.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
|
||||
cdSyncWithoutCheck(path);
|
||||
cdSyncWithoutCheck(resolvedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private void cdSyncWithoutCheck(String path) throws Exception {
|
||||
private void cdSyncWithoutCheck(FilePath path) throws Exception {
|
||||
if (fileSystem == null) {
|
||||
var fs = entry.getStore().createFileSystem();
|
||||
fs.open();
|
||||
|
@ -368,7 +376,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
loadFilesSync(path);
|
||||
}
|
||||
|
||||
public void withFiles(String dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
|
||||
public void withFiles(FilePath dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
if (dir != null) {
|
||||
startIfNeeded();
|
||||
|
@ -385,7 +393,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
});
|
||||
}
|
||||
|
||||
private boolean loadFilesSync(String dir) {
|
||||
private boolean loadFilesSync(FilePath dir) {
|
||||
try {
|
||||
startIfNeeded();
|
||||
var fs = getFileSystem();
|
||||
|
@ -456,7 +464,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
}
|
||||
|
||||
startIfNeeded();
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
|
||||
var abs = getCurrentDirectory().getPath().join(name);
|
||||
if (fileSystem.directoryExists(abs)) {
|
||||
throw ErrorEvent.expected(
|
||||
new IllegalStateException(String.format("Directory %s already exists", abs)));
|
||||
|
@ -468,8 +476,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
});
|
||||
}
|
||||
|
||||
public void createLinkAsync(String linkName, String targetFile) {
|
||||
if (linkName == null || linkName.isBlank() || targetFile == null || targetFile.isBlank()) {
|
||||
public void createLinkAsync(String linkName, FilePath targetFile) {
|
||||
if (linkName == null || linkName.isBlank() || targetFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -484,7 +492,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
}
|
||||
|
||||
startIfNeeded();
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), linkName);
|
||||
var abs = getCurrentDirectory().getPath().join(linkName);
|
||||
fileSystem.symbolicLink(abs, targetFile);
|
||||
refreshSync();
|
||||
});
|
||||
|
@ -549,7 +557,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
return;
|
||||
}
|
||||
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
|
||||
var abs = getCurrentDirectory().getPath().join(name);
|
||||
fileSystem.touch(abs);
|
||||
refreshSync();
|
||||
});
|
||||
|
@ -560,8 +568,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
return fileSystem == null;
|
||||
}
|
||||
|
||||
public void initWithGivenDirectory(String dir) {
|
||||
cdSync(dir);
|
||||
public void initWithGivenDirectory(FilePath dir) {
|
||||
cdSync(dir != null ? dir.toString() : null);
|
||||
}
|
||||
|
||||
public void initWithDefaultDirectory() {
|
||||
|
@ -570,7 +578,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
}
|
||||
|
||||
public void openTerminalAsync(
|
||||
String name, String directory, ProcessControl processControl, boolean dockIfPossible) {
|
||||
String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
if (fileSystem == null) {
|
||||
return;
|
||||
|
@ -597,11 +605,17 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
}
|
||||
|
||||
public void backSync(int i) {
|
||||
cdSync(history.back(i));
|
||||
var b = history.back(i);
|
||||
if (b != null) {
|
||||
cdSync(b.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void forthSync(int i) {
|
||||
cdSync(history.forth(i));
|
||||
var f = history.forth(i);
|
||||
if (f != null) {
|
||||
cdSync(f.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
|
|
@ -12,7 +12,6 @@ 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;
|
||||
|
@ -73,7 +72,7 @@ public class BrowserFileTransferOperation {
|
|||
this.progress.accept(progress);
|
||||
}
|
||||
|
||||
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, String target, boolean multiple)
|
||||
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, FilePath target, boolean multiple)
|
||||
throws Exception {
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
return BrowserAlerts.FileConflictChoice.CANCEL;
|
||||
|
@ -177,7 +176,7 @@ public class BrowserFileTransferOperation {
|
|||
}
|
||||
|
||||
var sourceFile = source.getPath();
|
||||
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
|
||||
var targetFile = target.getPath().join(sourceFile.getFileName());
|
||||
|
||||
if (sourceFile.equals(targetFile)) {
|
||||
// Duplicate file by renaming it
|
||||
|
@ -209,7 +208,7 @@ public class BrowserFileTransferOperation {
|
|||
}
|
||||
}
|
||||
|
||||
private String renameFileLoop(FileSystem fileSystem, String target, boolean dir) throws Exception {
|
||||
private FilePath renameFileLoop(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
|
||||
// Who has more than 10 copies?
|
||||
for (int i = 0; i < 10; i++) {
|
||||
target = renameFile(target);
|
||||
|
@ -220,23 +219,21 @@ public class BrowserFileTransferOperation {
|
|||
return target;
|
||||
}
|
||||
|
||||
private String renameFile(String target) {
|
||||
var targetFile = new FilePath(target);
|
||||
var name = targetFile.getFileName();
|
||||
private FilePath renameFile(FilePath target) {
|
||||
var name = target.getFileName();
|
||||
var pattern = Pattern.compile("(.+) \\((\\d+)\\)\\.(.+?)");
|
||||
var matcher = pattern.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
try {
|
||||
var number = Integer.parseInt(matcher.group(2));
|
||||
var newFile =
|
||||
targetFile.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
|
||||
return newFile.toString();
|
||||
var newFile = target.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
|
||||
return newFile;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
var noExt = targetFile.getFileName().equals(targetFile.getExtension());
|
||||
return targetFile.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + targetFile.getExtension());
|
||||
var noExt = target.getFileName().equals(target.getExtension());
|
||||
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + target.getExtension()));
|
||||
}
|
||||
|
||||
private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
|
||||
|
@ -248,7 +245,7 @@ public class BrowserFileTransferOperation {
|
|||
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getFileSystem().equals(target.getFileSystem())
|
||||
&& FileNames.startsWith(source.getPath(), target.getPath())) {
|
||||
&& source.getPath().startsWith(target.getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -260,17 +257,17 @@ public class BrowserFileTransferOperation {
|
|||
return;
|
||||
}
|
||||
|
||||
var directoryName = FileNames.getFileName(source.getPath());
|
||||
var directoryName = source.getPath().getFileName();
|
||||
flatFiles.put(source, directoryName);
|
||||
|
||||
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
|
||||
var baseRelative = source.getPath().getParent().toDirectory();
|
||||
List<FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
|
||||
for (FileEntry fileEntry : list) {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
|
||||
var rel = baseRelative.relativize(fileEntry.getPath()).toUnix().toString();
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
|
@ -284,7 +281,7 @@ public class BrowserFileTransferOperation {
|
|||
return;
|
||||
}
|
||||
|
||||
flatFiles.put(source, FileNames.getFileName(source.getPath()));
|
||||
flatFiles.put(source, source.getPath().getFileName());
|
||||
// Recalculate as it could have been changed meanwhile
|
||||
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
|
||||
}
|
||||
|
@ -297,10 +294,10 @@ public class BrowserFileTransferOperation {
|
|||
}
|
||||
|
||||
var sourceFile = e.getKey();
|
||||
var fixedRelPath = new FilePath(e.getValue())
|
||||
var fixedRelPath = FilePath.of(e.getValue())
|
||||
.fileSystemCompatible(
|
||||
target.getFileSystem().getShell().orElseThrow().getOsType());
|
||||
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
|
||||
var targetFile = target.getPath().join(fixedRelPath.toString());
|
||||
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
@ -328,7 +325,7 @@ public class BrowserFileTransferOperation {
|
|||
}
|
||||
|
||||
private void transfer(
|
||||
FileEntry sourceFile, String targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
|
||||
FileEntry sourceFile, FilePath targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
|
||||
throws Exception {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
|
@ -437,7 +434,8 @@ public class BrowserFileTransferOperation {
|
|||
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
updateProgress(
|
||||
new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
exception.set(ex);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
@ -24,6 +26,6 @@ public interface BrowserHistorySavedState {
|
|||
class Entry {
|
||||
|
||||
UUID uuid;
|
||||
String path;
|
||||
FilePath path;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
|||
.grow(true, false)
|
||||
.accessibleTextKey("restoreAllSessions");
|
||||
|
||||
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.separator(), tile));
|
||||
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.hseparator(), tile));
|
||||
layout.styleClass("welcome");
|
||||
layout.spacing(14);
|
||||
layout.maxWidth(1000);
|
||||
|
@ -154,7 +154,9 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
|||
var name = Bindings.createStringBinding(
|
||||
() -> {
|
||||
var n = e.getPath();
|
||||
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
|
||||
return AppPrefs.get().censorMode().get()
|
||||
? "*".repeat(n.toString().length())
|
||||
: n.toString();
|
||||
},
|
||||
AppPrefs.get().censorMode());
|
||||
return new ButtonComp(name, () -> {
|
||||
|
@ -162,7 +164,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
|||
model.restoreStateAsync(e, disable);
|
||||
});
|
||||
})
|
||||
.accessibleText(e.getPath())
|
||||
.accessibleText(e.getPath().toString())
|
||||
.disable(disable)
|
||||
.styleClass("directory-button")
|
||||
.apply(struc -> struc.get().setMaxWidth(20000))
|
||||
|
|
|
@ -3,6 +3,7 @@ package io.xpipe.app.browser.file;
|
|||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import java.nio.file.Files;
|
||||
|
@ -33,7 +34,7 @@ public class BrowserLocalFileSystem {
|
|||
|
||||
return new FileEntry(
|
||||
localFileSystem.open(),
|
||||
file.toString(),
|
||||
FilePath.of(file),
|
||||
Files.getLastModifiedTime(file).toInstant(),
|
||||
Files.size(file),
|
||||
null,
|
||||
|
|
|
@ -133,9 +133,9 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
|||
}
|
||||
|
||||
private Comp<CompStructure<TextField>> createPathBar() {
|
||||
var path = new SimpleStringProperty(model.getCurrentPath().get());
|
||||
var path = new SimpleStringProperty();
|
||||
model.getCurrentPath().subscribe((newValue) -> {
|
||||
path.set(newValue);
|
||||
path.set(newValue != null ? newValue.toString() : null);
|
||||
});
|
||||
path.addListener((observable, oldValue, newValue) -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
|
@ -202,7 +202,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
|||
continue;
|
||||
}
|
||||
|
||||
var mi = new MenuItem(f.get(i));
|
||||
var mi = new MenuItem(f.get(i).toString());
|
||||
int target = i + 1;
|
||||
mi.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
|
@ -219,7 +219,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
|||
}
|
||||
|
||||
if (model.getHistory().getCurrent() != null) {
|
||||
var current = new MenuItem(model.getHistory().getCurrent());
|
||||
var current = new MenuItem(model.getHistory().getCurrent().toString());
|
||||
current.setDisable(true);
|
||||
cm.getItems().add(current);
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
|||
continue;
|
||||
}
|
||||
|
||||
var mi = new MenuItem(b.get(i));
|
||||
var mi = new MenuItem(b.get(i).toString());
|
||||
int target = i + 1;
|
||||
mi.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
|
|
|
@ -41,13 +41,10 @@ public class BrowserOverviewComp extends SimpleComp {
|
|||
var commonPlatform = FXCollections.<FileEntry>observableArrayList();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var common = sc.getOsType().determineInterestingPaths(sc).stream()
|
||||
.filter(s -> !s.isBlank())
|
||||
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
|
||||
.filter(entry -> {
|
||||
try {
|
||||
return sc.getShellDialect()
|
||||
.directoryExists(sc, entry.getPath())
|
||||
.executeAndCheck();
|
||||
return model.getFileSystem().directoryExists(entry.getPath());
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
return false;
|
||||
|
|
|
@ -4,7 +4,6 @@ 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;
|
||||
|
@ -138,7 +137,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
|||
dockModel.toggleView(aBoolean);
|
||||
});
|
||||
});
|
||||
AppDialog.getModalOverlay().addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
if (c.getList().size() > 0) {
|
||||
dockModel.toggleView(false);
|
||||
} else {
|
||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.browser.icon;
|
|||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
@ -38,7 +37,8 @@ public abstract class BrowserIconDirectoryType {
|
|||
|
||||
@Override
|
||||
public boolean matches(FileEntry entry) {
|
||||
return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\");
|
||||
return entry.getPath().toString().equals("/")
|
||||
|| entry.getPath().toString().matches("\\w:\\\\");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -99,7 +99,7 @@ public abstract class BrowserIconDirectoryType {
|
|||
return false;
|
||||
}
|
||||
|
||||
var name = FileNames.getFileName(entry.getPath());
|
||||
var name = entry.getPath().getFileName();
|
||||
return names.contains(name);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.browser.icon;
|
|||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
@ -84,8 +83,8 @@ public abstract class BrowserIconFileType {
|
|||
return false;
|
||||
}
|
||||
|
||||
var name = FileNames.getFileName(entry.getPath());
|
||||
var ext = FileNames.getExtension(entry.getPath());
|
||||
var name = entry.getPath().getFileName();
|
||||
var ext = entry.getPath().getFileName();
|
||||
return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,10 +58,14 @@ public abstract class Comp<S extends CompStructure<?>> {
|
|||
};
|
||||
}
|
||||
|
||||
public static Comp<CompStructure<Separator>> separator() {
|
||||
public static Comp<CompStructure<Separator>> hseparator() {
|
||||
return of(() -> new Separator(Orientation.HORIZONTAL));
|
||||
}
|
||||
|
||||
public static Comp<CompStructure<Separator>> vseparator() {
|
||||
return of(() -> new Separator(Orientation.VERTICAL));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <IR extends Region, SIN extends CompStructure<IR>, OR extends Region> Comp<CompStructure<OR>> derive(
|
||||
Comp<SIN> comp, Function<IR, OR> r) {
|
||||
|
|
|
@ -88,7 +88,7 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
|
|||
if (!hide.get()) {
|
||||
var cm = contextMenu.get();
|
||||
if (cm != null) {
|
||||
cm.show(r, Side.BOTTOM, 0, 0);
|
||||
cm.show(r, Side.TOP, 0, 0);
|
||||
currentContextMenu.set(cm);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
|
|||
multi.styleClass("background");
|
||||
|
||||
var pane = new BorderPane();
|
||||
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
|
||||
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries(), model.getQueueEntries());
|
||||
StackPane multiR = (StackPane) multi.createRegion();
|
||||
pane.setCenter(multiR);
|
||||
var sidebarR = sidebar.createRegion();
|
||||
|
|
|
@ -34,7 +34,7 @@ public class AppMainWindowContentComp extends SimpleComp {
|
|||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var overlay = AppDialog.getModalOverlay();
|
||||
var overlay = AppDialog.getModalOverlays();
|
||||
var loaded = AppMainWindow.getLoadedContent();
|
||||
var bg = Comp.of(() -> {
|
||||
var loadingIcon = new ImageView();
|
||||
|
|
|
@ -11,12 +11,15 @@ import io.xpipe.app.prefs.AppPrefs;
|
|||
import io.xpipe.app.storage.ContextualFileReference;
|
||||
import io.xpipe.app.storage.DataStorageSyncHandler;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
@ -40,13 +43,13 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
}
|
||||
|
||||
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
|
||||
private final Property<String> filePath;
|
||||
private final Property<FilePath> filePath;
|
||||
private final ContextualFileReferenceSync sync;
|
||||
private final List<PreviousFileReference> previousFileReferences;
|
||||
|
||||
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
|
||||
Property<DataStoreEntryRef<T>> fileSystem,
|
||||
Property<String> filePath,
|
||||
Property<FilePath> filePath,
|
||||
ContextualFileReferenceSync sync,
|
||||
List<PreviousFileReference> previousFileReferences) {
|
||||
this.sync = sync;
|
||||
|
@ -86,7 +89,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
}
|
||||
|
||||
var currentPath = filePath.getValue();
|
||||
if (currentPath == null || currentPath.isBlank()) {
|
||||
if (currentPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -95,7 +98,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
}
|
||||
|
||||
try {
|
||||
var source = Path.of(currentPath.trim());
|
||||
var source = Path.of(currentPath.toString());
|
||||
var target = sync.getTargetLocation().apply(source);
|
||||
if (Files.exists(source)) {
|
||||
var shouldCopy = AppWindowHelper.showConfirmationAlert(
|
||||
|
@ -108,7 +111,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
var syncedTarget = handler.addDataFile(
|
||||
source, target, sync.getPerUser().test(source));
|
||||
Platform.runLater(() -> {
|
||||
filePath.setValue(syncedTarget.toString());
|
||||
filePath.setValue(FilePath.of(syncedTarget));
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -147,7 +150,14 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
var items = allFiles.stream()
|
||||
.map(previousFileReference -> previousFileReference.getPath().toString())
|
||||
.toList();
|
||||
var combo = new ComboTextFieldComp(filePath, items, param -> {
|
||||
var prop = new SimpleStringProperty();
|
||||
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
|
||||
prop.set(s != null ? s.toString() : null);
|
||||
}));
|
||||
prop.addListener((observable, oldValue, newValue) -> {
|
||||
filePath.setValue(newValue != null ? FilePath.of(newValue) : null);
|
||||
});
|
||||
var combo = new ComboTextFieldComp(prop, items, param -> {
|
||||
return new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
|
@ -172,7 +182,14 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
}
|
||||
|
||||
private Comp<?> createTextField() {
|
||||
var fileNameComp = new TextFieldComp(filePath)
|
||||
var prop = new SimpleStringProperty();
|
||||
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
|
||||
prop.set(s != null ? s.toString() : null);
|
||||
}));
|
||||
prop.addListener((observable, oldValue, newValue) -> {
|
||||
filePath.setValue(newValue != null ? FilePath.of(newValue) : null);
|
||||
});
|
||||
var fileNameComp = new TextFieldComp(prop)
|
||||
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS))
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.grow(false, true);
|
||||
|
|
|
@ -3,7 +3,11 @@ package io.xpipe.app.comp.base;
|
|||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
|
@ -11,10 +15,14 @@ import java.util.List;
|
|||
|
||||
public class HorizontalComp extends Comp<CompStructure<HBox>> {
|
||||
|
||||
private final List<Comp<?>> entries;
|
||||
private final ObservableList<Comp<?>> entries;
|
||||
|
||||
public HorizontalComp(List<Comp<?>> comps) {
|
||||
entries = List.copyOf(comps);
|
||||
entries = FXCollections.observableArrayList(List.copyOf(comps));
|
||||
}
|
||||
|
||||
public HorizontalComp(ObservableList<Comp<?>> entries) {
|
||||
this.entries = PlatformThread.sync(entries);
|
||||
}
|
||||
|
||||
public Comp<CompStructure<HBox>> spacing(double spacing) {
|
||||
|
@ -23,8 +31,11 @@ public class HorizontalComp extends Comp<CompStructure<HBox>> {
|
|||
|
||||
@Override
|
||||
public CompStructure<HBox> createBase() {
|
||||
HBox b = new HBox();
|
||||
var b = new HBox();
|
||||
b.getStyleClass().add("horizontal-comp");
|
||||
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
|
||||
b.getChildren().setAll(c.getList().stream().map(Comp::createRegion).toList());
|
||||
});
|
||||
for (var entry : entries) {
|
||||
b.getChildren().add(entry.createRegion());
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import io.xpipe.app.comp.Comp;
|
|||
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;
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
import lombok.Value;
|
||||
|
@ -27,6 +30,12 @@ public class ModalButton {
|
|||
@NonFinal
|
||||
Consumer<Button> augment;
|
||||
|
||||
public static ModalButton hide(ObservableValue<String> name, LabelGraphic icon, Runnable action) {
|
||||
return new ModalButton("hide", () -> {
|
||||
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(name, icon, action));
|
||||
}, true, false);
|
||||
}
|
||||
|
||||
public static ModalButton finish(Runnable action) {
|
||||
return new ModalButton("finish", action, true, true);
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ public class ModalOverlay {
|
|||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return AppDialog.getModalOverlay().contains(this);
|
||||
return AppDialog.getModalOverlays().contains(this);
|
||||
}
|
||||
|
||||
public void showAndWait() {
|
||||
|
|
|
@ -281,6 +281,7 @@ public class ModalOverlayComp extends SimpleComp {
|
|||
if (mb.getAugment() != null) {
|
||||
mb.getAugment().accept(button);
|
||||
}
|
||||
button.managedProperty().bind(button.visibleProperty());
|
||||
button.setOnAction(event -> {
|
||||
if (mb.getAction() != null) {
|
||||
mb.getAction().run();
|
||||
|
|
|
@ -12,30 +12,110 @@ import io.xpipe.app.util.PlatformThread;
|
|||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
private final Property<AppLayoutModel.Entry> value;
|
||||
private final List<AppLayoutModel.Entry> entries;
|
||||
|
||||
public SideMenuBarComp(Property<AppLayoutModel.Entry> value, List<AppLayoutModel.Entry> entries) {
|
||||
this.value = value;
|
||||
this.entries = entries;
|
||||
}
|
||||
private final ObservableList<AppLayoutModel.QueueEntry> queueEntries;
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
var vbox = new VBox();
|
||||
vbox.setFillWidth(true);
|
||||
|
||||
for (AppLayoutModel.Entry e : entries) {
|
||||
var b = new IconButtonComp(e.icon(), () -> {
|
||||
if (e.action() != null) {
|
||||
e.action().run();
|
||||
return;
|
||||
}
|
||||
|
||||
value.setValue(e);
|
||||
});
|
||||
b.tooltip(e.name());
|
||||
b.accessibleText(e.name());
|
||||
|
||||
var stack = createStyle(e, b);
|
||||
var shortcut = e.combination();
|
||||
if (shortcut != null) {
|
||||
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
|
||||
}
|
||||
vbox.getChildren().add(stack.createRegion());
|
||||
}
|
||||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded());
|
||||
b
|
||||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
var stack = createStyle(null, b);
|
||||
stack.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return AppDistributionType.get()
|
||||
.getUpdateHandler()
|
||||
.getPreparedUpdate()
|
||||
.getValue()
|
||||
== null;
|
||||
},
|
||||
AppDistributionType.get().getUpdateHandler().getPreparedUpdate())));
|
||||
vbox.getChildren().add(stack.createRegion());
|
||||
}
|
||||
|
||||
var filler = new Button();
|
||||
filler.setDisable(true);
|
||||
filler.setMaxHeight(3000);
|
||||
vbox.getChildren().add(filler);
|
||||
VBox.setVgrow(filler, Priority.ALWAYS);
|
||||
vbox.getStyleClass().add("sidebar-comp");
|
||||
|
||||
var queueButtons = new VBox();
|
||||
queueEntries.addListener((ListChangeListener<? super AppLayoutModel.QueueEntry>) c -> {
|
||||
queueButtons.getChildren().clear();
|
||||
for (int i = c.getList().size() - 1; i >= 0; i--) {
|
||||
var item = c.getList().get(i);
|
||||
var b = new IconButtonComp(item.getIcon(), () -> {
|
||||
item.getAction().run();
|
||||
queueEntries.remove(item);
|
||||
});
|
||||
b.tooltip(item.getName());
|
||||
b.accessibleText(item.getName());
|
||||
var stack = createStyle(null, b);
|
||||
queueButtons.getChildren().add(stack.createRegion());
|
||||
}
|
||||
});
|
||||
vbox.getChildren().add(queueButtons);
|
||||
|
||||
return new SimpleCompStructure<>(vbox);
|
||||
}
|
||||
|
||||
private Comp<?> createStyle(AppLayoutModel.Entry e, IconButtonComp b) {
|
||||
var selected = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.get().pseudoClassStateChanged(selected, n.equals(e));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var selectedBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var c = Platform.getPreferences()
|
||||
|
@ -45,7 +125,6 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(14, 1, 14, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var hoverBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var c = Platform.getPreferences()
|
||||
|
@ -56,95 +135,38 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(14, 1, 14, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var noneBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
return Background.fill(Color.TRANSPARENT);
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var selected = PseudoClass.getPseudoClass("selected");
|
||||
for (AppLayoutModel.Entry e : entries) {
|
||||
var b = new IconButtonComp(e.icon(), () -> {
|
||||
if (e.action() != null) {
|
||||
e.action().run();
|
||||
return;
|
||||
}
|
||||
var indicator = Comp.empty().styleClass("indicator");
|
||||
var stack = new StackComp(List.of(indicator, b))
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
|
||||
stack.apply(struc -> {
|
||||
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
|
||||
indicatorRegion.setMaxWidth(7);
|
||||
indicatorRegion
|
||||
.backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (value.getValue().equals(e)) {
|
||||
return selectedBorder.get();
|
||||
}
|
||||
|
||||
value.setValue(e);
|
||||
});
|
||||
var shortcut = e.combination();
|
||||
b.apply(new TooltipAugment<>(e.name(), shortcut));
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
if (struc.get().isHover()) {
|
||||
return hoverBorder.get();
|
||||
}
|
||||
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.get().pseudoClassStateChanged(selected, n.equals(e));
|
||||
});
|
||||
});
|
||||
});
|
||||
b.accessibleText(e.name());
|
||||
|
||||
var indicator = Comp.empty().styleClass("indicator");
|
||||
var stack = new StackComp(List.of(indicator, b))
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
|
||||
stack.apply(struc -> {
|
||||
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
|
||||
indicatorRegion.setMaxWidth(7);
|
||||
indicatorRegion
|
||||
.backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (value.getValue().equals(e)) {
|
||||
return selectedBorder.get();
|
||||
}
|
||||
|
||||
if (struc.get().isHover()) {
|
||||
return hoverBorder.get();
|
||||
}
|
||||
|
||||
return noneBorder.get();
|
||||
},
|
||||
struc.get().hoverProperty(),
|
||||
value,
|
||||
hoverBorder,
|
||||
selectedBorder,
|
||||
noneBorder));
|
||||
});
|
||||
if (shortcut != null) {
|
||||
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
|
||||
}
|
||||
vbox.getChildren().add(stack.createRegion());
|
||||
}
|
||||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded())
|
||||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
});
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return AppDistributionType.get()
|
||||
.getUpdateHandler()
|
||||
.getPreparedUpdate()
|
||||
.getValue()
|
||||
== null;
|
||||
},
|
||||
AppDistributionType.get().getUpdateHandler().getPreparedUpdate())));
|
||||
vbox.getChildren().add(b.createRegion());
|
||||
}
|
||||
|
||||
var filler = new Button();
|
||||
filler.setDisable(true);
|
||||
filler.setMaxHeight(3000);
|
||||
vbox.getChildren().add(filler);
|
||||
VBox.setVgrow(filler, Priority.ALWAYS);
|
||||
vbox.getStyleClass().add("sidebar-comp");
|
||||
return new SimpleCompStructure<>(vbox);
|
||||
return noneBorder.get();
|
||||
},
|
||||
struc.get().hoverProperty(),
|
||||
value,
|
||||
hoverBorder,
|
||||
selectedBorder,
|
||||
noneBorder));
|
||||
});
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
|
41
app/src/main/java/io/xpipe/app/comp/base/ToolbarComp.java
Normal file
41
app/src/main/java/io/xpipe/app/comp/base/ToolbarComp.java
Normal file
|
@ -0,0 +1,41 @@
|
|||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.ToolBar;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ToolbarComp extends Comp<CompStructure<ToolBar>> {
|
||||
|
||||
private final ObservableList<Comp<?>> entries;
|
||||
|
||||
public ToolbarComp(List<Comp<?>> comps) {
|
||||
entries = FXCollections.observableArrayList(List.copyOf(comps));
|
||||
}
|
||||
|
||||
public ToolbarComp(ObservableList<Comp<?>> entries) {
|
||||
this.entries = PlatformThread.sync(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<ToolBar> createBase() {
|
||||
var b = new ToolBar();
|
||||
b.getStyleClass().add("horizontal-comp");
|
||||
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
|
||||
b.getItems().setAll(c.getList().stream().map(Comp::createRegion).toList());
|
||||
});
|
||||
for (var entry : entries) {
|
||||
b.getItems().add(entry.createRegion());
|
||||
}
|
||||
b.visibleProperty().bind(Bindings.isNotEmpty(entries));
|
||||
return new SimpleCompStructure<>(b);
|
||||
}
|
||||
}
|
|
@ -77,9 +77,20 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
var notes = new StoreNotesComp(getWrapper()).createRegion();
|
||||
var userIcon = createUserIcon().createRegion();
|
||||
|
||||
var selection = createBatchSelection().createRegion();
|
||||
grid.add(selection, 0, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(25));
|
||||
StoreViewState.get().getBatchMode().subscribe(batch -> {
|
||||
if (batch) {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
|
||||
} else {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(-8));
|
||||
}
|
||||
});
|
||||
|
||||
var storeIcon = createIcon(28, 24);
|
||||
GridPane.setHalignment(storeIcon, HPos.CENTER);
|
||||
grid.add(storeIcon, 0, 0);
|
||||
grid.add(storeIcon, 1, 0);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(34));
|
||||
|
||||
var customSize = content != null ? 100 : 0;
|
||||
|
|
|
@ -43,15 +43,26 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
grid.setHgap(6);
|
||||
grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0);
|
||||
|
||||
var selection = createBatchSelection();
|
||||
grid.add(selection.createRegion(), 0, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(25));
|
||||
StoreViewState.get().getBatchMode().subscribe(batch -> {
|
||||
if (batch) {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
|
||||
} else {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(-6));
|
||||
}
|
||||
});
|
||||
|
||||
var storeIcon = createIcon(46, 40);
|
||||
grid.add(storeIcon, 0, 0, 1, 2);
|
||||
grid.add(storeIcon, 1, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(52));
|
||||
|
||||
var active = new StoreActiveComp(getWrapper()).createRegion();
|
||||
var nameBox = new HBox(name, userIcon, notes);
|
||||
nameBox.setSpacing(6);
|
||||
nameBox.setAlignment(Pos.CENTER_LEFT);
|
||||
grid.add(nameBox, 1, 0);
|
||||
grid.add(nameBox, 2, 0);
|
||||
GridPane.setVgrow(nameBox, Priority.ALWAYS);
|
||||
getWrapper().getSessionActive().subscribe(aBoolean -> {
|
||||
if (!aBoolean) {
|
||||
|
@ -64,7 +75,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
var summaryBox = new HBox(createSummary());
|
||||
summaryBox.setAlignment(Pos.TOP_LEFT);
|
||||
GridPane.setVgrow(summaryBox, Priority.ALWAYS);
|
||||
grid.add(summaryBox, 1, 1);
|
||||
grid.add(summaryBox, 2, 1);
|
||||
|
||||
var nameCC = new ColumnConstraints();
|
||||
nameCC.setMinWidth(100);
|
||||
|
@ -72,7 +83,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
nameCC.setPrefWidth(100);
|
||||
grid.getColumnConstraints().addAll(nameCC);
|
||||
|
||||
grid.add(createInformation(), 2, 0, 1, 2);
|
||||
grid.add(createInformation(), 3, 0, 1, 2);
|
||||
var info = new ColumnConstraints();
|
||||
info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);
|
||||
info.setHalignment(HPos.LEFT);
|
||||
|
@ -89,7 +100,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
controls.setAlignment(Pos.CENTER_RIGHT);
|
||||
controls.setSpacing(10);
|
||||
controls.setPadding(new Insets(0, 0, 0, 10));
|
||||
grid.add(controls, 3, 0, 1, 2);
|
||||
grid.add(controls, 4, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(custom);
|
||||
|
||||
grid.getStyleClass().add("store-entry-grid");
|
||||
|
|
|
@ -13,8 +13,8 @@ import io.xpipe.app.storage.DataStoreCategory;
|
|||
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;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
|
@ -41,467 +42,39 @@ import java.util.UUID;
|
|||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class StoreCreationComp extends DialogComp {
|
||||
public class StoreCreationComp extends SimpleComp {
|
||||
|
||||
Stage window;
|
||||
CreationConsumer consumer;
|
||||
Property<DataStoreProvider> provider;
|
||||
ObjectProperty<DataStore> store;
|
||||
Predicate<DataStoreProvider> filter;
|
||||
BooleanProperty busy = new SimpleBooleanProperty();
|
||||
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
|
||||
Property<ModalOverlay> messageProp = new SimpleObjectProperty<>();
|
||||
BooleanProperty finished = new SimpleBooleanProperty();
|
||||
ObservableValue<DataStoreEntry> entry;
|
||||
BooleanProperty changedSinceError = new SimpleBooleanProperty();
|
||||
BooleanProperty skippable = new SimpleBooleanProperty();
|
||||
BooleanProperty connectable = new SimpleBooleanProperty();
|
||||
StringProperty name;
|
||||
DataStoreEntry existingEntry;
|
||||
boolean staticDisplay;
|
||||
private final StoreCreationModel model;
|
||||
|
||||
public StoreCreationComp(
|
||||
Stage window,
|
||||
CreationConsumer consumer,
|
||||
Property<DataStoreProvider> provider,
|
||||
ObjectProperty<DataStore> store,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
String initialName,
|
||||
DataStoreEntry existingEntry,
|
||||
boolean staticDisplay) {
|
||||
this.window = window;
|
||||
this.consumer = consumer;
|
||||
this.provider = provider;
|
||||
this.store = store;
|
||||
this.filter = filter;
|
||||
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
|
||||
this.existingEntry = existingEntry;
|
||||
this.staticDisplay = staticDisplay;
|
||||
this.store.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
this.name.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
|
||||
this.provider.addListener((c, o, n) -> {
|
||||
store.unbind();
|
||||
store.setValue(null);
|
||||
if (n != null) {
|
||||
store.setValue(n.defaultStore());
|
||||
}
|
||||
});
|
||||
|
||||
this.provider.subscribe((n) -> {
|
||||
if (n != null) {
|
||||
connectable.setValue(n.canConnectDuringCreation());
|
||||
}
|
||||
});
|
||||
|
||||
this.apply(r -> {
|
||||
r.get().setPrefWidth(650);
|
||||
r.get().setPrefHeight(750);
|
||||
});
|
||||
|
||||
this.validator.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
newValue.validate();
|
||||
});
|
||||
});
|
||||
this.entry = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (name.getValue() == null || store.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var testE = DataStoreEntry.createNew(
|
||||
UUID.randomUUID(),
|
||||
DataStorage.get().getSelectedCategory().getUuid(),
|
||||
name.getValue(),
|
||||
store.getValue());
|
||||
var p = DataStorage.get().getDefaultDisplayParent(testE).orElse(null);
|
||||
|
||||
var targetCategory = p != null
|
||||
? p.getCategoryUuid()
|
||||
: DataStorage.get().getSelectedCategory().getUuid();
|
||||
var rootCategory = DataStorage.get()
|
||||
.getRootCategory(DataStorage.get()
|
||||
.getStoreCategoryIfPresent(targetCategory)
|
||||
.orElseThrow());
|
||||
|
||||
// Don't put it in the wrong root category
|
||||
if ((provider.getValue().getCreationCategory() == null
|
||||
|| !provider.getValue()
|
||||
.getCreationCategory()
|
||||
.getCategory()
|
||||
.equals(rootCategory.getUuid()))) {
|
||||
targetCategory = provider.getValue().getCreationCategory() != null
|
||||
? provider.getValue().getCreationCategory().getCategory()
|
||||
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all connections category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllConnectionsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.get()
|
||||
.getDefaultConnectionsCategory()
|
||||
.getUuid();
|
||||
}
|
||||
|
||||
// Don't use the all scripts category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllScriptsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all identities category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllIdentitiesCategory().getUuid())) {
|
||||
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Custom category stuff
|
||||
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
|
||||
|
||||
return DataStoreEntry.createNew(
|
||||
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
|
||||
},
|
||||
name,
|
||||
store);
|
||||
|
||||
skippable.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
store,
|
||||
name));
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e) {
|
||||
showEdit(e, dataStoreEntry -> {});
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
|
||||
show(
|
||||
e.getName(),
|
||||
e.getProvider(),
|
||||
e.getStore(),
|
||||
v -> true,
|
||||
(newE, validated) -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
||||
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
||||
} else {
|
||||
// We didn't change anything
|
||||
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,
|
||||
e);
|
||||
}
|
||||
|
||||
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
|
||||
showCreation(selected != null ? selected.defaultStore() : null, category, dataStoreEntry -> {}, true);
|
||||
}
|
||||
|
||||
public static void showCreation(
|
||||
DataStore base,
|
||||
DataStoreCreationCategory category,
|
||||
Consumer<DataStoreEntry> listener,
|
||||
boolean selectCategory) {
|
||||
var prov = base != null ? DataStoreProviders.byStore(base) : null;
|
||||
show(
|
||||
null,
|
||||
prov,
|
||||
base,
|
||||
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|
||||
|| dataStoreProvider.equals(prov),
|
||||
(e, validated) -> {
|
||||
try {
|
||||
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
|
||||
listener.accept(returned);
|
||||
if (validated
|
||||
&& e.getProvider().shouldShowScan()
|
||||
&& AppPrefs.get()
|
||||
.openConnectionSearchWindowOnConnectionCreation()
|
||||
.get()) {
|
||||
ScanDialog.showAsync(e);
|
||||
}
|
||||
|
||||
if (selectCategory) {
|
||||
// Select new category if needed
|
||||
var cat = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(e.getCategoryUuid())
|
||||
.orElseThrow();
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
StoreViewState.get()
|
||||
.getActiveCategory()
|
||||
.setValue(StoreViewState.get().getCategoryWrapper(cat));
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
}
|
||||
},
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
public interface CreationConsumer {
|
||||
|
||||
void consume(DataStoreEntry entry, boolean validated);
|
||||
}
|
||||
|
||||
private static void show(
|
||||
String initialName,
|
||||
DataStoreProvider provider,
|
||||
DataStore s,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
CreationConsumer con,
|
||||
boolean staticDisplay,
|
||||
DataStoreEntry existingEntry) {
|
||||
var prop = new SimpleObjectProperty<>(provider);
|
||||
var store = new SimpleObjectProperty<>(s);
|
||||
DialogComp.showWindow(
|
||||
"addConnection",
|
||||
stage -> new StoreCreationComp(
|
||||
stage, con, prop, store, filter, initialName, existingEntry, staticDisplay));
|
||||
}
|
||||
|
||||
private static boolean showInvalidConfirmAlert() {
|
||||
return AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
|
||||
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
|
||||
alert.getDialogPane()
|
||||
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
|
||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||
alert.getButtonTypes().clear();
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("retry"), ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OK_DONE));
|
||||
})
|
||||
.map(b -> b.getButtonData().isDefaultButton())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Comp<?>> customButtons() {
|
||||
return List.of(
|
||||
new ButtonComp(AppI18n.observable("skipValidation"), () -> {
|
||||
if (showInvalidConfirmAlert()) {
|
||||
commit(false);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.visible(skippable),
|
||||
new ButtonComp(AppI18n.observable("connect"), () -> {
|
||||
var temp = DataStoreEntry.createTempWrapper(store.getValue());
|
||||
var action = provider.getValue().launchAction(temp);
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
action.execute();
|
||||
});
|
||||
})
|
||||
.hide(connectable
|
||||
.not()
|
||||
.or(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return store.getValue() == null
|
||||
|| !store.getValue().isComplete();
|
||||
},
|
||||
store))));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObservableValue<Boolean> busy() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void discard() {}
|
||||
|
||||
@Override
|
||||
protected void finish() {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We didn't change anything
|
||||
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
|
||||
commit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.getValue().validate()) {
|
||||
var msg = validator
|
||||
.getValue()
|
||||
.getValidationResult()
|
||||
.getMessages()
|
||||
.getFirst()
|
||||
.getText();
|
||||
TrackEvent.info(msg);
|
||||
messageProp.setValue(createErrorOverlay(msg));
|
||||
changedSinceError.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
// Might have changed since last time
|
||||
if (entry.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BooleanScope(busy).start()) {
|
||||
DataStorage.get().addStoreEntryInProgress(entry.getValue());
|
||||
entry.getValue().validateOrThrow();
|
||||
commit(true);
|
||||
} catch (Throwable ex) {
|
||||
String message;
|
||||
if (ex instanceof ValidationException) {
|
||||
ErrorEvent.expected(ex);
|
||||
message = ex.getMessage();
|
||||
} else if (ex instanceof StackOverflowError) {
|
||||
// Cycles in connection graphs can fail hard but are expected
|
||||
ErrorEvent.expected(ex);
|
||||
message = "StackOverflowError";
|
||||
} else {
|
||||
message = ex.getMessage();
|
||||
}
|
||||
|
||||
messageProp.setValue(createErrorOverlay(message));
|
||||
changedSinceError.setValue(false);
|
||||
|
||||
ErrorEvent.fromThrowable(ex).omit().handle();
|
||||
} finally {
|
||||
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> content() {
|
||||
return Comp.of(this::createLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Comp<?> pane(Comp<?> content) {
|
||||
var back = super.pane(content);
|
||||
return new ModalOverlayComp(back, messageProp);
|
||||
}
|
||||
|
||||
private ModalOverlay createErrorOverlay(String message) {
|
||||
var comp = Comp.of(() -> {
|
||||
var l = new TextArea();
|
||||
l.setText(message);
|
||||
l.setWrapText(true);
|
||||
l.getStyleClass().add("error-overlay-comp");
|
||||
l.setEditable(false);
|
||||
return l;
|
||||
});
|
||||
var overlay = ModalOverlay.of("error", comp, new LabelGraphic.NodeGraphic(() -> {
|
||||
var graphic = new FontIcon("mdomz-warning");
|
||||
graphic.setIconColor(Color.RED);
|
||||
return new StackPane(graphic);
|
||||
}));
|
||||
return overlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> bottom() {
|
||||
var disable = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return provider.getValue() == null
|
||||
|| store.getValue() == null
|
||||
|| !store.getValue().isComplete()
|
||||
// When switching providers, both observables change one after another.
|
||||
// So temporarily there might be a store class mismatch
|
||||
|| provider.getValue().getStoreClasses().stream()
|
||||
.noneMatch(aClass -> aClass.isAssignableFrom(
|
||||
store.getValue().getClass()))
|
||||
|| provider.getValue().createInsightsMarkdown(store.getValue()) == null;
|
||||
},
|
||||
provider,
|
||||
store);
|
||||
return new PopupMenuButtonComp(
|
||||
new SimpleStringProperty("Insights >"),
|
||||
Comp.of(() -> {
|
||||
return provider.getValue() != null
|
||||
? provider.getValue()
|
||||
.createInsightsComp(store)
|
||||
.createRegion()
|
||||
: null;
|
||||
}),
|
||||
true)
|
||||
.hide(disable)
|
||||
.styleClass("button-comp");
|
||||
}
|
||||
public StoreCreationComp(StoreCreationModel model) {this.model = model;}
|
||||
|
||||
private Region createStoreProperties(Comp<?> comp, Validator propVal) {
|
||||
var p = provider.getValue();
|
||||
var nameKey = p == null
|
||||
|| p.getCreationCategory() == null
|
||||
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
|
||||
? "connection"
|
||||
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
|
||||
? "script"
|
||||
: "identity";
|
||||
var nameKey = model.storeTypeNameKey();
|
||||
return new OptionsBuilder()
|
||||
.addComp(comp, store)
|
||||
.addComp(comp, model.getStore())
|
||||
.name(nameKey + "Name")
|
||||
.description(nameKey + "NameDescription")
|
||||
.addString(name, false)
|
||||
.addString(model.getName(), false)
|
||||
.nonNull(propVal)
|
||||
.buildComp()
|
||||
.onSceneAssign(struc -> {
|
||||
if (staticDisplay) {
|
||||
if (model.isStaticDisplay()) {
|
||||
struc.get().requestFocus();
|
||||
}
|
||||
})
|
||||
.styleClass("store-creator-options")
|
||||
.createRegion();
|
||||
}
|
||||
|
||||
private void commit(boolean validated) {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
finished.setValue(true);
|
||||
|
||||
if (entry.getValue() != null) {
|
||||
consumer.consume(entry.getValue(), validated);
|
||||
}
|
||||
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
private Region createLayout() {
|
||||
var layout = new BorderPane();
|
||||
layout.getStyleClass().add("store-creator");
|
||||
var providerChoice = new StoreProviderChoiceComp(filter, provider);
|
||||
var showProviders = (!staticDisplay
|
||||
var providerChoice = new StoreProviderChoiceComp(model.getFilter(), model.getProvider());
|
||||
var showProviders = (!model.isStaticDisplay()
|
||||
&& (providerChoice.getProviders().size() > 1
|
||||
|| providerChoice.getProviders().getFirst().showProviderChoice()))
|
||||
|| (staticDisplay && provider.getValue().showProviderChoice());
|
||||
if (staticDisplay) {
|
||||
|| (model.isStaticDisplay() && model.getProvider().getValue().showProviderChoice());
|
||||
if (model.isStaticDisplay()) {
|
||||
providerChoice.apply(struc -> struc.get().setDisable(true));
|
||||
}
|
||||
if (showProviders) {
|
||||
|
@ -509,21 +82,25 @@ public class StoreCreationComp extends DialogComp {
|
|||
}
|
||||
providerChoice.apply(GrowAugment.create(true, false));
|
||||
|
||||
provider.subscribe(n -> {
|
||||
model.getProvider().subscribe(n -> {
|
||||
if (n != null) {
|
||||
var d = n.guiDialog(existingEntry, store);
|
||||
var d = n.guiDialog(model.getExistingEntry(), model.getStore());
|
||||
var propVal = new SimpleValidator();
|
||||
var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal);
|
||||
|
||||
var sp = new ScrollPane(propR);
|
||||
var valSp = new GraphicDecorationStackPane();
|
||||
valSp.getChildren().add(propR);
|
||||
|
||||
var sp = new ScrollPane(valSp);
|
||||
sp.setFitToWidth(true);
|
||||
|
||||
layout.setCenter(sp);
|
||||
|
||||
validator.setValue(new ChainedValidator(List.of(
|
||||
model.getValidator().setValue(new ChainedValidator(List.of(
|
||||
d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal)));
|
||||
} else {
|
||||
layout.setCenter(null);
|
||||
validator.setValue(new SimpleValidator());
|
||||
model.getValidator().setValue(new SimpleValidator());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -533,13 +110,12 @@ public class StoreCreationComp extends DialogComp {
|
|||
top.getStyleClass().add("top");
|
||||
if (showProviders) {
|
||||
layout.setTop(top);
|
||||
layout.setPadding(new Insets(15, 20, 20, 20));
|
||||
} else {
|
||||
layout.setPadding(new Insets(5, 20, 20, 20));
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
var valSp = new GraphicDecorationStackPane();
|
||||
valSp.getChildren().add(layout);
|
||||
return valSp;
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
return createLayout();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.base.DialogComp;
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import org.bouncycastle.math.raw.Mod;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class StoreCreationDialog {
|
||||
|
||||
public static void showEdit(DataStoreEntry e) {
|
||||
showEdit(e, dataStoreEntry -> {});
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
|
||||
show(
|
||||
e.getName(),
|
||||
e.getProvider(),
|
||||
e.getStore(),
|
||||
v -> true,
|
||||
(newE, validated) -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
||||
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
||||
} else {
|
||||
// We didn't change anything
|
||||
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,
|
||||
e);
|
||||
}
|
||||
|
||||
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
|
||||
showCreation(selected != null ? selected.defaultStore() : null, category, dataStoreEntry -> {}, true);
|
||||
}
|
||||
|
||||
public static void showCreation(
|
||||
DataStore base,
|
||||
DataStoreCreationCategory category,
|
||||
Consumer<DataStoreEntry> listener,
|
||||
boolean selectCategory) {
|
||||
var prov = base != null ? DataStoreProviders.byStore(base) : null;
|
||||
show(
|
||||
null,
|
||||
prov,
|
||||
base,
|
||||
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|
||||
|| dataStoreProvider.equals(prov),
|
||||
(e, validated) -> {
|
||||
try {
|
||||
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
|
||||
listener.accept(returned);
|
||||
if (validated
|
||||
&& e.getProvider().shouldShowScan()
|
||||
&& AppPrefs.get()
|
||||
.openConnectionSearchWindowOnConnectionCreation()
|
||||
.get()) {
|
||||
ScanDialog.showAsync(e);
|
||||
}
|
||||
|
||||
if (selectCategory) {
|
||||
// Select new category if needed
|
||||
var cat = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(e.getCategoryUuid())
|
||||
.orElseThrow();
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
StoreViewState.get()
|
||||
.getActiveCategory()
|
||||
.setValue(StoreViewState.get().getCategoryWrapper(cat));
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
}
|
||||
},
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
public interface CreationConsumer {
|
||||
|
||||
void consume(DataStoreEntry entry, boolean validated);
|
||||
}
|
||||
|
||||
private static void show(
|
||||
String initialName,
|
||||
DataStoreProvider provider,
|
||||
DataStore s,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
CreationConsumer con,
|
||||
boolean staticDisplay,
|
||||
DataStoreEntry existingEntry) {
|
||||
var prop = new SimpleObjectProperty<>(provider);
|
||||
var store = new SimpleObjectProperty<>(s);
|
||||
var model = new StoreCreationModel(prop, store, filter, initialName, existingEntry, staticDisplay);
|
||||
var modal = createModalOverlay(model);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
private static boolean showInvalidConfirmAlert() {
|
||||
return AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
|
||||
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
|
||||
alert.getDialogPane()
|
||||
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
|
||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||
alert.getButtonTypes().clear();
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("retry"), ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OK_DONE));
|
||||
})
|
||||
.map(b -> b.getButtonData().isDefaultButton())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static ModalOverlay createModalOverlay(StoreCreationModel model) {
|
||||
var comp = new StoreCreationComp(model);
|
||||
comp.prefWidth(650);
|
||||
var nameKey = model.storeTypeNameKey() + "Add";
|
||||
var modal = ModalOverlay.of(nameKey, comp);
|
||||
modal.persist();
|
||||
modal.addButton(new ModalButton("docs", () -> {
|
||||
model.showDocs();
|
||||
}, false, false).augment(button -> {
|
||||
button.visibleProperty().bind(Bindings.not(model.canShowDocs()));
|
||||
}));
|
||||
modal.addButton(ModalButton.cancel());
|
||||
var graphic = model.getProvider().getValue() != null ?
|
||||
new LabelGraphic.ImageGraphic(model.getProvider().getValue().getDisplayIconFileName(null), 20) :
|
||||
new LabelGraphic.IconGraphic("mdi2b-beaker-plus-outline");
|
||||
modal.addButton(ModalButton.hide(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
|
||||
modal.show();
|
||||
}));
|
||||
modal.addButton(new ModalButton("connect", () -> {
|
||||
model.connect();
|
||||
}, false, false).augment(button -> {
|
||||
button.visibleProperty().bind(Bindings.not(model.canConnect()));
|
||||
}));
|
||||
modal.addButton(new ModalButton("skipValidation", () -> {
|
||||
if (showInvalidConfirmAlert()) {
|
||||
model.commit();
|
||||
} else {
|
||||
model.finish();
|
||||
}
|
||||
}, true, false));
|
||||
modal.addButton(new ModalButton("finish", () -> {
|
||||
model.finish();
|
||||
}, true, true));
|
||||
return modal;
|
||||
}
|
||||
}
|
|
@ -67,7 +67,7 @@ public class StoreCreationMenu {
|
|||
item.setGraphic(new FontIcon(graphic));
|
||||
item.textProperty().bind(AppI18n.observable(name));
|
||||
item.setOnAction(event -> {
|
||||
StoreCreationComp.showCreation(
|
||||
StoreCreationDialog.showCreation(
|
||||
defaultProvider != null
|
||||
? DataStoreProviders.byId(defaultProvider).orElseThrow()
|
||||
: null,
|
||||
|
@ -85,7 +85,7 @@ public class StoreCreationMenu {
|
|||
return;
|
||||
}
|
||||
|
||||
StoreCreationComp.showCreation(
|
||||
StoreCreationDialog.showCreation(
|
||||
defaultProvider != null
|
||||
? DataStoreProviders.byId(defaultProvider).orElseThrow()
|
||||
: null,
|
||||
|
@ -108,7 +108,7 @@ public class StoreCreationMenu {
|
|||
item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)
|
||||
.createRegion());
|
||||
item.setOnAction(event -> {
|
||||
StoreCreationComp.showCreation(dataStoreProvider, category);
|
||||
StoreCreationDialog.showCreation(dataStoreProvider, category);
|
||||
event.consume();
|
||||
});
|
||||
menu.getItems().add(item);
|
||||
|
|
|
@ -0,0 +1,284 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.DialogComp;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.store.ValidatableStore;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import net.synedra.validatorfx.GraphicDecorationStackPane;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@Getter
|
||||
public class StoreCreationModel {
|
||||
|
||||
Property<DataStoreProvider> provider;
|
||||
ObjectProperty<DataStore> store;
|
||||
Predicate<DataStoreProvider> filter;
|
||||
BooleanProperty busy = new SimpleBooleanProperty();
|
||||
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
|
||||
BooleanProperty finished = new SimpleBooleanProperty();
|
||||
ObservableValue<DataStoreEntry> entry;
|
||||
BooleanProperty changedSinceError = new SimpleBooleanProperty();
|
||||
BooleanProperty skippable = new SimpleBooleanProperty();
|
||||
BooleanProperty connectable = new SimpleBooleanProperty();
|
||||
StringProperty name;
|
||||
DataStoreEntry existingEntry;
|
||||
boolean staticDisplay;
|
||||
|
||||
public StoreCreationModel(
|
||||
Property<DataStoreProvider> provider,
|
||||
ObjectProperty<DataStore> store, Predicate<DataStoreProvider> filter,
|
||||
String initialName,
|
||||
DataStoreEntry existingEntry,
|
||||
boolean staticDisplay) {
|
||||
this.provider = provider;
|
||||
this.store = store;
|
||||
this.filter = filter;
|
||||
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
|
||||
this.existingEntry = existingEntry;
|
||||
this.staticDisplay = staticDisplay;
|
||||
this.store.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
this.name.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
|
||||
this.provider.addListener((c, o, n) -> {
|
||||
store.unbind();
|
||||
store.setValue(null);
|
||||
if (n != null) {
|
||||
store.setValue(n.defaultStore());
|
||||
}
|
||||
});
|
||||
|
||||
this.provider.subscribe((n) -> {
|
||||
if (n != null) {
|
||||
connectable.setValue(n.canConnectDuringCreation());
|
||||
}
|
||||
});
|
||||
|
||||
this.validator.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
newValue.validate();
|
||||
});
|
||||
});
|
||||
this.entry = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (name.getValue() == null || store.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var testE = DataStoreEntry.createNew(
|
||||
UUID.randomUUID(),
|
||||
DataStorage.get().getSelectedCategory().getUuid(),
|
||||
name.getValue(),
|
||||
store.getValue());
|
||||
var p = DataStorage.get().getDefaultDisplayParent(testE).orElse(null);
|
||||
|
||||
var targetCategory = p != null
|
||||
? p.getCategoryUuid()
|
||||
: DataStorage.get().getSelectedCategory().getUuid();
|
||||
var rootCategory = DataStorage.get()
|
||||
.getRootCategory(DataStorage.get()
|
||||
.getStoreCategoryIfPresent(targetCategory)
|
||||
.orElseThrow());
|
||||
|
||||
// Don't put it in the wrong root category
|
||||
if ((provider.getValue().getCreationCategory() == null
|
||||
|| !provider.getValue()
|
||||
.getCreationCategory()
|
||||
.getCategory()
|
||||
.equals(rootCategory.getUuid()))) {
|
||||
targetCategory = provider.getValue().getCreationCategory() != null
|
||||
? provider.getValue().getCreationCategory().getCategory()
|
||||
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all connections category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllConnectionsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.get()
|
||||
.getDefaultConnectionsCategory()
|
||||
.getUuid();
|
||||
}
|
||||
|
||||
// Don't use the all scripts category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllScriptsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all identities category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllIdentitiesCategory().getUuid())) {
|
||||
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Custom category stuff
|
||||
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
|
||||
|
||||
return DataStoreEntry.createNew(
|
||||
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
|
||||
},
|
||||
name,
|
||||
store);
|
||||
|
||||
skippable.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
store,
|
||||
name));
|
||||
}
|
||||
|
||||
ObservableBooleanValue canConnect() {
|
||||
return connectable
|
||||
.not()
|
||||
.or(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return store.getValue() == null
|
||||
|| !store.getValue().isComplete();
|
||||
},
|
||||
store));
|
||||
}
|
||||
|
||||
void connect() {
|
||||
var temp = DataStoreEntry.createTempWrapper(store.getValue());
|
||||
var action = provider.getValue().launchAction(temp);
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
action.execute();
|
||||
});
|
||||
}
|
||||
|
||||
ObservableValue<Boolean> busy() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
void finish() {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We didn't change anything
|
||||
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
|
||||
commit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.getValue().validate()) {
|
||||
var msg = validator
|
||||
.getValue()
|
||||
.getValidationResult()
|
||||
.getMessages()
|
||||
.getFirst()
|
||||
.getText();
|
||||
ErrorEvent.fromMessage(msg).handle();
|
||||
changedSinceError.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
// Might have changed since last time
|
||||
if (entry.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BooleanScope(busy).start()) {
|
||||
DataStorage.get().addStoreEntryInProgress(entry.getValue());
|
||||
entry.getValue().validateOrThrow();
|
||||
commit();
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof ValidationException) {
|
||||
ErrorEvent.expected(ex);
|
||||
} else if (ex instanceof StackOverflowError) {
|
||||
// Cycles in connection graphs can fail hard but are expected
|
||||
ErrorEvent.expected(ex);
|
||||
}
|
||||
|
||||
changedSinceError.setValue(false);
|
||||
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
} finally {
|
||||
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void showDocs() {
|
||||
Hyperlinks.open(provider.getValue().getHelpLink());
|
||||
}
|
||||
|
||||
ObservableBooleanValue canShowDocs() {
|
||||
var disable = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return provider.getValue() == null || provider.getValue().getHelpLink() == null;
|
||||
},
|
||||
provider);
|
||||
return disable;
|
||||
}
|
||||
|
||||
void commit() {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
finished.setValue(true);
|
||||
}
|
||||
|
||||
public String storeTypeNameKey() {
|
||||
var p = provider.getValue();
|
||||
var nameKey = p == null
|
||||
|| p.getCreationCategory() == null
|
||||
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
|
||||
? "connection"
|
||||
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
|
||||
? "script"
|
||||
: "identity";
|
||||
return nameKey;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
public class StoreEntryBatchSelectComp extends SimpleComp {
|
||||
|
||||
private final StoreSection section;
|
||||
|
||||
public StoreEntryBatchSelectComp(StoreSection section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var cb = new CheckBox();
|
||||
cb.setAllowIndeterminate(true);
|
||||
cb.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
StoreViewState.get().selectBatchMode(section);
|
||||
} else {
|
||||
StoreViewState.get().unselectBatchMode(section);
|
||||
}
|
||||
});
|
||||
|
||||
StoreViewState.get().getBatchModeSelection().getList().addListener((ListChangeListener<
|
||||
? super StoreEntryWrapper>)
|
||||
c -> {
|
||||
Platform.runLater(() -> {
|
||||
update(cb);
|
||||
});
|
||||
});
|
||||
section.getShownChildren().getList().addListener((ListChangeListener<? super StoreSection>) c -> {
|
||||
if (cb.isSelected()) {
|
||||
StoreViewState.get().selectBatchMode(section);
|
||||
} else {
|
||||
StoreViewState.get().unselectBatchMode(section);
|
||||
}
|
||||
});
|
||||
|
||||
cb.getStyleClass().add("batch-mode-selector");
|
||||
cb.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
cb.setSelected(!cb.isSelected());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
return cb;
|
||||
}
|
||||
|
||||
private void update(CheckBox checkBox) {
|
||||
var isSelected = StoreViewState.get().isSectionSelected(section);
|
||||
checkBox.setSelected(isSelected);
|
||||
if (section.getShownChildren().getList().size() == 0) {
|
||||
checkBox.setIndeterminate(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var count = section.getShownChildren().getList().stream()
|
||||
.filter(c ->
|
||||
StoreViewState.get().getBatchModeSelection().getList().contains(c.getWrapper()))
|
||||
.count();
|
||||
checkBox.setIndeterminate(
|
||||
count > 0 && count != section.getShownChildren().getList().size());
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -88,6 +88,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var r = createContent();
|
||||
var buttonBar = r.lookup(".button-bar");
|
||||
var iconChooser = r.lookup(".icon");
|
||||
var batchMode = r.lookup(".batch-mode-selector");
|
||||
|
||||
var button = new Button();
|
||||
button.setGraphic(r);
|
||||
|
@ -105,6 +106,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|
||||
|| NodeHelper.isParent(batchMode, event.getTarget())
|
||||
|| NodeHelper.isParent(buttonBar, event.getTarget());
|
||||
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
|
||||
|
@ -118,6 +120,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|
||||
|| NodeHelper.isParent(batchMode, event.getTarget())
|
||||
|| NodeHelper.isParent(buttonBar, event.getTarget());
|
||||
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
|
||||
|
@ -276,6 +279,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
return settingsButton;
|
||||
}
|
||||
|
||||
protected Comp<?> createBatchSelection() {
|
||||
var c = new StoreEntryBatchSelectComp(section);
|
||||
c.hide(StoreViewState.get().getBatchMode().not());
|
||||
return c;
|
||||
}
|
||||
|
||||
protected ContextMenu createContextMenu() {
|
||||
var contextMenu = ContextMenuHelper.create();
|
||||
|
||||
|
|
|
@ -4,15 +4,19 @@ import io.xpipe.app.comp.Comp;
|
|||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.comp.base.MultiContentComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class StoreEntryListComp extends SimpleComp {
|
||||
|
||||
|
@ -48,7 +52,15 @@ public class StoreEntryListComp extends SimpleComp {
|
|||
struc.get().setVvalue(0);
|
||||
});
|
||||
});
|
||||
return content.styleClass("store-list-comp");
|
||||
content.styleClass("store-list-comp");
|
||||
content.vgrow();
|
||||
|
||||
var statusBar = new StoreEntryListStatusBarComp();
|
||||
statusBar.apply(struc -> {
|
||||
VBox.setMargin(struc.get(), new Insets(3, 6, 4, 2));
|
||||
});
|
||||
statusBar.hide(StoreViewState.get().getBatchMode().not());
|
||||
return new VerticalComp(List.of(content, statusBar));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,8 +13,6 @@ import io.xpipe.app.util.ThreadHelper;
|
|||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
|
@ -26,6 +24,7 @@ import javafx.scene.layout.Region;
|
|||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
@ -91,24 +90,30 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
StoreViewState.get().getFilterString().setValue(newValue);
|
||||
});
|
||||
});
|
||||
var filter = new FilterComp(StoreViewState.get().getFilterString());
|
||||
var f = filter.createRegion();
|
||||
var button = createAddButton();
|
||||
var hbox = new HBox(button, f);
|
||||
f.minHeightProperty().bind(button.heightProperty());
|
||||
f.prefHeightProperty().bind(button.heightProperty());
|
||||
f.maxHeightProperty().bind(button.heightProperty());
|
||||
var filter = new FilterComp(StoreViewState.get().getFilterString()).createRegion();
|
||||
var add = createAddButton();
|
||||
var batchMode = createBatchModeButton().createRegion();
|
||||
var hbox = new HBox(add, filter, batchMode);
|
||||
filter.minHeightProperty().bind(add.heightProperty());
|
||||
filter.prefHeightProperty().bind(add.heightProperty());
|
||||
filter.maxHeightProperty().bind(add.heightProperty());
|
||||
batchMode.minHeightProperty().bind(add.heightProperty());
|
||||
batchMode.prefHeightProperty().bind(add.heightProperty());
|
||||
batchMode.maxHeightProperty().bind(add.heightProperty());
|
||||
batchMode.minWidthProperty().bind(add.heightProperty());
|
||||
batchMode.prefWidthProperty().bind(add.heightProperty());
|
||||
batchMode.maxWidthProperty().bind(add.heightProperty());
|
||||
hbox.setSpacing(8);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
HBox.setHgrow(f, Priority.ALWAYS);
|
||||
HBox.setHgrow(filter, Priority.ALWAYS);
|
||||
|
||||
f.getStyleClass().add("filter-bar");
|
||||
filter.getStyleClass().add("filter-bar");
|
||||
return hbox;
|
||||
}
|
||||
|
||||
private Region createAddButton() {
|
||||
var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick"));
|
||||
menu.textProperty().bind(AppI18n.observable("addConnections"));
|
||||
menu.textProperty().bind(AppI18n.observable("new"));
|
||||
menu.setAlignment(Pos.CENTER);
|
||||
menu.setTextAlignment(TextAlignment.CENTER);
|
||||
StoreCreationMenu.addButtons(menu);
|
||||
|
@ -124,6 +129,27 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
return menu;
|
||||
}
|
||||
|
||||
private Comp<?> createBatchModeButton() {
|
||||
var batchMode = StoreViewState.get().getBatchMode();
|
||||
var b = new IconButtonComp("mdi2l-layers", () -> {
|
||||
batchMode.setValue(!batchMode.getValue());
|
||||
});
|
||||
b.apply(struc -> {
|
||||
struc.get()
|
||||
.opacityProperty()
|
||||
.bind(Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
if (batchMode.getValue()) {
|
||||
return 1.0;
|
||||
}
|
||||
return 0.4;
|
||||
},
|
||||
batchMode));
|
||||
struc.get().getStyleClass().remove(Styles.FLAT);
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
private Comp<?> createAlphabeticalSortButton() {
|
||||
var sortMode = StoreViewState.get().getSortMode();
|
||||
var icon = Bindings.createObjectBinding(
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.augment.ContextMenuAugment;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class StoreEntryListStatusBarComp extends SimpleComp {
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var checkbox = new StoreEntryBatchSelectComp(StoreViewState.get().getCurrentTopLevelSection());
|
||||
var l = new LabelComp(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return AppI18n.get(
|
||||
"connectionsSelected",
|
||||
StoreViewState.get()
|
||||
.getEffectiveBatchModeSelection()
|
||||
.getList()
|
||||
.size());
|
||||
},
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList(),
|
||||
AppI18n.activeLanguage()));
|
||||
l.minWidth(Region.USE_PREF_SIZE);
|
||||
l.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
});
|
||||
var actions = new ToolbarComp(createActions());
|
||||
var close = new IconButtonComp("mdi2c-close", () -> {
|
||||
StoreViewState.get().getBatchMode().setValue(false);
|
||||
});
|
||||
close.apply(struc -> {
|
||||
struc.get().getStyleClass().remove(Styles.FLAT);
|
||||
struc.get().minWidthProperty().bind(struc.get().heightProperty());
|
||||
struc.get().prefWidthProperty().bind(struc.get().heightProperty());
|
||||
struc.get().maxWidthProperty().bind(struc.get().heightProperty());
|
||||
});
|
||||
var bar = new HorizontalComp(List.of(
|
||||
checkbox, Comp.hspacer(12), l, Comp.hspacer(20), actions, Comp.hspacer(), Comp.hspacer(20), close));
|
||||
bar.apply(struc -> {
|
||||
struc.get().setFillHeight(true);
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
});
|
||||
bar.minHeight(40);
|
||||
bar.prefHeight(40);
|
||||
bar.styleClass("bar");
|
||||
bar.styleClass("store-entry-list-status-bar");
|
||||
return bar.createRegion();
|
||||
}
|
||||
|
||||
private ObservableList<Comp<?>> createActions() {
|
||||
var l = new DerivedObservableList<ActionProvider>(FXCollections.observableArrayList(), true);
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList().addListener((ListChangeListener<
|
||||
? super StoreEntryWrapper>)
|
||||
c -> {
|
||||
l.setContent(getCompatibleActionProviders());
|
||||
});
|
||||
return l.<Comp<?>>mapped(actionProvider -> {
|
||||
return buildButton(actionProvider);
|
||||
})
|
||||
.getList();
|
||||
}
|
||||
|
||||
private List<ActionProvider> getCompatibleActionProviders() {
|
||||
var l = StoreViewState.get().getEffectiveBatchModeSelection().getList();
|
||||
if (l.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var all = new ArrayList<>(ActionProvider.ALL);
|
||||
for (StoreEntryWrapper w : l) {
|
||||
var actions = ActionProvider.ALL.stream()
|
||||
.filter(actionProvider -> {
|
||||
var s = actionProvider.getBatchDataStoreCallSite();
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s.getApplicableClass()
|
||||
.isAssignableFrom(w.getStore().getValue().getClass())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s.isApplicable(w.getEntry().ref())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.toList();
|
||||
all.removeIf(actionProvider -> !actions.contains(actionProvider));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends DataStore> Comp<?> buildButton(ActionProvider p) {
|
||||
ActionProvider.BatchDataStoreCallSite<T> s =
|
||||
(ActionProvider.BatchDataStoreCallSite<T>) p.getBatchDataStoreCallSite();
|
||||
if (s == null) {
|
||||
return Comp.empty();
|
||||
}
|
||||
|
||||
List<DataStoreEntryRef<T>> childrenRefs =
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList().stream()
|
||||
.map(storeEntryWrapper -> storeEntryWrapper.getEntry().<T>ref())
|
||||
.toList();
|
||||
var batchActions = s.getChildren(childrenRefs);
|
||||
var button = new ButtonComp(
|
||||
s.getName(), new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(s.getIcon())), () -> {
|
||||
if (batchActions.size() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
runActions(s);
|
||||
});
|
||||
if (batchActions.size() > 0) {
|
||||
button.apply(new ContextMenuAugment<>(
|
||||
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
|
||||
var cm = ContextMenuHelper.create();
|
||||
s.getChildren(childrenRefs).forEach(childProvider -> {
|
||||
var menu = buildMenuItemForAction(childrenRefs, childProvider);
|
||||
cm.getItems().add(menu);
|
||||
});
|
||||
return cm;
|
||||
}));
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends DataStore> MenuItem buildMenuItemForAction(List<DataStoreEntryRef<T>> batch, ActionProvider a) {
|
||||
ActionProvider.BatchDataStoreCallSite<T> s =
|
||||
(ActionProvider.BatchDataStoreCallSite<T>) a.getBatchDataStoreCallSite();
|
||||
var name = s.getName();
|
||||
var icon = s.getIcon();
|
||||
var children = s.getChildren(batch);
|
||||
if (children.size() > 0) {
|
||||
var menu = new Menu();
|
||||
menu.textProperty().bind(name);
|
||||
menu.setGraphic(new LabelGraphic.IconGraphic(icon).createGraphicNode());
|
||||
var items = children.stream()
|
||||
.filter(actionProvider -> actionProvider.getBatchDataStoreCallSite() != null)
|
||||
.map(c -> buildMenuItemForAction(batch, c))
|
||||
.toList();
|
||||
menu.getItems().addAll(items);
|
||||
return menu;
|
||||
} else {
|
||||
var item = new MenuItem();
|
||||
item.textProperty().bind(name);
|
||||
item.setGraphic(new LabelGraphic.IconGraphic(icon).createGraphicNode());
|
||||
item.setOnAction(event -> {
|
||||
runActions(s);
|
||||
event.consume();
|
||||
if (event.getTarget() instanceof Menu m) {
|
||||
m.getParentPopup().hide();
|
||||
}
|
||||
});
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends DataStore> void runActions(ActionProvider.BatchDataStoreCallSite<?> s) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var l = new ArrayList<>(
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList());
|
||||
var mapped = l.stream().map(w -> w.getEntry().<T>ref()).toList();
|
||||
var action = ((ActionProvider.BatchDataStoreCallSite<T>) s).createAction(mapped);
|
||||
if (action != null) {
|
||||
action.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -120,7 +120,7 @@ public class StoreEntryWrapper {
|
|||
}
|
||||
|
||||
public void editDialog() {
|
||||
StoreCreationComp.showEdit(entry);
|
||||
StoreCreationDialog.showEdit(entry);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.comp.SimpleComp;
|
|||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
@ -54,6 +55,10 @@ public class StoreIconComp extends SimpleComp {
|
|||
|
||||
stack.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (wrapper.getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
|
||||
return;
|
||||
}
|
||||
|
||||
StoreIconChoiceDialog.show(wrapper.getEntry());
|
||||
event.consume();
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
|
|||
var prov = canSync
|
||||
? DataStoreProviders.byId("syncedIdentity").orElseThrow()
|
||||
: DataStoreProviders.byId("localIdentity").orElseThrow();
|
||||
StoreCreationComp.showCreation(prov, DataStoreCreationCategory.IDENTITY);
|
||||
StoreCreationDialog.showCreation(prov, DataStoreCreationCategory.IDENTITY);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ public class StoreLayoutComp extends SimpleComp {
|
|||
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
|
||||
})
|
||||
.createStructure();
|
||||
struc.getLeft().setMinWidth(260);
|
||||
struc.getLeft().setMinWidth(270);
|
||||
struc.getLeft().setMaxWidth(500);
|
||||
struc.get().getStyleClass().add("store-layout");
|
||||
InputHelper.onKeyCombination(
|
||||
|
|
|
@ -84,7 +84,7 @@ public class StoreSectionComp extends StoreSectionBaseComp {
|
|||
|
||||
var full = new VerticalComp(List.of(
|
||||
topEntryList,
|
||||
Comp.separator().hide(Bindings.not(effectiveExpanded)),
|
||||
Comp.hseparator().hide(Bindings.not(effectiveExpanded)),
|
||||
content));
|
||||
full.styleClass("store-entry-section-comp");
|
||||
full.apply(struc -> {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.ext.DataStoreUsageCategory;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
|
@ -13,6 +14,7 @@ import io.xpipe.app.util.PlatformThread;
|
|||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
@ -42,6 +44,27 @@ public class StoreViewState {
|
|||
@Getter
|
||||
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
|
||||
|
||||
@Getter
|
||||
private final BooleanProperty batchMode = new SimpleBooleanProperty(true);
|
||||
|
||||
@Getter
|
||||
private final DerivedObservableList<StoreEntryWrapper> batchModeSelection =
|
||||
new DerivedObservableList<>(FXCollections.observableArrayList(), true);
|
||||
|
||||
@Getter
|
||||
private final DerivedObservableList<StoreEntryWrapper> effectiveBatchModeSelection =
|
||||
batchModeSelection.filtered(storeEntryWrapper -> {
|
||||
if (!storeEntryWrapper.getValidity().getValue().isUsable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storeEntryWrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@Getter
|
||||
private StoreSection currentTopLevelSection;
|
||||
|
||||
|
@ -60,6 +83,7 @@ public class StoreViewState {
|
|||
INSTANCE.initSections();
|
||||
INSTANCE.updateContent();
|
||||
INSTANCE.initFilterListener();
|
||||
INSTANCE.initBatchListener();
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
|
@ -80,6 +104,42 @@ public class StoreViewState {
|
|||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void selectBatchMode(StoreSection section) {
|
||||
var wrapper = section.getWrapper();
|
||||
if (wrapper != null && !batchModeSelection.getList().contains(wrapper)) {
|
||||
batchModeSelection.getList().add(wrapper);
|
||||
}
|
||||
if (wrapper == null
|
||||
|| (wrapper.getValidity().getValue().isUsable()
|
||||
&& wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
|
||||
section.getShownChildren().getList().forEach(c -> selectBatchMode(c));
|
||||
}
|
||||
}
|
||||
|
||||
public void unselectBatchMode(StoreSection section) {
|
||||
var wrapper = section.getWrapper();
|
||||
if (wrapper != null) {
|
||||
batchModeSelection.getList().remove(wrapper);
|
||||
}
|
||||
if (wrapper == null
|
||||
|| (wrapper.getValidity().getValue().isUsable()
|
||||
&& wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
|
||||
section.getShownChildren().getList().forEach(c -> unselectBatchMode(c));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSectionSelected(StoreSection section) {
|
||||
if (section.getWrapper() == null) {
|
||||
var batchSet = new HashSet<>(batchModeSelection.getList());
|
||||
var childSet = section.getShownChildren().getList().stream()
|
||||
.map(s -> s.getWrapper())
|
||||
.toList();
|
||||
return batchSet.containsAll(childSet);
|
||||
}
|
||||
|
||||
return getBatchModeSelection().getList().contains(section.getWrapper());
|
||||
}
|
||||
|
||||
private void updateContent() {
|
||||
categories.getList().forEach(c -> c.update());
|
||||
allEntries.getList().forEach(e -> e.update());
|
||||
|
@ -115,6 +175,14 @@ public class StoreViewState {
|
|||
});
|
||||
}
|
||||
|
||||
private void initBatchListener() {
|
||||
allEntries.getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
|
||||
batchModeSelection.getList().removeIf(storeEntryWrapper -> {
|
||||
return allEntries.getList().contains(storeEntryWrapper);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void initContent() {
|
||||
allEntries
|
||||
.getList()
|
||||
|
|
|
@ -12,6 +12,8 @@ import io.xpipe.app.util.LicenseProvider;
|
|||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
@ -19,6 +21,7 @@ import javafx.scene.input.KeyCombination;
|
|||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -35,10 +38,13 @@ public class AppLayoutModel {
|
|||
|
||||
private final Property<Entry> selected;
|
||||
|
||||
private final ObservableList<QueueEntry> queueEntries;
|
||||
|
||||
public AppLayoutModel(SavedState savedState) {
|
||||
this.savedState = savedState;
|
||||
this.entries = createEntryList();
|
||||
this.selected = new SimpleObjectProperty<>(entries.getFirst());
|
||||
this.queueEntries = FXCollections.observableArrayList();
|
||||
}
|
||||
|
||||
public static AppLayoutModel get() {
|
||||
|
@ -46,7 +52,7 @@ public class AppLayoutModel {
|
|||
}
|
||||
|
||||
public static void init() {
|
||||
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(260, 300));
|
||||
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(270, 300));
|
||||
INSTANCE = new AppLayoutModel(state);
|
||||
}
|
||||
|
||||
|
@ -121,18 +127,18 @@ public class AppLayoutModel {
|
|||
// "http://localhost:" + AppBeaconServer.get().getPort()),
|
||||
// null),
|
||||
new Entry(
|
||||
AppI18n.observable("documentation"),
|
||||
AppI18n.observable("docs"),
|
||||
new LabelGraphic.IconGraphic("mdi2b-book-open-variant"),
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.DOCS),
|
||||
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));
|
||||
AppI18n.observable("webtop"),
|
||||
new LabelGraphic.IconGraphic("mdi2d-desktop-mac"),
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
|
||||
null));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
@ -152,4 +158,12 @@ public class AppLayoutModel {
|
|||
Comp<?> comp,
|
||||
Runnable action,
|
||||
KeyCombination combination) {}
|
||||
|
||||
@Value
|
||||
public static class QueueEntry {
|
||||
|
||||
ObservableValue<String> name;
|
||||
LabelGraphic icon;
|
||||
Runnable action;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import io.xpipe.app.ext.ActionProvider;
|
|||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
|
@ -133,7 +134,7 @@ public class AppOpenArguments {
|
|||
var dir = Files.isDirectory(file) ? file : file.getParent();
|
||||
AppLayoutModel.get().selectBrowser();
|
||||
BrowserFullSessionModel.DEFAULT.openFileSystemAsync(
|
||||
DataStorage.get().local().ref(), model -> dir.toString(), null);
|
||||
DataStorage.get().local().ref(), model -> FilePath.of(dir.toString()), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,9 @@ public class AppTheme {
|
|||
}
|
||||
|
||||
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
|
||||
TrackEvent.withTrace("Platform preference changed").tag("change", change.toString()).handle();
|
||||
TrackEvent.withTrace("Platform preference changed")
|
||||
.tag("change", change.toString())
|
||||
.handle();
|
||||
});
|
||||
|
||||
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
|
||||
|
@ -140,10 +142,10 @@ public class AppTheme {
|
|||
|
||||
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");
|
||||
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) {
|
||||
|
@ -172,8 +174,7 @@ public class AppTheme {
|
|||
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
|
||||
}
|
||||
|
||||
if (colorScheme != ColorScheme.DARK
|
||||
&& AppPrefs.get().theme().getValue().isDark()) {
|
||||
if (colorScheme != ColorScheme.DARK && AppPrefs.get().theme().getValue().isDark()) {
|
||||
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
|
||||
}
|
||||
}
|
||||
|
|
88
app/src/main/java/io/xpipe/app/core/AppWindowsShutdown.java
Normal file
88
app/src/main/java/io/xpipe/app/core/AppWindowsShutdown.java
Normal file
|
@ -0,0 +1,88 @@
|
|||
package io.xpipe.app.core;
|
||||
|
||||
import com.sun.jna.*;
|
||||
import com.sun.jna.platform.win32.User32;
|
||||
import com.sun.jna.platform.win32.WinDef;
|
||||
import com.sun.jna.platform.win32.WinUser;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AppWindowsShutdown {
|
||||
|
||||
// Prevent GC
|
||||
private static final WinShutdownHookProc PROC = new WinShutdownHookProc();
|
||||
|
||||
public static void registerHook(WinDef.HWND hwnd) {
|
||||
int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);
|
||||
if (windowThreadID == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PROC.hwnd = hwnd;
|
||||
PROC.hhook = User32.INSTANCE.SetWindowsHookEx(4, PROC, null, windowThreadID);
|
||||
}
|
||||
|
||||
public static class CWPSSTRUCT extends Structure {
|
||||
public WinDef.LPARAM lParam;
|
||||
public WinDef.WPARAM wParam;
|
||||
public WinDef.DWORD message;
|
||||
public WinDef.HWND hwnd;
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return List.of("lParam", "wParam", "message", "hwnd");
|
||||
}
|
||||
}
|
||||
|
||||
public interface WinHookProc extends WinUser.HOOKPROC {
|
||||
|
||||
WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct);
|
||||
}
|
||||
|
||||
public static final int WM_ENDSESSION = 0x16;
|
||||
public static final int WM_QUERYENDSESSION = 0x11;
|
||||
public static final long ENDSESSION_CRITICAL = 0x40000000L;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public static final class WinShutdownHookProc implements WinHookProc {
|
||||
|
||||
@Setter
|
||||
private WinUser.HHOOK hhook;
|
||||
@Setter
|
||||
private WinDef.HWND hwnd;
|
||||
|
||||
@Override
|
||||
public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct) {
|
||||
if (nCode >= 0 && hookProcStruct.hwnd.equals(hwnd)) {
|
||||
if (hookProcStruct.message.longValue() == WM_QUERYENDSESSION) {
|
||||
// Indicates that we need to run the endsession case blocking
|
||||
return new WinDef.LRESULT(0);
|
||||
}
|
||||
|
||||
if (hookProcStruct.message.longValue() == WM_ENDSESSION) {
|
||||
// Instant exit for critical shutdowns
|
||||
if (hookProcStruct.lParam.longValue() == ENDSESSION_CRITICAL) {
|
||||
OperationMode.halt(0);
|
||||
}
|
||||
|
||||
// A shutdown hook will be started in parallel while we exit
|
||||
// The only thing we have to do is wait for it to exit the platform
|
||||
while (PlatformState.getCurrent() != PlatformState.EXITED) {
|
||||
ThreadHelper.sleep(100);
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
}
|
||||
|
||||
return new WinDef.LRESULT(0);
|
||||
}
|
||||
}
|
||||
return User32.INSTANCE.CallNextHookEx(hhook, nCode, wParam, new WinDef.LPARAM(Pointer.nativeValue(hookProcStruct.getPointer())));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ public class AppJavaOptionsCheck {
|
|||
.formatted(env)
|
||||
+ " This will forcefully apply all custom JVM options to XPipe and can cause a variety of different issues."
|
||||
+ " Please remove this global environment variable and use local configuration instead for your other JVM programs.")
|
||||
.noDefaultActions()
|
||||
.expected()
|
||||
.handle();
|
||||
AppCache.update("javaOptionsWarningShown", true);
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.core.check;
|
|||
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
|
@ -27,7 +28,7 @@ public class AppRosettaCheck {
|
|||
ErrorEvent.fromMessage("You are running the Intel version of XPipe on an Apple Silicon system."
|
||||
+ " There is a native build available that comes with much better performance."
|
||||
+ " Please install that one instead.")
|
||||
.noDefaultActions()
|
||||
.documentationLink(DocumentationLink.MACOS_SETUP)
|
||||
.expected()
|
||||
.handle();
|
||||
}
|
||||
|
|
|
@ -85,9 +85,8 @@ public abstract class AppShellChecker {
|
|||
|
||||
private Optional<FailureResult> selfTestErrorCheck() {
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var scriptFile = ScriptHelper.getExecScriptFile(sc);
|
||||
var scriptContent = sc.getShellDialect().prepareScriptContent("echo test");
|
||||
sc.view().writeScriptFile(scriptFile, scriptContent);
|
||||
var scriptContent = "echo test";
|
||||
var scriptFile = ScriptHelper.createExecScript(sc, scriptContent);
|
||||
var out = sc.command(sc.getShellDialect().runScriptCommand(sc, scriptFile.toString()))
|
||||
.readStdoutOrThrow();
|
||||
if (!out.equals("test")) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.core.window.AppMainWindow;
|
|||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class GuiMode extends PlatformMode {
|
||||
|
|
|
@ -19,6 +19,7 @@ import javafx.application.Platform;
|
|||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class OperationMode {
|
||||
|
@ -34,9 +35,6 @@ public abstract class OperationMode {
|
|||
@Getter
|
||||
private static boolean inShutdown;
|
||||
|
||||
@Getter
|
||||
private static boolean inShutdownHook;
|
||||
|
||||
private static OperationMode CURRENT = null;
|
||||
|
||||
public static OperationMode map(XPipeDaemonMode mode) {
|
||||
|
@ -73,7 +71,7 @@ public abstract class OperationMode {
|
|||
}
|
||||
|
||||
TrackEvent.info("Received SIGTERM externally");
|
||||
OperationMode.shutdown(true, false);
|
||||
OperationMode.shutdown(false);
|
||||
}));
|
||||
|
||||
// Handle uncaught exceptions
|
||||
|
@ -174,7 +172,7 @@ public abstract class OperationMode {
|
|||
if (OsType.getLocal() != OsType.LINUX) {
|
||||
OperationMode.switchToSyncOrThrow(OperationMode.GUI);
|
||||
}
|
||||
OperationMode.shutdown(false, false);
|
||||
OperationMode.shutdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -256,7 +254,8 @@ 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
|
||||
+ "\" \"-Dio.xpipe.app.restarted=true\"",
|
||||
true);
|
||||
LocalShell.getShell().executeSimpleCommand(exec);
|
||||
}
|
||||
|
@ -274,7 +273,6 @@ public abstract class OperationMode {
|
|||
}
|
||||
|
||||
inShutdown = true;
|
||||
inShutdownHook = false;
|
||||
try {
|
||||
if (CURRENT != null) {
|
||||
CURRENT.finalTeardown();
|
||||
|
@ -319,35 +317,20 @@ public abstract class OperationMode {
|
|||
});
|
||||
}
|
||||
|
||||
public static void shutdown(boolean inShutdownHook, boolean hasError) {
|
||||
@SneakyThrows
|
||||
public static void shutdown(boolean hasError) {
|
||||
if (isInStartup()) {
|
||||
TrackEvent.info("Received shutdown request while in startup. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
|
||||
// In case we are stuck while in shutdown, instantly exit this application
|
||||
if (inShutdown && inShutdownHook) {
|
||||
TrackEvent.info("Received another shutdown request while in shutdown hook. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
|
||||
if (inShutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run a timer to always exit after some time in case we get stuck
|
||||
if (!hasError && !AppProperties.get().isDevelopmentEnvironment()) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ThreadHelper.sleep(25000);
|
||||
TrackEvent.info("Shutdown took too long. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
});
|
||||
}
|
||||
|
||||
TrackEvent.info("Starting shutdown ...");
|
||||
|
||||
inShutdown = true;
|
||||
OperationMode.inShutdownHook = inShutdownHook;
|
||||
// Keep a non-daemon thread running
|
||||
var thread = ThreadHelper.createPlatformThread("shutdown", false, () -> {
|
||||
try {
|
||||
|
@ -363,6 +346,14 @@ public abstract class OperationMode {
|
|||
OperationMode.halt(hasError ? 1 : 0);
|
||||
});
|
||||
thread.start();
|
||||
|
||||
// Use a timer to always exit after some time in case we get stuck
|
||||
var limit = !hasError && !AppProperties.get().isDevelopmentEnvironment() ? 25000 : Integer.MAX_VALUE;
|
||||
var exited = thread.join(Duration.ofMillis(limit));
|
||||
if (!exited) {
|
||||
TrackEvent.info("Shutdown took too long. Halting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized void set(OperationMode newMode) {
|
||||
|
@ -380,7 +371,7 @@ public abstract class OperationMode {
|
|||
|
||||
try {
|
||||
if (newMode == null) {
|
||||
shutdown(false, false);
|
||||
shutdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
public class AppDialog {
|
||||
|
||||
@Getter
|
||||
private static final ObservableList<ModalOverlay> modalOverlay = FXCollections.observableArrayList();
|
||||
private static final ObservableList<ModalOverlay> modalOverlays = FXCollections.observableArrayList();
|
||||
|
||||
private static void showMainWindow() {
|
||||
PlatformInit.init(true);
|
||||
|
@ -34,20 +34,20 @@ public class AppDialog {
|
|||
|
||||
public static void closeDialog(ModalOverlay overlay) {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
synchronized (modalOverlay) {
|
||||
modalOverlay.remove(overlay);
|
||||
synchronized (modalOverlays) {
|
||||
modalOverlays.remove(overlay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void waitForAllDialogsClose() {
|
||||
while (!modalOverlay.isEmpty()) {
|
||||
while (!modalOverlays.isEmpty()) {
|
||||
ThreadHelper.sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
private static void waitForDialogClose(ModalOverlay overlay) {
|
||||
while (modalOverlay.contains(overlay)) {
|
||||
while (modalOverlays.contains(overlay)) {
|
||||
ThreadHelper.sleep(10);
|
||||
}
|
||||
}
|
||||
|
@ -64,8 +64,8 @@ public class AppDialog {
|
|||
showMainWindow();
|
||||
if (!Platform.isFxApplicationThread()) {
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
synchronized (modalOverlay) {
|
||||
modalOverlay.add(o);
|
||||
synchronized (modalOverlays) {
|
||||
modalOverlays.add(o);
|
||||
}
|
||||
});
|
||||
if (wait) {
|
||||
|
@ -75,9 +75,9 @@ public class AppDialog {
|
|||
} else {
|
||||
var key = new Object();
|
||||
PlatformThread.runLaterIfNeededBlocking(() -> {
|
||||
synchronized (modalOverlay) {
|
||||
modalOverlay.add(o);
|
||||
modalOverlay.addListener(new ListChangeListener<>() {
|
||||
synchronized (modalOverlays) {
|
||||
modalOverlays.add(o);
|
||||
modalOverlays.addListener(new ListChangeListener<>() {
|
||||
@Override
|
||||
public void onChanged(Change<? extends ModalOverlay> c) {
|
||||
if (!c.getList().contains(o)) {
|
||||
|
@ -88,7 +88,7 @@ public class AppDialog {
|
|||
}
|
||||
});
|
||||
transition.play();
|
||||
modalOverlay.removeListener(this);
|
||||
modalOverlays.removeListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -56,6 +56,8 @@ public class AppMainWindow {
|
|||
@Getter
|
||||
private static final Property<String> loadingText = new SimpleObjectProperty<>();
|
||||
|
||||
private boolean shown = false;
|
||||
|
||||
private AppMainWindow(Stage stage) {
|
||||
this.stage = stage;
|
||||
}
|
||||
|
@ -145,9 +147,12 @@ public class AppMainWindow {
|
|||
|
||||
public void show() {
|
||||
stage.show();
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
NativeWinWindowControl.MAIN_WINDOW = new NativeWinWindowControl(stage);
|
||||
if (OsType.getLocal() == OsType.WINDOWS && !shown) {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
NativeWinWindowControl.MAIN_WINDOW = ctrl;
|
||||
AppWindowsShutdown.registerHook(ctrl.getWindowHandle());
|
||||
}
|
||||
shown = true;
|
||||
}
|
||||
|
||||
public void focus() {
|
||||
|
@ -306,7 +311,7 @@ public class AppMainWindow {
|
|||
}
|
||||
|
||||
// Close dialogs
|
||||
AppDialog.getModalOverlay().clear();
|
||||
AppDialog.getModalOverlays().clear();
|
||||
|
||||
// Close other windows
|
||||
Stage.getWindows().stream().filter(w -> !w.equals(stage)).toList().forEach(w -> w.fireEvent(e));
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
package io.xpipe.app.core.window;
|
||||
|
||||
import io.xpipe.app.core.AppLogs;
|
||||
import io.xpipe.app.core.AppWindowsShutdown;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
|
@ -14,8 +19,11 @@ import javafx.stage.StageStyle;
|
|||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
public class ModifiedStage extends Stage {
|
||||
|
||||
public static boolean mergeFrame() {
|
||||
|
@ -55,6 +63,7 @@ public class ModifiedStage extends Stage {
|
|||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static void applyModes(Stage stage) {
|
||||
if (stage.getScene() == null) {
|
||||
return;
|
||||
|
@ -71,47 +80,40 @@ public class ModifiedStage extends Stage {
|
|||
return;
|
||||
}
|
||||
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {}
|
||||
case OsType.MacOs macOs -> {
|
||||
var ctrl = new NativeMacOsWindowControl(stage);
|
||||
var seamlessFrame = AppMainWindow.getInstance() != null
|
||||
&& AppMainWindow.getInstance().getStage() == stage
|
||||
&& !AppPrefs.get().performanceMode().get()
|
||||
&& mergeFrame();
|
||||
var seamlessFrameApplied = ctrl.setAppearance(
|
||||
seamlessFrame, AppPrefs.get().theme().getValue().isDark())
|
||||
&& seamlessFrame;
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
ctrl.setWindowAttribute(
|
||||
NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
|
||||
AppPrefs.get().theme().getValue().isDark());
|
||||
boolean seamlessFrame;
|
||||
if (AppPrefs.get().performanceMode().get()
|
||||
|| !mergeFrame()
|
||||
|| AppMainWindow.getInstance() == null
|
||||
|| stage != AppMainWindow.getInstance().getStage()) {
|
||||
seamlessFrame = false;
|
||||
} else {
|
||||
// This is not available on Windows 10
|
||||
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT)
|
||||
|| SystemUtils.IS_OS_WINDOWS_10;
|
||||
try {
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
var ctrl = new NativeMacOsWindowControl(stage);
|
||||
var seamlessFrame = AppMainWindow.getInstance() != null &&
|
||||
AppMainWindow.getInstance().getStage() == stage &&
|
||||
!AppPrefs.get().performanceMode().get() &&
|
||||
mergeFrame();
|
||||
var seamlessFrameApplied = ctrl.setAppearance(seamlessFrame, AppPrefs.get().theme().getValue().isDark()) && seamlessFrame;
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
ctrl.setWindowAttribute(NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
|
||||
AppPrefs.get().theme().getValue().isDark());
|
||||
boolean seamlessFrame;
|
||||
if (AppPrefs.get().performanceMode().get() ||
|
||||
!mergeFrame() ||
|
||||
AppMainWindow.getInstance() == null ||
|
||||
stage != AppMainWindow.getInstance().getStage()) {
|
||||
seamlessFrame = false;
|
||||
} else {
|
||||
// This is not available on Windows 10
|
||||
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT) || SystemUtils.IS_OS_WINDOWS_10;
|
||||
}
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
|
||||
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
|
||||
}
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
ErrorEvent.fromThrowable(t).omit().handle();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,21 @@ import java.util.List;
|
|||
@EqualsAndHashCode
|
||||
public class NativeWinWindowControl {
|
||||
|
||||
@SneakyThrows
|
||||
public static WinDef.HWND byWindow(Window window) {
|
||||
Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
|
||||
tkStageGetter.setAccessible(true);
|
||||
Object tkStage = tkStageGetter.invoke(window);
|
||||
Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow");
|
||||
getPlatformWindow.setAccessible(true);
|
||||
Object platformWindow = getPlatformWindow.invoke(tkStage);
|
||||
Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle");
|
||||
getNativeHandle.setAccessible(true);
|
||||
Object nativeHandle = getNativeHandle.invoke(platformWindow);
|
||||
var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));
|
||||
return hwnd;
|
||||
}
|
||||
|
||||
public static List<NativeWinWindowControl> byPid(long pid) {
|
||||
var refs = new ArrayList<NativeWinWindowControl>();
|
||||
User32.INSTANCE.EnumWindows(
|
||||
|
@ -50,17 +65,7 @@ public class NativeWinWindowControl {
|
|||
|
||||
@SneakyThrows
|
||||
public NativeWinWindowControl(Window stage) {
|
||||
Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
|
||||
tkStageGetter.setAccessible(true);
|
||||
Object tkStage = tkStageGetter.invoke(stage);
|
||||
Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow");
|
||||
getPlatformWindow.setAccessible(true);
|
||||
Object platformWindow = getPlatformWindow.invoke(tkStage);
|
||||
Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle");
|
||||
getNativeHandle.setAccessible(true);
|
||||
Object nativeHandle = getNativeHandle.invoke(platformWindow);
|
||||
var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));
|
||||
this.windowHandle = hwnd;
|
||||
this.windowHandle = byWindow(stage);
|
||||
}
|
||||
|
||||
public NativeWinWindowControl(WinDef.HWND windowHandle) {
|
||||
|
|
|
@ -52,6 +52,10 @@ public interface ActionProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
default BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default DefaultDataStoreCallSite<?> getDefaultDataStoreCallSite() {
|
||||
return null;
|
||||
}
|
||||
|
@ -191,6 +195,44 @@ public interface ActionProvider {
|
|||
}
|
||||
}
|
||||
|
||||
interface BatchDataStoreCallSite<T extends DataStore> {
|
||||
|
||||
ObservableValue<String> getName();
|
||||
|
||||
String getIcon();
|
||||
|
||||
Class<?> getApplicableClass();
|
||||
|
||||
default boolean isApplicable(DataStoreEntryRef<T> o) {
|
||||
return true;
|
||||
}
|
||||
|
||||
default Action createAction(List<DataStoreEntryRef<T>> stores) {
|
||||
var individual = stores.stream()
|
||||
.map(ref -> {
|
||||
return createAction(ref);
|
||||
})
|
||||
.filter(action -> action != null)
|
||||
.toList();
|
||||
return new Action() {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
for (Action action : individual) {
|
||||
action.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default Action createAction(DataStoreEntryRef<T> store) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<T>> batch) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
class Loader implements ModuleLayerLoader {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package io.xpipe.app.ext;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
@ -28,9 +30,11 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
}
|
||||
|
||||
@Override
|
||||
public long getFileSize(String file) throws Exception {
|
||||
return Long.parseLong(
|
||||
shellControl.getShellDialect().queryFileSize(shellControl, file).readStdoutOrThrow());
|
||||
public long getFileSize(FilePath file) throws Exception {
|
||||
return Long.parseLong(shellControl
|
||||
.getShellDialect()
|
||||
.queryFileSize(shellControl, file.toString())
|
||||
.readStdoutOrThrow());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,8 +58,11 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
|
||||
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));
|
||||
var ex = new UnsupportedOperationException(
|
||||
"Shell has a PTY allocated and as a result does not support file system operations.");
|
||||
ErrorEvent.preconfigure(ErrorEvent.fromThrowable(ex)
|
||||
.documentationLink(DocumentationLink.TTY));
|
||||
throw ex;
|
||||
}
|
||||
|
||||
shellControl.checkLicenseOrThrow();
|
||||
|
@ -64,114 +71,119 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
}
|
||||
|
||||
@Override
|
||||
public InputStream openInput(String file) throws Exception {
|
||||
public InputStream openInput(FilePath file) throws Exception {
|
||||
return shellControl
|
||||
.getShellDialect()
|
||||
.getFileReadCommand(shellControl, file)
|
||||
.getFileReadCommand(shellControl, file.toString())
|
||||
.startExternalStdout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream openOutput(String file, long totalBytes) throws Exception {
|
||||
var cmd = shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file, totalBytes);
|
||||
public OutputStream openOutput(FilePath file, long totalBytes) throws Exception {
|
||||
var cmd =
|
||||
shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file.toString(), totalBytes);
|
||||
cmd.setExitTimeout(Duration.ofMillis(Long.MAX_VALUE));
|
||||
return cmd.startExternalStdin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fileExists(String file) throws Exception {
|
||||
public boolean fileExists(FilePath file) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.getShellDialect()
|
||||
.createFileExistsCommand(shellControl, file)
|
||||
.createFileExistsCommand(shellControl, file.toString())
|
||||
.start()) {
|
||||
return pc.discardAndCheckExit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String file) throws Exception {
|
||||
public void delete(FilePath file) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.getShellDialect()
|
||||
.deleteFileOrDirectory(shellControl, file)
|
||||
.deleteFileOrDirectory(shellControl, file.toString())
|
||||
.start()) {
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(String file, String newFile) throws Exception {
|
||||
public void copy(FilePath file, FilePath newFile) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.getShellDialect()
|
||||
.getFileCopyCommand(shellControl, file, newFile)
|
||||
.getFileCopyCommand(shellControl, file.toString(), newFile.toString())
|
||||
.start()) {
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(String file, String newFile) throws Exception {
|
||||
public void move(FilePath file, FilePath newFile) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.getShellDialect()
|
||||
.getFileMoveCommand(shellControl, file, newFile)
|
||||
.getFileMoveCommand(shellControl, file.toString(), newFile.toString())
|
||||
.start()) {
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mkdirs(String file) throws Exception {
|
||||
public void mkdirs(FilePath file) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.command(
|
||||
CommandBuilder.ofFunction(proc -> proc.getShellDialect().getMkdirsCommand(file)))
|
||||
CommandBuilder.ofFunction(proc -> proc.getShellDialect().getMkdirsCommand(file.toString())))
|
||||
.start()) {
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void touch(String file) throws Exception {
|
||||
public void touch(FilePath file) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.getShellDialect()
|
||||
.getFileTouchCommand(shellControl, file)
|
||||
.getFileTouchCommand(shellControl, file.toString())
|
||||
.start()) {
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void symbolicLink(String linkFile, String targetFile) throws Exception {
|
||||
public void symbolicLink(FilePath linkFile, FilePath targetFile) throws Exception {
|
||||
try (var pc = shellControl
|
||||
.getShellDialect()
|
||||
.symbolicLink(shellControl, linkFile, targetFile)
|
||||
.symbolicLink(shellControl, linkFile.toString(), targetFile.toString())
|
||||
.start()) {
|
||||
pc.discardOrThrow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean directoryExists(String file) throws Exception {
|
||||
public boolean directoryExists(FilePath file) throws Exception {
|
||||
return shellControl
|
||||
.getShellDialect()
|
||||
.directoryExists(shellControl, file)
|
||||
.directoryExists(shellControl, file.toString())
|
||||
.executeAndCheck();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void directoryAccessible(String file) throws Exception {
|
||||
public void directoryAccessible(FilePath file) throws Exception {
|
||||
var current = shellControl.executeSimpleStringCommand(
|
||||
shellControl.getShellDialect().getPrintWorkingDirectoryCommand());
|
||||
shellControl.command(shellControl.getShellDialect().getCdCommand(file));
|
||||
shellControl.command(shellControl.getShellDialect().getCdCommand(file.toString()));
|
||||
shellControl.command(shellControl.getShellDialect().getCdCommand(current));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<FileEntry> listFiles(String file) throws Exception {
|
||||
return shellControl.getShellDialect().listFiles(this, shellControl, file);
|
||||
public Stream<FileEntry> listFiles(FilePath file) throws Exception {
|
||||
return shellControl.getShellDialect().listFiles(this, shellControl, file.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listRoots() throws Exception {
|
||||
return shellControl.getShellDialect().listRoots(shellControl).toList();
|
||||
public List<FilePath> listRoots() throws Exception {
|
||||
return shellControl
|
||||
.getShellDialect()
|
||||
.listRoots(shellControl)
|
||||
.map(s -> FilePath.of(s))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,6 +19,7 @@ public class ContainerStoreState extends ShellStoreState {
|
|||
|
||||
String imageName;
|
||||
String containerState;
|
||||
Boolean shellMissing;
|
||||
|
||||
@Override
|
||||
public DataStoreState mergeCopy(DataStoreState newer) {
|
||||
|
@ -32,5 +33,6 @@ public class ContainerStoreState extends ShellStoreState {
|
|||
super.mergeBuilder(css, b);
|
||||
b.containerState(useNewer(containerState, css.getContainerState()));
|
||||
b.imageName(useNewer(imageName, css.getImageName()));
|
||||
b.shellMissing(useNewer(shellMissing, css.getShellMissing()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,18 +2,15 @@ package io.xpipe.app.ext;
|
|||
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.MarkdownComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreSectionComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.resources.AppImages;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -26,6 +23,10 @@ import java.util.UUID;
|
|||
|
||||
public interface DataStoreProvider {
|
||||
|
||||
default String getHelpLink() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default boolean canMoveCategories() {
|
||||
return true;
|
||||
}
|
||||
|
@ -109,32 +110,6 @@ public interface DataStoreProvider {
|
|||
return false;
|
||||
}
|
||||
|
||||
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
|
||||
var content = Bindings.createStringBinding(
|
||||
() -> {
|
||||
if (store.getValue() == null
|
||||
|| !store.getValue().isComplete()
|
||||
|| !getStoreClasses().contains(store.getValue().getClass())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return "## Insights\n\n" + createInsightsMarkdown(store.getValue());
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return "?";
|
||||
}
|
||||
},
|
||||
store);
|
||||
return new MarkdownComp(content, s -> s, true)
|
||||
.apply(struc -> struc.get().setPrefWidth(450))
|
||||
.apply(struc -> struc.get().setPrefHeight(250));
|
||||
}
|
||||
|
||||
default String createInsightsMarkdown(DataStore store) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default DataStoreCreationCategory getCreationCategory() {
|
||||
return null;
|
||||
}
|
||||
|
|
6
app/src/main/java/io/xpipe/app/ext/NameableStore.java
Normal file
6
app/src/main/java/io/xpipe/app/ext/NameableStore.java
Normal file
|
@ -0,0 +1,6 @@
|
|||
package io.xpipe.app.ext;
|
||||
|
||||
public interface NameableStore {
|
||||
|
||||
String getName();
|
||||
}
|
|
@ -7,6 +7,10 @@ import io.xpipe.core.store.*;
|
|||
|
||||
public interface ShellStore extends DataStore, FileSystemStore, ValidatableStore, SingletonSessionStore<ShellSession> {
|
||||
|
||||
default boolean isConnectionAttemptCostly() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default ShellControl getOrStartSession() throws Exception {
|
||||
var session = getSession();
|
||||
if (session != null) {
|
||||
|
|
|
@ -23,7 +23,6 @@ import javax.imageio.ImageIO;
|
|||
public class SystemIconCache {
|
||||
|
||||
private static enum ImageColorScheme {
|
||||
|
||||
TRANSPARENT,
|
||||
MIXED,
|
||||
LIGHT,
|
||||
|
@ -71,14 +70,17 @@ public class SystemIconCache {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (scheme != ImageColorScheme.DARK || icon.getColorSchemeData() != SystemIconSourceFile.ColorSchemeData.DEFAULT) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
@ -131,7 +133,8 @@ public class SystemIconCache {
|
|||
}
|
||||
}
|
||||
|
||||
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark) throws IOException {
|
||||
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark)
|
||||
throws IOException {
|
||||
try {
|
||||
ImageColorScheme c = null;
|
||||
for (var size : sizes) {
|
||||
|
@ -173,8 +176,8 @@ public class SystemIconCache {
|
|||
return image;
|
||||
}
|
||||
|
||||
|
||||
private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image) throws IOException {
|
||||
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;
|
||||
|
@ -184,12 +187,12 @@ public class SystemIconCache {
|
|||
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());
|
||||
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;
|
||||
|
@ -201,11 +204,11 @@ public class SystemIconCache {
|
|||
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;
|
||||
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;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
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.process.ProcessOutputException;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
|
@ -91,6 +94,13 @@ public interface SystemIconSource {
|
|||
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;
|
||||
}
|
||||
|
||||
var dir = SystemIconManager.getPoolPath().resolve(id);
|
||||
if (!Files.exists(dir)) {
|
||||
sc.command(CommandBuilder.of()
|
||||
|
@ -100,7 +110,7 @@ public interface SystemIconSource {
|
|||
.execute();
|
||||
} else {
|
||||
sc.command(CommandBuilder.of().add("git", "pull"))
|
||||
.withWorkingDirectory(dir.toString())
|
||||
.withWorkingDirectory(FilePath.of(dir))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,11 +35,12 @@ public class SystemIconSourceData {
|
|||
.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();
|
||||
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);
|
||||
|
@ -51,33 +52,40 @@ public class SystemIconSourceData {
|
|||
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));
|
||||
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));
|
||||
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));
|
||||
sourceFiles.add(new SystemIconSourceFile(
|
||||
source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
if (hasDarkModeVariant) {
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
|
||||
sourceFiles.add(new SystemIconSourceFile(
|
||||
source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
|
||||
}
|
||||
} else {
|
||||
if (hasDarkModeVariant) {
|
||||
sourceFiles.add(
|
||||
new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
sourceFiles.add(new SystemIconSourceFile(
|
||||
source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
sourceFiles.add(new SystemIconSourceFile(
|
||||
source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package io.xpipe.app.issue;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
|
||||
public interface ErrorAction {
|
||||
|
||||
static ErrorAction openDocumentation(String link) {
|
||||
static ErrorAction openDocumentation(DocumentationLink link) {
|
||||
return new ErrorAction() {
|
||||
@Override
|
||||
public String getName() {
|
||||
|
@ -19,53 +20,12 @@ public interface ErrorAction {
|
|||
|
||||
@Override
|
||||
public boolean handle(ErrorEvent event) {
|
||||
Hyperlinks.open(link);
|
||||
link.open();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static ErrorAction reportOnGithub() {
|
||||
return new ErrorAction() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppI18n.get("reportOnGithub");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return AppI18n.get("reportOnGithubDescription");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(ErrorEvent event) {
|
||||
var url = "https://github.com/xpipe-io/xpipe/issues/new";
|
||||
Hyperlinks.open(url);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static ErrorAction automaticallyReport() {
|
||||
return new ErrorAction() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return AppI18n.get("reportError");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return AppI18n.get("reportErrorDescription");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(ErrorEvent event) {
|
||||
UserReportComp.show(event);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static ErrorAction ignore() {
|
||||
return new ErrorAction() {
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.xpipe.app.issue;
|
||||
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
@ -23,9 +24,6 @@ public class ErrorEvent {
|
|||
@Builder.Default
|
||||
private final boolean reportable = true;
|
||||
|
||||
@Setter
|
||||
private boolean disableDefaultActions;
|
||||
|
||||
private final Throwable throwable;
|
||||
|
||||
@Singular
|
||||
|
@ -43,6 +41,8 @@ public class ErrorEvent {
|
|||
@Singular
|
||||
private List<Path> attachments;
|
||||
|
||||
private DocumentationLink documentationLink;
|
||||
|
||||
private String email;
|
||||
private String userReport;
|
||||
private boolean unhandled;
|
||||
|
@ -162,10 +162,6 @@ public class ErrorEvent {
|
|||
return omit().expected();
|
||||
}
|
||||
|
||||
public ErrorEventBuilder noDefaultActions() {
|
||||
return disableDefaultActions(true);
|
||||
}
|
||||
|
||||
public void handle() {
|
||||
build().handle();
|
||||
}
|
||||
|
|
|
@ -66,22 +66,6 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
return b.createRegion();
|
||||
}
|
||||
|
||||
private Region createDetails() {
|
||||
var content = new ErrorDetailsComp(event).prefWidth(600).prefHeight(750);
|
||||
var modal = ModalOverlay.of("errorDetails", content);
|
||||
var button = new ButtonComp(
|
||||
null,
|
||||
new SimpleObjectProperty<>(new LabelGraphic.NodeGraphic(() -> {
|
||||
return createActionButtonGraphic(AppI18n.get("showDetails"), AppI18n.get("showDetailsDescription"));
|
||||
})),
|
||||
() -> {
|
||||
modal.show();
|
||||
});
|
||||
var r = button.grow(true, false).createRegion();
|
||||
r.getStyleClass().add("details");
|
||||
return r;
|
||||
}
|
||||
|
||||
private Region createTop() {
|
||||
var desc = event.getDescription();
|
||||
if (desc == null && event.getThrowable() != null) {
|
||||
|
@ -112,10 +96,10 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
@Override
|
||||
protected Region createSimple() {
|
||||
var top = createTop();
|
||||
var content = new VBox(top, new Separator(Orientation.HORIZONTAL));
|
||||
var content = new VBox(top);
|
||||
var header = new Label(AppI18n.get("possibleActions"));
|
||||
AppFontSizes.xl(header);
|
||||
var actionBox = new VBox(header);
|
||||
var actionBox = new VBox();
|
||||
actionBox.getStyleClass().add("actions");
|
||||
actionBox.setFillWidth(true);
|
||||
|
||||
|
@ -137,7 +121,6 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
event.setDisableDefaultActions(true);
|
||||
}
|
||||
|
||||
var custom = event.getCustomActions();
|
||||
|
@ -147,21 +130,17 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
actionBox.getChildren().add(ac);
|
||||
}
|
||||
|
||||
if (!event.isDisableDefaultActions()) {
|
||||
for (var action :
|
||||
List.of(ErrorAction.automaticallyReport(), ErrorAction.reportOnGithub(), ErrorAction.ignore())) {
|
||||
var ac = createActionComp(action);
|
||||
actionBox.getChildren().add(ac);
|
||||
}
|
||||
} else if (event.getCustomActions().isEmpty()) {
|
||||
for (var action : List.of(ErrorAction.ignore())) {
|
||||
var ac = createActionComp(action);
|
||||
actionBox.getChildren().add(ac);
|
||||
}
|
||||
if (event.getDocumentationLink() != null) {
|
||||
actionBox.getChildren().add(createActionComp(ErrorAction.openDocumentation(event.getDocumentationLink())));
|
||||
}
|
||||
|
||||
if (actionBox.getChildren().size() > 0) {
|
||||
actionBox.getChildren().addFirst(header);
|
||||
content.getChildren().add(new Separator(Orientation.HORIZONTAL));
|
||||
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED);
|
||||
content.getChildren().addAll(actionBox);
|
||||
}
|
||||
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
|
||||
|
||||
content.getChildren().addAll(actionBox);
|
||||
content.getStyleClass().add("top");
|
||||
content.setFillWidth(true);
|
||||
content.setMinHeight(Region.USE_PREF_SIZE);
|
||||
|
@ -170,13 +149,6 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
layout.getChildren().add(content);
|
||||
layout.getStyleClass().add("error-handler-comp");
|
||||
|
||||
if (event.getThrowable() != null) {
|
||||
content.getChildren().add(new Separator(Orientation.HORIZONTAL));
|
||||
var details = createDetails();
|
||||
layout.getChildren().add(details);
|
||||
layout.prefHeightProperty().bind(content.heightProperty().add(65).add(details.prefHeightProperty()));
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.xpipe.app.issue;
|
||||
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
|
@ -44,13 +45,25 @@ public class ErrorHandlerDialog {
|
|||
var comp = new ErrorHandlerComp(event, () -> {
|
||||
AppDialog.closeDialog(modal.get());
|
||||
});
|
||||
comp.prefWidth(550);
|
||||
comp.prefWidth(500);
|
||||
var headerId = event.isTerminal() ? "terminalErrorOccured" : "errorOccured";
|
||||
modal.set(ModalOverlay.of(headerId, comp, new LabelGraphic.NodeGraphic(() -> {
|
||||
var errorModal = ModalOverlay.of(headerId, comp, new LabelGraphic.NodeGraphic(() -> {
|
||||
var graphic = new FontIcon("mdomz-warning");
|
||||
graphic.setIconColor(Color.RED);
|
||||
return graphic;
|
||||
})));
|
||||
}));
|
||||
if (event.getThrowable() != null && event.isReportable()) {
|
||||
errorModal.addButton(new ModalButton("stackTrace", () -> {
|
||||
var content = new ErrorDetailsComp(event).prefWidth(600).prefHeight(750);
|
||||
var detailsModal = ModalOverlay.of("errorDetails", content);
|
||||
detailsModal.show();
|
||||
}, false, false));
|
||||
}
|
||||
errorModal.addButton(new ModalButton("report", () -> {
|
||||
UserReportComp.show(event);
|
||||
}, false, false));
|
||||
errorModal.addButton(ModalButton.ok());
|
||||
modal.set(errorModal);
|
||||
AppDialog.showAndWait(modal.get());
|
||||
if (comp.getTakenAction().getValue() == null) {
|
||||
ErrorAction.ignore().handle(event);
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.xpipe.app.comp.base.VerticalComp;
|
|||
import io.xpipe.app.core.AppDistributionType;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.JfxHelper;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
|
@ -32,10 +33,11 @@ public class AboutCategory extends AppPrefsCategory {
|
|||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new TileButtonComp("documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS);
|
||||
e.consume();
|
||||
})
|
||||
new TileButtonComp(
|
||||
"documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS);
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
|
@ -47,23 +49,23 @@ public class AboutCategory extends AppPrefsCategory {
|
|||
null)
|
||||
.addComp(
|
||||
new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
|
||||
Hyperlinks.open(Hyperlinks.DOCS_PRIVACY);
|
||||
DocumentationLink.PRIVACY.open();
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
null)
|
||||
.addComp(
|
||||
new TileButtonComp("thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> {
|
||||
var comp = new ThirdPartyDependencyListComp()
|
||||
.prefWidth(650)
|
||||
.styleClass("open-source-notices");
|
||||
var modal = ModalOverlay.of("openSourceNotices", comp);
|
||||
modal.show();
|
||||
})
|
||||
.grow(true, false))
|
||||
var comp = new ThirdPartyDependencyListComp()
|
||||
.prefWidth(650)
|
||||
.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);
|
||||
DocumentationLink.EULA.open();
|
||||
e.consume();
|
||||
})
|
||||
.grow(true, false),
|
||||
|
@ -80,7 +82,7 @@ public class AboutCategory extends AppPrefsCategory {
|
|||
protected Comp<?> create() {
|
||||
var props = createProperties().padding(new Insets(0, 0, 0, 5));
|
||||
var update = new UpdateCheckComp().grow(true, false);
|
||||
return new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks()))
|
||||
return new VerticalComp(List.of(props, Comp.hseparator(), update, Comp.hseparator(), createLinks()))
|
||||
.apply(s -> s.get().setFillWidth(true))
|
||||
.apply(struc -> struc.get().setSpacing(15))
|
||||
.styleClass("information")
|
||||
|
|
|
@ -131,6 +131,8 @@ public class AppPrefs {
|
|||
mapLocal(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class, false);
|
||||
final BooleanProperty checkForSecurityUpdates =
|
||||
mapLocal(new SimpleBooleanProperty(true), "checkForSecurityUpdates", Boolean.class, false);
|
||||
final BooleanProperty disableApiHttpsTlsCheck =
|
||||
mapLocal(new SimpleBooleanProperty(false), "disableApiHttpsTlsCheck", Boolean.class, false);
|
||||
final BooleanProperty condenseConnectionDisplay =
|
||||
mapLocal(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class, false);
|
||||
final BooleanProperty showChildCategoriesInParentCategory =
|
||||
|
|
|
@ -10,7 +10,7 @@ public enum CloseBehaviour implements PrefsChoiceValue {
|
|||
QUIT("app.quit") {
|
||||
@Override
|
||||
public void run() {
|
||||
OperationMode.shutdown(false, false);
|
||||
OperationMode.shutdown(false);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -29,8 +29,7 @@ public class ConnectionsCategory extends AppPrefsCategory {
|
|||
.sub(new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell));
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
options.addTitle("sshConfiguration")
|
||||
.sub(new OptionsBuilder()
|
||||
.addComp(prefs.getCustomComp("x11WslInstance")));
|
||||
.sub(new OptionsBuilder().addComp(prefs.getCustomComp("x11WslInstance")));
|
||||
}
|
||||
return options.buildComp();
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ package io.xpipe.app.prefs;
|
|||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExternalApplicationHelper {
|
||||
|
||||
|
@ -21,17 +21,32 @@ public class ExternalApplicationHelper {
|
|||
}
|
||||
|
||||
public static void startAsync(String raw) throws Exception {
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
if (ShellDialects.isPowershell(sc)) {
|
||||
// Do the best effort here
|
||||
// This does not respect quoting rules, but otherwise powershell wouldn't work at all
|
||||
var split = raw.split("\\s+");
|
||||
var splitBuilder = CommandBuilder.of().addAll(Arrays.asList(split));
|
||||
startAsync(splitBuilder);
|
||||
} else {
|
||||
startAsync(CommandBuilder.ofString(raw));
|
||||
}
|
||||
if (raw == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
raw = raw.trim();
|
||||
var split = Arrays.asList(raw.split("\\s+"));
|
||||
if (split.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String exec;
|
||||
String args;
|
||||
if (raw.startsWith("\"")) {
|
||||
var end = raw.substring(1).indexOf("\"");
|
||||
if (end == -1) {
|
||||
return;
|
||||
}
|
||||
end++;
|
||||
exec = raw.substring(1, end);
|
||||
args = raw.substring(end + 1).trim();
|
||||
} else {
|
||||
exec = split.getFirst();
|
||||
args = split.stream().skip(1).collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
startAsync(CommandBuilder.of().addFile(exec).add(args));
|
||||
}
|
||||
|
||||
public static void startAsync(CommandBuilder b) throws Exception {
|
||||
|
|
|
@ -133,7 +133,7 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
|
|||
try (var sc = LocalShell.getShell().start()) {
|
||||
var out = CommandSupport.findProgram(sc, executable);
|
||||
if (out.isPresent()) {
|
||||
return out.map(Path::of);
|
||||
return out.map(filePath -> Path.of(filePath.toString()));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).omit().handle();
|
||||
|
|
|
@ -187,12 +187,36 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
ExternalEditorType PYCHARM = new GenericPathType("app.pycharm", "pycharm", false);
|
||||
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<LinuxPathType> LINUX_EDITORS =
|
||||
List.of(ExternalEditorType.WINDSURF_LINUX, 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<ExternalEditorType> WINDOWS_EDITORS = List.of(
|
||||
CURSOR_WINDOWS,
|
||||
WINDSURF_WINDOWS,
|
||||
TRAE_WINDOWS,
|
||||
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<ExternalEditorType> MACOS_EDITORS = List.of(
|
||||
CURSOR_MACOS,
|
||||
WINDSURF_MACOS,
|
||||
TRAE_MACOS,
|
||||
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,16 +10,15 @@ 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.*;
|
||||
import java.util.function.Supplier;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public interface ExternalRdpClientType extends PrefsChoiceValue {
|
||||
|
||||
|
@ -126,7 +125,12 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
|
||||
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
|
||||
launch(configuration.getTitle(), CommandBuilder.of().addFile(file.toString()).add("/cert-ignore").add("/p:'" + escapedPw + "'"));
|
||||
launch(
|
||||
configuration.getTitle(),
|
||||
CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
.add("/cert-ignore")
|
||||
.add("/p:'" + escapedPw + "'"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -293,7 +297,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
.add(ExternalApplicationHelper.replaceFileArgument(
|
||||
format,
|
||||
"FILE",
|
||||
writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()).toString())));
|
||||
writeRdpConfigFile(configuration.getTitle(), configuration.getConfig())
|
||||
.toString())));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -307,9 +312,11 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
}
|
||||
}
|
||||
|
||||
class RemminaRdpType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType {
|
||||
class RemminaRdpType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType {
|
||||
|
||||
public RemminaRdpType() {super("app.remmina", "remmina", true);}
|
||||
public RemminaRdpType() {
|
||||
super("app.remmina", "remmina", true);
|
||||
}
|
||||
|
||||
private List<String> toStrip() {
|
||||
return List.of("auto connect", "password 51", "prompt for credentials", "smart sizing");
|
||||
|
@ -324,7 +331,9 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
var encrypted = encryptPassword(configuration.getPassword());
|
||||
if (encrypted.isPresent()) {
|
||||
var file = writeRemminaConfigFile(configuration, encrypted.get());
|
||||
launch(configuration.getTitle(), CommandBuilder.of().add("-c").addFile(file.toString()));
|
||||
launch(
|
||||
configuration.getTitle(),
|
||||
CommandBuilder.of().add("-c").addFile(file.toString()));
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
ThreadHelper.sleep(5000);
|
||||
FileUtils.deleteQuietly(file.toFile());
|
||||
|
@ -343,7 +352,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref").readStdoutIfPossible();
|
||||
var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref")
|
||||
.readStdoutIfPossible();
|
||||
if (prefSecretBase64.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
@ -367,7 +377,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
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 = """
|
||||
var string =
|
||||
"""
|
||||
[remmina]
|
||||
protocol=RDP
|
||||
name=%s
|
||||
|
@ -375,11 +386,20 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
server=%s
|
||||
password=%s
|
||||
cert_ignore=1
|
||||
""".formatted(configuration.getTitle(),
|
||||
configuration.getConfig().get("username").orElseThrow().getValue(),
|
||||
configuration.getConfig().get("full address").orElseThrow().getValue(),
|
||||
password
|
||||
);
|
||||
"""
|
||||
.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();
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.xpipe.app.icon.SystemIconManager;
|
|||
import io.xpipe.app.icon.SystemIconSource;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -15,7 +16,6 @@ import javafx.beans.property.SimpleStringProperty;
|
|||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
@ -96,7 +96,7 @@ public class IconsCategory extends AppPrefsCategory {
|
|||
|
||||
var addDirectoryButton = new TileButtonComp(
|
||||
"addDirectoryIconSource", "addDirectoryIconSourceDescription", "mdi2f-folder-plus", e -> {
|
||||
var dir = new SimpleStringProperty();
|
||||
var dir = new SimpleObjectProperty<FilePath>();
|
||||
var modal = ModalOverlay.of(
|
||||
"iconDirectory",
|
||||
new ContextualFileReferenceChoiceComp(
|
||||
|
@ -107,12 +107,12 @@ public class IconsCategory extends AppPrefsCategory {
|
|||
List.of())
|
||||
.prefWidth(350));
|
||||
modal.withDefaultButtons(() -> {
|
||||
if (dir.get() == null || dir.get().isBlank()) {
|
||||
if (dir.get() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var source = SystemIconSource.Directory.builder()
|
||||
.path(Path.of(dir.get()))
|
||||
.path(dir.get().asLocalPath())
|
||||
.id(UUID.randomUUID().toString())
|
||||
.build();
|
||||
if (!sources.contains(source)) {
|
||||
|
@ -131,9 +131,9 @@ public class IconsCategory extends AppPrefsCategory {
|
|||
var vbox = new VerticalComp(List.of(
|
||||
Comp.vspacer(10),
|
||||
box,
|
||||
Comp.separator(),
|
||||
Comp.hseparator(),
|
||||
refreshButton,
|
||||
Comp.separator(),
|
||||
Comp.hseparator(),
|
||||
addDirectoryButton,
|
||||
addGitButton));
|
||||
vbox.spacing(10);
|
||||
|
|
|
@ -92,7 +92,7 @@ public class PasswordManagerCategory extends AppPrefsCategory {
|
|||
|
||||
var docsLinkProperty = new SimpleStringProperty();
|
||||
var docsLinkButton =
|
||||
new ButtonComp(AppI18n.observable("documentation"), new FontIcon("mdi2h-help-circle-outline"), () -> {
|
||||
new ButtonComp(AppI18n.observable("docs"), new FontIcon("mdi2h-help-circle-outline"), () -> {
|
||||
var l = docsLinkProperty.get();
|
||||
if (l != null) {
|
||||
Hyperlinks.open(l);
|
||||
|
|
|
@ -30,7 +30,9 @@ public class SecurityCategory extends AppPrefsCategory {
|
|||
.pref(prefs.dontAutomaticallyStartVmSshServer)
|
||||
.addToggle(prefs.dontAutomaticallyStartVmSshServer)
|
||||
.pref(prefs.disableTerminalRemotePasswordPreparation)
|
||||
.addToggle(prefs.disableTerminalRemotePasswordPreparation));
|
||||
.addToggle(prefs.disableTerminalRemotePasswordPreparation)
|
||||
.pref(prefs.disableApiHttpsTlsCheck)
|
||||
.addToggle(prefs.disableApiHttpsTlsCheck));
|
||||
return builder.buildComp();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@ package io.xpipe.app.prefs;
|
|||
import io.xpipe.app.comp.Comp;
|
||||
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.DocumentationLink;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
|
@ -27,14 +28,6 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
return "vaultSync";
|
||||
}
|
||||
|
||||
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() {
|
||||
var prefs = AppPrefs.get();
|
||||
AtomicReference<Region> button = new AtomicReference<>();
|
||||
|
@ -61,7 +54,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"), () -> {
|
||||
showHelpAlert();
|
||||
DocumentationLink.SYNC.open();
|
||||
});
|
||||
var remoteRow = new HorizontalComp(List.of(remoteRepo, helpButton)).spacing(10);
|
||||
remoteRow.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue