mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Compare commits
35 commits
82003734b5
...
4c7f91fec9
Author | SHA1 | Date | |
---|---|---|---|
|
4c7f91fec9 | ||
|
8d6eb1051c | ||
|
33e75fec2a | ||
|
e4f5738fa5 | ||
|
1cabd5b93d | ||
|
cb9145dd37 | ||
|
ab13f17fe7 | ||
|
8051412e9f | ||
|
650398b541 | ||
|
2d367aadb9 | ||
|
7d0db085c0 | ||
|
db4dc20c70 | ||
|
48e9ece152 | ||
|
12a2e85535 | ||
|
fbf9902b58 | ||
|
5d6c8b95bc | ||
|
dcbbd211fe | ||
|
60207dd24f | ||
|
3bc1dc6cad | ||
|
366a6e74e7 | ||
|
9ad98e4638 | ||
|
e5fdec3a5a | ||
|
e5c2079264 | ||
|
568d1c2e6f | ||
|
fd629c62bc | ||
|
6408390535 | ||
|
c7f6bcf7d7 | ||
|
7176f4dd0a | ||
|
6fc48a7d74 | ||
|
dc50b0b155 | ||
|
733df4c005 | ||
|
9c3eaa479c | ||
|
bed38d425f | ||
|
3d0dd67389 | ||
|
b112d23163 |
141 changed files with 1431 additions and 883 deletions
|
@ -43,7 +43,6 @@ dependencies {
|
|||
api 'info.picocli:picocli:4.7.5'
|
||||
api 'org.kohsuke:github-api:1.321'
|
||||
api 'io.sentry:sentry:7.8.0'
|
||||
api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final'
|
||||
api 'commons-io:commons-io:2.16.1'
|
||||
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0"
|
||||
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0"
|
||||
|
@ -75,7 +74,6 @@ application {
|
|||
mainModule = 'io.xpipe.app'
|
||||
mainClass = 'io.xpipe.app.Main'
|
||||
applicationDefaultJvmArgs = jvmRunArgs
|
||||
applicationDefaultJvmArgs.add('-XX:+EnableDynamicAgentLoading')
|
||||
}
|
||||
|
||||
run {
|
||||
|
|
|
@ -86,8 +86,7 @@ public final class BrowserBookmarkComp extends SimpleComp {
|
|||
selectedCategory)
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.minWidth(Region.USE_PREF_SIZE);
|
||||
var filter =
|
||||
new FilterComp(filterText).hgrow();
|
||||
var filter = new FilterComp(filterText).hgrow();
|
||||
|
||||
var top = new HorizontalComp(List.of(category, filter))
|
||||
.styleClass("categories")
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.file.FileSystemHelper;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.LocalFileSystem;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.ProcessControlProvider;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import io.xpipe.core.util.FailableRunnable;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.Dragboard;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.Value;
|
||||
|
||||
|
@ -45,14 +44,14 @@ public class BrowserClipboard {
|
|||
|
||||
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
|
||||
var files =
|
||||
data.stream().map(string -> string.toPath()).toList();
|
||||
data.stream().map(f -> f.toPath()).toList();
|
||||
if (files.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = new ArrayList<FileSystem.FileEntry>();
|
||||
var entries = new ArrayList<BrowserEntry>();
|
||||
for (Path file : files) {
|
||||
entries.add(FileSystemHelper.getLocal(file));
|
||||
entries.add(LocalFileSystem.getLocalBrowserEntry(file));
|
||||
}
|
||||
|
||||
currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries));
|
||||
|
@ -64,7 +63,7 @@ public class BrowserClipboard {
|
|||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
|
||||
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<BrowserEntry> selected) {
|
||||
if (selected.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
@ -77,7 +76,7 @@ public class BrowserClipboard {
|
|||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
|
||||
public static void startCopy(FileSystem.FileEntry base, List<BrowserEntry> selected) {
|
||||
if (selected.isEmpty()) {
|
||||
currentCopyClipboard.setValue(null);
|
||||
return;
|
||||
|
@ -118,11 +117,11 @@ public class BrowserClipboard {
|
|||
public static class Instance {
|
||||
UUID uuid;
|
||||
FileSystem.FileEntry baseDirectory;
|
||||
List<FileSystem.FileEntry> entries;
|
||||
List<BrowserEntry> entries;
|
||||
|
||||
public String toClipboardString() {
|
||||
return entries.stream()
|
||||
.map(fileEntry -> "\"" + fileEntry.getPath() + "\"")
|
||||
.map(fileEntry -> "\"" + fileEntry.getRawFileEntry().getPath() + "\"")
|
||||
.collect(Collectors.joining(ProcessControlProvider.get()
|
||||
.getEffectiveLocalDialect()
|
||||
.getNewLine()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.icon.FileIconManager;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.core.AppStyle;
|
||||
import io.xpipe.app.core.AppWindowHelper;
|
||||
|
@ -8,9 +8,6 @@ import io.xpipe.app.fxcomps.Comp;
|
|||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
|
@ -21,7 +18,6 @@ import javafx.scene.control.OverrunStyle;
|
|||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
@ -33,14 +29,14 @@ import java.util.function.Function;
|
|||
@AllArgsConstructor
|
||||
public class BrowserSelectionListComp extends SimpleComp {
|
||||
|
||||
ObservableList<FileSystem.FileEntry> list;
|
||||
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation;
|
||||
ObservableList<BrowserEntry> list;
|
||||
Function<BrowserEntry, ObservableValue<String>> nameTransformation;
|
||||
|
||||
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) {
|
||||
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath())));
|
||||
public BrowserSelectionListComp(ObservableList<BrowserEntry> list) {
|
||||
this(list, entry -> new SimpleStringProperty(entry.getFileName()));
|
||||
}
|
||||
|
||||
public static Image snapshot(ObservableList<FileSystem.FileEntry> list) {
|
||||
public static Image snapshot(ObservableList<BrowserEntry> list) {
|
||||
var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion();
|
||||
var scene = new Scene(r);
|
||||
AppWindowHelper.setupStylesheets(scene);
|
||||
|
@ -54,7 +50,7 @@ public class BrowserSelectionListComp extends SimpleComp {
|
|||
protected Region createSimple() {
|
||||
var c = new ListBoxViewComp<>(list, list, entry -> {
|
||||
return Comp.of(() -> {
|
||||
var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24)
|
||||
var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24)
|
||||
.createRegion();
|
||||
var l = new Label(null, image);
|
||||
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import io.xpipe.app.browser.file.BrowserContextMenu;
|
||||
import io.xpipe.app.browser.file.BrowserFileListCompEntry;
|
||||
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||
|
@ -11,16 +12,16 @@ import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
|||
import io.xpipe.app.fxcomps.impl.LabelComp;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.app.util.HumanReadableFormat;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.control.ToolBar;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BrowserStatusBarComp extends SimpleComp {
|
||||
|
@ -56,7 +57,9 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());
|
||||
var all = HumanReadableFormat.byteCount(p.getTotal());
|
||||
var name = (p.getName() != null ? " @ " + p.getName() + " " : "");
|
||||
return transferred + " / " + all + name;
|
||||
var time = p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0 ? " | "
|
||||
+ HumanReadableFormat.duration(p.expectedTimeRemaining()) : " | ...";
|
||||
return transferred + " / " + all + name + time;
|
||||
}
|
||||
});
|
||||
var progressComp = new LabelComp(text).styleClass("progress");
|
||||
|
@ -87,9 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
|
||||
var allCount = Bindings.createIntegerBinding(
|
||||
() -> {
|
||||
return (int) model.getFileList().getAll().getValue().stream()
|
||||
.filter(entry -> !entry.isSynthetic())
|
||||
.count();
|
||||
return model.getFileList().getAll().getValue().size();
|
||||
},
|
||||
model.getFileList().getAll());
|
||||
var selectedComp = new LabelComp(Bindings.createStringBinding(
|
||||
|
|
|
@ -9,10 +9,7 @@ import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
|
|||
import io.xpipe.app.fxcomps.impl.*;
|
||||
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
|
@ -21,7 +18,6 @@ import javafx.scene.input.Dragboard;
|
|||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -50,25 +46,21 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
var backgroundStack =
|
||||
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
|
||||
|
||||
var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getFileEntry());
|
||||
var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry());
|
||||
var list = new BrowserSelectionListComp(
|
||||
binding,
|
||||
entry -> Bindings.createStringBinding(
|
||||
() -> {
|
||||
var sourceItem = syncItems.stream()
|
||||
.filter(item -> item.getFileEntry() == entry)
|
||||
.filter(item -> item.getBrowserEntry() == entry)
|
||||
.findAny();
|
||||
if (sourceItem.isEmpty()) {
|
||||
return "?";
|
||||
}
|
||||
var name =
|
||||
sourceItem.get().downloadFinished().get()
|
||||
var name = entry.getModel() == null || sourceItem.get().downloadFinished().get()
|
||||
? "Local"
|
||||
: DataStorage.get()
|
||||
.getStoreDisplayName(entry.getFileSystem()
|
||||
.getStore())
|
||||
.orElse("?");
|
||||
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
|
||||
: entry.getModel().getFileSystemModel().getName();
|
||||
return entry.getFileName() + " (" + name + ")";
|
||||
},
|
||||
syncAllDownloaded))
|
||||
.apply(struc -> struc.get().setMinHeight(150))
|
||||
|
@ -154,7 +146,7 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
}
|
||||
|
||||
var selected = syncItems.stream()
|
||||
.map(BrowserTransferModel.Item::getFileEntry)
|
||||
.map(item -> item.getBrowserEntry())
|
||||
.toList();
|
||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.file.FileSystemHelper;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
|
||||
import io.xpipe.app.browser.file.LocalFileSystem;
|
||||
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ShellTemp;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
|
@ -17,7 +16,6 @@ import javafx.beans.property.SimpleObjectProperty;
|
|||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import lombok.Value;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
|
@ -66,9 +64,9 @@ public class BrowserTransferModel {
|
|||
items.clear();
|
||||
}
|
||||
|
||||
public void drop(OpenFileSystemModel model, List<FileSystem.FileEntry> entries) {
|
||||
public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
entries.forEach(entry -> {
|
||||
var name = FileNames.getFileName(entry.getPath());
|
||||
var name = entry.getFileName();
|
||||
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
|
||||
return;
|
||||
}
|
||||
|
@ -89,14 +87,14 @@ public class BrowserTransferModel {
|
|||
try {
|
||||
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
|
||||
for (Path path : paths) {
|
||||
var entry = FileSystemHelper.getLocal(path);
|
||||
var name = entry.getName();
|
||||
var entry = LocalFileSystem.getLocalBrowserEntry(path);
|
||||
var name = entry.getFileName();
|
||||
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var item = new Item(null, name, entry, path);
|
||||
item.progress.setValue(BrowserTransferProgress.finished(entry.getName(), entry.getSize()));
|
||||
item.progress.setValue(BrowserTransferProgress.finished(entry.getFileName(), entry.getRawFileEntry().getSize()));
|
||||
items.add(item);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
@ -128,15 +126,16 @@ public class BrowserTransferModel {
|
|||
|
||||
try {
|
||||
try (var b = new BooleanScope(downloading).start()) {
|
||||
FileSystemHelper.dropFilesInto(
|
||||
FileSystemHelper.getLocal(TEMP),
|
||||
List.of(item.getFileEntry()),
|
||||
var op = new BrowserFileTransferOperation(
|
||||
LocalFileSystem.getLocalFileEntry(TEMP),
|
||||
List.of(item.getBrowserEntry().getRawFileEntry()),
|
||||
true,
|
||||
false,
|
||||
progress -> {
|
||||
item.getProgress().setValue(progress);
|
||||
item.getOpenFileSystemModel().getProgress().setValue(progress);
|
||||
});
|
||||
op.execute();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
ErrorEvent.fromThrowable(t).handle();
|
||||
|
@ -151,15 +150,15 @@ public class BrowserTransferModel {
|
|||
public static class Item {
|
||||
OpenFileSystemModel openFileSystemModel;
|
||||
String name;
|
||||
FileSystem.FileEntry fileEntry;
|
||||
BrowserEntry browserEntry;
|
||||
Path localFile;
|
||||
Property<BrowserTransferProgress> progress;
|
||||
|
||||
public Item(
|
||||
OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) {
|
||||
OpenFileSystemModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) {
|
||||
this.openFileSystemModel = openFileSystemModel;
|
||||
this.name = name;
|
||||
this.fileEntry = fileEntry;
|
||||
this.browserEntry = browserEntry;
|
||||
this.localFile = localFile;
|
||||
this.progress = new SimpleObjectProperty<>();
|
||||
}
|
||||
|
|
|
@ -2,26 +2,45 @@ package io.xpipe.app.browser;
|
|||
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@Value
|
||||
public class BrowserTransferProgress {
|
||||
|
||||
String name;
|
||||
long transferred;
|
||||
long total;
|
||||
Instant start;
|
||||
|
||||
public static BrowserTransferProgress empty() {
|
||||
return new BrowserTransferProgress(null, 0, 0);
|
||||
return new BrowserTransferProgress(null, 0, 0, Instant.now());
|
||||
}
|
||||
|
||||
static BrowserTransferProgress empty(String name, long size) {
|
||||
return new BrowserTransferProgress(name, 0, size);
|
||||
return new BrowserTransferProgress(name, 0, size, Instant.now());
|
||||
}
|
||||
|
||||
public static BrowserTransferProgress finished(String name, long size) {
|
||||
return new BrowserTransferProgress(name, size, size);
|
||||
return new BrowserTransferProgress(name, size, size, Instant.now());
|
||||
}
|
||||
|
||||
public boolean done() {
|
||||
return transferred >= total;
|
||||
}
|
||||
|
||||
public Duration elapsedTime() {
|
||||
var now = Instant.now();
|
||||
var elapsed = Duration.between(start,now);
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
public Duration expectedTimeRemaining() {
|
||||
var elapsed = elapsedTime();
|
||||
var share = (double) transferred / total;
|
||||
var rest = (1.0 - share) / share;
|
||||
var restMillis = (long) (elapsed.toMillis() * rest);
|
||||
return Duration.of(restMillis, ChronoUnit.MILLIS);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
|
@ -32,6 +31,7 @@ import javafx.scene.layout.Region;
|
|||
import javafx.scene.layout.VBox;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -139,14 +139,18 @@ public class BrowserWelcomeComp extends SimpleComp {
|
|||
|
||||
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
|
||||
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
||||
var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
|
||||
var graphic =
|
||||
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
|
||||
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
|
||||
return new ButtonComp(
|
||||
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
|
||||
view.createRegion(),
|
||||
() -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.restoreStateAsync(e, disable);
|
||||
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
||||
if (storageEntry.isPresent()) {
|
||||
model.openFileSystemAsync(storageEntry.get().ref(), null, disable);
|
||||
}
|
||||
});
|
||||
})
|
||||
.minWidth(250)
|
||||
|
|
|
@ -8,8 +8,10 @@ import io.xpipe.app.fxcomps.util.Shortcuts;
|
|||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.MenuItem;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -91,7 +93,8 @@ public interface LeafAction extends BrowserAction {
|
|||
mi.setMnemonicParsing(false);
|
||||
mi.setDisable(!isActive(model, selected));
|
||||
|
||||
if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
|
||||
if (getProFeatureId() != null
|
||||
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
|
||||
mi.setDisable(true);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,8 +32,7 @@ public final class BrowserContextMenu extends ContextMenu {
|
|||
? selected.stream()
|
||||
.map(browserEntry -> new BrowserEntry(
|
||||
browserEntry.getRawFileEntry().resolved(),
|
||||
browserEntry.getModel(),
|
||||
browserEntry.isSynthetic()))
|
||||
browserEntry.getModel()))
|
||||
.toList()
|
||||
: selected;
|
||||
}
|
||||
|
@ -44,7 +43,7 @@ public final class BrowserContextMenu extends ContextMenu {
|
|||
var empty = source == null;
|
||||
var selected = new ArrayList<>(
|
||||
empty
|
||||
? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false))
|
||||
? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList()))
|
||||
: model.getFileList().getSelection());
|
||||
if (source != null && !selected.contains(source)) {
|
||||
selected.add(source);
|
||||
|
|
|
@ -13,14 +13,12 @@ public class BrowserEntry {
|
|||
|
||||
private final BrowserFileListModel model;
|
||||
private final FileSystem.FileEntry rawFileEntry;
|
||||
private final boolean synthetic;
|
||||
private final BrowserIconFileType fileType;
|
||||
private final BrowserIconDirectoryType directoryType;
|
||||
|
||||
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model, boolean synthetic) {
|
||||
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model) {
|
||||
this.rawFileEntry = rawFileEntry;
|
||||
this.model = model;
|
||||
this.synthetic = synthetic;
|
||||
this.fileType = fileType(rawFileEntry);
|
||||
this.directoryType = directoryType(rawFileEntry);
|
||||
}
|
||||
|
@ -52,6 +50,17 @@ public class BrowserEntry {
|
|||
|
||||
return null;
|
||||
}
|
||||
public String getIcon() {
|
||||
if (fileType != null) {
|
||||
return fileType.getIcon();
|
||||
} else if (directoryType != null) {
|
||||
return directoryType.getIcon(rawFileEntry, false);
|
||||
} else {
|
||||
return rawFileEntry.getKind() == FileKind.DIRECTORY
|
||||
? "default_folder.svg"
|
||||
: "default_file.svg";
|
||||
}
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return getRawFileEntry().getName();
|
||||
|
@ -61,9 +70,4 @@ public class BrowserEntry {
|
|||
var n = getFileName();
|
||||
return FileNames.quoteIfNecessary(n);
|
||||
}
|
||||
|
||||
public String getOptionallyQuotedFilePath() {
|
||||
var n = rawFileEntry.getPath();
|
||||
return FileNames.quoteIfNecessary(n);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.icon.FileIconManager;
|
||||
import io.xpipe.app.comp.base.LazyTextFieldComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
|
@ -16,7 +17,6 @@ import io.xpipe.core.process.OsType;
|
|||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
|
@ -38,9 +38,6 @@ import javafx.scene.layout.HBox;
|
|||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
|
@ -137,29 +134,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
table.getSelectionModel().setCellSelectionEnabled(false);
|
||||
|
||||
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
||||
var toSelect = new ArrayList<>(c.getList());
|
||||
// Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in
|
||||
// JavaFX
|
||||
toSelect.removeIf(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() != null
|
||||
&& entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.equals(fileList.getFileSystemModel()
|
||||
.getCurrentParentDirectory()
|
||||
.getPath()));
|
||||
// Remove unsuitable selection
|
||||
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY
|
||||
&& !fileList.getSelectionMode().isAcceptsDirectories())
|
||||
|| (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY
|
||||
&& !fileList.getSelectionMode().isAcceptsFiles()));
|
||||
fileList.getSelection().setAll(toSelect);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
var toUnselect = table.getSelectionModel().getSelectedItems().stream()
|
||||
.filter(entry -> !toSelect.contains(entry))
|
||||
.toList();
|
||||
toUnselect.forEach(entry -> table.getSelectionModel()
|
||||
.clearSelection(table.getItems().indexOf(entry)));
|
||||
});
|
||||
fileList.getSelection().setAll(c.getList());
|
||||
});
|
||||
|
||||
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
||||
|
@ -174,7 +149,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
}
|
||||
|
||||
var indices = c.getList().stream()
|
||||
.skip(1)
|
||||
.mapToInt(entry -> table.getItems().indexOf(entry))
|
||||
.toArray();
|
||||
table.getSelectionModel()
|
||||
|
@ -276,10 +250,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
},
|
||||
null,
|
||||
() -> {
|
||||
if (row.getItem() != null && row.getItem().isSynthetic()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem());
|
||||
})
|
||||
.augment(new SimpleCompStructure<>(row));
|
||||
|
@ -573,15 +543,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
// Visibility seems to be bugged, so use opacity
|
||||
setOpacity(0.0);
|
||||
} else {
|
||||
var isParentLink = getTableRow()
|
||||
.getItem()
|
||||
.getRawFileEntry()
|
||||
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
||||
img.set(FileIconManager.getFileIcon(
|
||||
isParentLink
|
||||
? fileList.getFileSystemModel().getCurrentDirectory()
|
||||
: getTableRow().getItem().getRawFileEntry(),
|
||||
isParentLink));
|
||||
img.set(getTableRow().getItem().getIcon());
|
||||
|
||||
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
|
||||
pseudoClassStateChanged(FOLDER, isDirectory);
|
||||
|
@ -594,9 +556,8 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
.resolved()
|
||||
.getPath()
|
||||
: getTableRow().getItem().getFileName();
|
||||
var fileName = isParentLink ? ".." : normalName;
|
||||
var hidden = !isParentLink
|
||||
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
|
||||
var fileName = normalName;
|
||||
var hidden = getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith(".");
|
||||
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
|
||||
text.set(fileName);
|
||||
// Visibility seems to be bugged, so use opacity
|
||||
|
|
|
@ -109,7 +109,7 @@ public class BrowserFileListCompEntry {
|
|||
|
||||
if (!Objects.equals(
|
||||
model.getFileSystemModel().getFileSystem(),
|
||||
cb.getEntries().getFirst().getFileSystem())) {
|
||||
cb.getEntries().getFirst().getRawFileEntry().getFileSystem())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -157,7 +157,7 @@ public class BrowserFileListCompEntry {
|
|||
var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY
|
||||
? item.getRawFileEntry()
|
||||
: model.getFileSystemModel().getCurrentDirectory();
|
||||
model.getFileSystemModel().dropFilesIntoAsync(target, files, false);
|
||||
model.getFileSystemModel().dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), false);
|
||||
event.setDropCompleted(true);
|
||||
event.consume();
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ public class BrowserFileListCompEntry {
|
|||
return;
|
||||
}
|
||||
|
||||
var selected = model.getSelectedRaw();
|
||||
var selected = model.getSelection();
|
||||
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
|
||||
db.setContent(BrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected));
|
||||
|
||||
|
@ -244,7 +244,7 @@ public class BrowserFileListCompEntry {
|
|||
return;
|
||||
}
|
||||
|
||||
if (item == null || item.isSynthetic()) {
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -59,10 +59,7 @@ public final class BrowserFileListModel {
|
|||
|
||||
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
|
||||
try (var s = newFiles) {
|
||||
var parent = fileSystemModel.getCurrentParentDirectory();
|
||||
var l = Stream.concat(
|
||||
parent != null ? Stream.of(new BrowserEntry(parent, this, true)) : Stream.of(),
|
||||
s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this, false)))
|
||||
var l = s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this))
|
||||
.toList();
|
||||
all.setValue(l);
|
||||
refreshShown();
|
||||
|
@ -94,14 +91,13 @@ public final class BrowserFileListModel {
|
|||
}
|
||||
|
||||
public Comparator<BrowserEntry> order() {
|
||||
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
|
||||
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
|
||||
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
var comp = comparatorProperty.getValue();
|
||||
|
||||
Comparator<BrowserEntry> us = comp != null
|
||||
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
|
||||
: syntheticFirst.thenComparing(dirsFirst);
|
||||
? dirsFirst.thenComparing(comp)
|
||||
: dirsFirst;
|
||||
return us;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
|
|||
var graphic = new HorizontalComp(List.of(
|
||||
icon,
|
||||
new BrowserQuickAccessButtonComp(
|
||||
() -> new BrowserEntry(entry, model.getFileList(), false), model)));
|
||||
() -> new BrowserEntry(entry, model.getFileList()), model)));
|
||||
var l = new Button(entry.getPath(), graphic.createRegion());
|
||||
l.setGraphicTextGap(1);
|
||||
l.setOnAction(event -> {
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.BrowserTransferProgress;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class BrowserFileTransferOperation {
|
||||
|
||||
private final FileSystem.FileEntry target;
|
||||
private final List<FileSystem.FileEntry> files;
|
||||
private final boolean explicitCopy;
|
||||
private final boolean checkConflicts;
|
||||
private final Consumer<BrowserTransferProgress> progress;
|
||||
|
||||
BrowserAlerts.FileConflictChoice lastConflictChoice;
|
||||
|
||||
public BrowserFileTransferOperation(FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy, boolean checkConflicts,
|
||||
Consumer<BrowserTransferProgress> progress
|
||||
) {
|
||||
this.target = target;
|
||||
this.files = files;
|
||||
this.explicitCopy = explicitCopy;
|
||||
this.checkConflicts = checkConflicts;
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public static BrowserFileTransferOperation ofLocal(FileSystem.FileEntry target, List<Path> files, boolean explicitCopy, boolean checkConflicts, Consumer<BrowserTransferProgress> progress) {
|
||||
var entries = files.stream()
|
||||
.map(path -> {
|
||||
try {
|
||||
return LocalFileSystem.getLocalFileEntry(path);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
return new BrowserFileTransferOperation(target, entries, explicitCopy, checkConflicts, progress);
|
||||
}
|
||||
|
||||
private void updateProgress(BrowserTransferProgress progress) {
|
||||
this.progress.accept(progress);
|
||||
}
|
||||
|
||||
private boolean handleChoice(
|
||||
FileSystem fileSystem,
|
||||
String target,
|
||||
boolean multiple)
|
||||
throws Exception {
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileSystem.fileExists(target)) {
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var choice = BrowserAlerts.showFileConflictAlert(target, multiple);
|
||||
if (choice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.CANCEL;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.SKIP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.SKIP_ALL;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.REPLACE_ALL;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void execute()
|
||||
throws Exception {
|
||||
if (files.isEmpty()) {
|
||||
updateProgress(BrowserTransferProgress.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
|
||||
if (same && !explicitCopy) {
|
||||
if (!BrowserAlerts.showMoveAlert(files, target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (var file : files) {
|
||||
if (file.getFileSystem().equals(target.getFileSystem())) {
|
||||
handleSingleOnSameFileSystem(file);
|
||||
updateProgress(BrowserTransferProgress.finished(file.getName(), file.getSize()));
|
||||
} else {
|
||||
handleSingleAcrossFileSystems(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSingleOnSameFileSystem(FileSystem.FileEntry source)
|
||||
throws Exception {
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getPath().equals(target.getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceFile = source.getPath();
|
||||
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
|
||||
|
||||
if (sourceFile.equals(targetFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
|
||||
throw ErrorEvent.expected(
|
||||
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
|
||||
}
|
||||
|
||||
if (checkConflicts && !handleChoice(target.getFileSystem(), targetFile, files.size() > 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (explicitCopy) {
|
||||
target.getFileSystem().copy(sourceFile, targetFile);
|
||||
} else {
|
||||
target.getFileSystem().move(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSingleAcrossFileSystems(FileSystem.FileEntry source)
|
||||
throws Exception {
|
||||
if (target.getKind() != FileKind.DIRECTORY) {
|
||||
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
|
||||
}
|
||||
|
||||
var flatFiles = new LinkedHashMap<FileSystem.FileEntry, String>();
|
||||
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getFileSystem().equals(target.getFileSystem())
|
||||
&& FileNames.startsWith(source.getPath(), target.getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicLong totalSize = new AtomicLong();
|
||||
if (source.getKind() == FileKind.DIRECTORY) {
|
||||
var directoryName = FileNames.getFileName(source.getPath());
|
||||
flatFiles.put(source, directoryName);
|
||||
|
||||
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
|
||||
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
|
||||
for (FileSystem.FileEntry fileEntry : list) {
|
||||
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
totalSize.addAndGet(fileEntry.getSize());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flatFiles.put(source, FileNames.getFileName(source.getPath()));
|
||||
// Recalculate as it could have been changed meanwhile
|
||||
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
|
||||
}
|
||||
|
||||
AtomicLong transferred = new AtomicLong();
|
||||
for (var e : flatFiles.entrySet()) {
|
||||
var sourceFile = e.getKey();
|
||||
var fixedRelPath = new FilePath(e.getValue())
|
||||
.fileSystemCompatible(
|
||||
target.getFileSystem().getShell().orElseThrow().getOsType());
|
||||
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
|
||||
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
if (sourceFile.getKind() == FileKind.DIRECTORY) {
|
||||
target.getFileSystem().mkdirs(targetFile);
|
||||
} else if (sourceFile.getKind() == FileKind.FILE) {
|
||||
if (checkConflicts
|
||||
&& !handleChoice(
|
||||
target.getFileSystem(),
|
||||
targetFile,
|
||||
files.size() > 1 || flatFiles.size() > 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
|
||||
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
|
||||
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
|
||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize);
|
||||
inputStream.transferTo(OutputStream.nullOutputStream());
|
||||
} catch (Exception ex) {
|
||||
// Mark progress as finished to reset any progress display
|
||||
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
|
||||
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (Exception om) {
|
||||
// This is expected as the process control has to be killed
|
||||
// When calling close, it will throw an exception when it has to kill
|
||||
// ErrorEvent.fromThrowable(om).handle();
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (Exception om) {
|
||||
// This is expected as the process control has to be killed
|
||||
// When calling close, it will throw an exception when it has to kill
|
||||
// ErrorEvent.fromThrowable(om).handle();
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
|
||||
Exception exception = null;
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (Exception om) {
|
||||
exception = om;
|
||||
}
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (Exception om) {
|
||||
if (exception != null) {
|
||||
ErrorEvent.fromThrowable(om).handle();
|
||||
} else {
|
||||
exception = om;
|
||||
}
|
||||
}
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
|
||||
}
|
||||
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024;
|
||||
|
||||
private void transferFile(
|
||||
FileSystem.FileEntry sourceFile,
|
||||
InputStream inputStream,
|
||||
OutputStream outputStream,
|
||||
AtomicLong transferred,
|
||||
AtomicLong total)
|
||||
throws IOException {
|
||||
// Initialize progress immediately prior to reading anything
|
||||
var now = Instant.now();
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), now));
|
||||
|
||||
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
|
||||
byte[] buffer = new byte[bs];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), now));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,6 +63,10 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
|||
getItems().clear();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var entry = base.get();
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
|
||||
return;
|
||||
}
|
||||
|
@ -97,7 +101,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
|||
newItems.add(empty);
|
||||
} else {
|
||||
var browserEntries = list.stream()
|
||||
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false))
|
||||
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList()))
|
||||
.toList();
|
||||
var menus = browserEntries.stream()
|
||||
.sorted(model.getFileList().order())
|
||||
|
@ -126,10 +130,8 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
|||
this.browserEntry = browserEntry;
|
||||
this.menu = new Menu(
|
||||
// Use original name, not the link target
|
||||
browserEntry.getRawFileEntry().getName(),
|
||||
PrettyImageHelper.ofFixedSizeSquare(
|
||||
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
|
||||
.createRegion());
|
||||
browserEntry.getRawFileEntry().getName(), PrettyImageHelper.ofFixedRasterized(
|
||||
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24, 24).createRegion());
|
||||
createMenu();
|
||||
addInputListeners();
|
||||
}
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.BrowserTransferProgress;
|
||||
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.*;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class FileSystemHelper {
|
||||
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024;
|
||||
private static FileSystem localFileSystem;
|
||||
|
||||
public static String adjustPath(OpenFileSystemModel model, String path) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
|
@ -134,23 +123,6 @@ public class FileSystemHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static FileSystem.FileEntry getLocal(Path file) throws Exception {
|
||||
if (localFileSystem == null) {
|
||||
localFileSystem = new LocalStore().createFileSystem();
|
||||
localFileSystem.open();
|
||||
}
|
||||
|
||||
return new FileSystem.FileEntry(
|
||||
localFileSystem,
|
||||
file.toString(),
|
||||
Files.getLastModifiedTime(file).toInstant(),
|
||||
Files.isHidden(file),
|
||||
Files.isExecutable(file),
|
||||
Files.size(file),
|
||||
null,
|
||||
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||
}
|
||||
|
||||
public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
|
||||
return new FileSystem.FileEntry(
|
||||
fileSystem,
|
||||
|
@ -163,24 +135,6 @@ public class FileSystemHelper {
|
|||
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||
}
|
||||
|
||||
public static void dropLocalFilesInto(
|
||||
FileSystem.FileEntry entry,
|
||||
List<Path> files,
|
||||
Consumer<BrowserTransferProgress> progress,
|
||||
boolean checkConflicts)
|
||||
throws Exception {
|
||||
var entries = files.stream()
|
||||
.map(path -> {
|
||||
try {
|
||||
return getLocal(path);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
dropFilesInto(entry, entries, false, checkConflicts, progress);
|
||||
}
|
||||
|
||||
public static void delete(List<FileSystem.FileEntry> files) {
|
||||
if (files.isEmpty()) {
|
||||
return;
|
||||
|
@ -194,255 +148,4 @@ public class FileSystemHelper {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void dropFilesInto(
|
||||
FileSystem.FileEntry target,
|
||||
List<FileSystem.FileEntry> files,
|
||||
boolean explicitCopy,
|
||||
boolean checkConflicts,
|
||||
Consumer<BrowserTransferProgress> progress)
|
||||
throws Exception {
|
||||
if (files.isEmpty()) {
|
||||
progress.accept(BrowserTransferProgress.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
|
||||
if (same && !explicitCopy) {
|
||||
if (!BrowserAlerts.showMoveAlert(files, target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice = new AtomicReference<>();
|
||||
for (var file : files) {
|
||||
if (file.getFileSystem().equals(target.getFileSystem())) {
|
||||
dropFileAcrossSameFileSystem(
|
||||
target, file, explicitCopy, lastConflictChoice, files.size() > 1, checkConflicts);
|
||||
progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize()));
|
||||
} else {
|
||||
dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1, checkConflicts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void dropFileAcrossSameFileSystem(
|
||||
FileSystem.FileEntry target,
|
||||
FileSystem.FileEntry source,
|
||||
boolean explicitCopy,
|
||||
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
|
||||
boolean multiple,
|
||||
boolean checkConflicts)
|
||||
throws Exception {
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getPath().equals(target.getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sourceFile = source.getPath();
|
||||
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
|
||||
|
||||
if (sourceFile.equals(targetFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
|
||||
throw ErrorEvent.expected(
|
||||
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
|
||||
}
|
||||
|
||||
if (checkConflicts && !handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (explicitCopy) {
|
||||
target.getFileSystem().copy(sourceFile, targetFile);
|
||||
} else {
|
||||
target.getFileSystem().move(sourceFile, targetFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void dropFileAcrossFileSystems(
|
||||
FileSystem.FileEntry target,
|
||||
FileSystem.FileEntry source,
|
||||
Consumer<BrowserTransferProgress> progress,
|
||||
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
|
||||
boolean multiple,
|
||||
boolean checkConflicts)
|
||||
throws Exception {
|
||||
if (target.getKind() != FileKind.DIRECTORY) {
|
||||
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
|
||||
}
|
||||
|
||||
var flatFiles = new LinkedHashMap<FileSystem.FileEntry, String>();
|
||||
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getFileSystem().equals(target.getFileSystem())
|
||||
&& FileNames.startsWith(source.getPath(), target.getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
AtomicLong totalSize = new AtomicLong();
|
||||
if (source.getKind() == FileKind.DIRECTORY) {
|
||||
var directoryName = FileNames.getFileName(source.getPath());
|
||||
flatFiles.put(source, directoryName);
|
||||
|
||||
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
|
||||
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
|
||||
for (FileSystem.FileEntry fileEntry : list) {
|
||||
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
totalSize.addAndGet(fileEntry.getSize());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flatFiles.put(source, FileNames.getFileName(source.getPath()));
|
||||
// Recalculate as it could have been changed meanwhile
|
||||
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
|
||||
}
|
||||
|
||||
AtomicLong transferred = new AtomicLong();
|
||||
for (var e : flatFiles.entrySet()) {
|
||||
var sourceFile = e.getKey();
|
||||
var fixedRelPath = new FilePath(e.getValue())
|
||||
.fileSystemCompatible(
|
||||
target.getFileSystem().getShell().orElseThrow().getOsType());
|
||||
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
|
||||
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
if (sourceFile.getKind() == FileKind.DIRECTORY) {
|
||||
target.getFileSystem().mkdirs(targetFile);
|
||||
} else if (sourceFile.getKind() == FileKind.FILE) {
|
||||
if (checkConflicts
|
||||
&& !handleChoice(
|
||||
lastConflictChoice,
|
||||
target.getFileSystem(),
|
||||
targetFile,
|
||||
multiple || flatFiles.size() > 1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
|
||||
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
|
||||
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
|
||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, progress);
|
||||
inputStream.transferTo(OutputStream.nullOutputStream());
|
||||
} catch (Exception ex) {
|
||||
// Mark progress as finished to reset any progress display
|
||||
progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
|
||||
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (Exception om) {
|
||||
// This is expected as the process control has to be killed
|
||||
// When calling close, it will throw an exception when it has to kill
|
||||
// ErrorEvent.fromThrowable(om).handle();
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (Exception om) {
|
||||
// This is expected as the process control has to be killed
|
||||
// When calling close, it will throw an exception when it has to kill
|
||||
// ErrorEvent.fromThrowable(om).handle();
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
|
||||
Exception exception = null;
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (Exception om) {
|
||||
exception = om;
|
||||
}
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (Exception om) {
|
||||
if (exception != null) {
|
||||
ErrorEvent.fromThrowable(om).handle();
|
||||
} else {
|
||||
exception = om;
|
||||
}
|
||||
}
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
progress.accept(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
|
||||
}
|
||||
|
||||
private static boolean handleChoice(
|
||||
AtomicReference<BrowserAlerts.FileConflictChoice> previous,
|
||||
FileSystem fileSystem,
|
||||
String target,
|
||||
boolean multiple)
|
||||
throws Exception {
|
||||
if (previous.get() == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previous.get() == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileSystem.fileExists(target)) {
|
||||
if (previous.get() == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var choice = BrowserAlerts.showFileConflictAlert(target, multiple);
|
||||
if (choice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
previous.set(BrowserAlerts.FileConflictChoice.CANCEL);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.SKIP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
|
||||
previous.set(BrowserAlerts.FileConflictChoice.SKIP_ALL);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
|
||||
previous.set(BrowserAlerts.FileConflictChoice.REPLACE_ALL);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void transferFile(
|
||||
FileSystem.FileEntry sourceFile,
|
||||
InputStream inputStream,
|
||||
OutputStream outputStream,
|
||||
AtomicLong transferred,
|
||||
AtomicLong total,
|
||||
Consumer<BrowserTransferProgress> progress)
|
||||
throws IOException {
|
||||
// Initialize progress immediately prior to reading anything
|
||||
progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get()));
|
||||
|
||||
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
|
||||
byte[] buffer = new byte[bs];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import io.xpipe.core.store.LocalStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class LocalFileSystem {
|
||||
|
||||
private static FileSystem localFileSystem;
|
||||
|
||||
public static void init() throws Exception {
|
||||
if (localFileSystem == null) {
|
||||
localFileSystem = new LocalStore().createFileSystem();
|
||||
localFileSystem.open();
|
||||
}
|
||||
}
|
||||
|
||||
public static FileSystem.FileEntry getLocalFileEntry(Path file) throws IOException {
|
||||
if (localFileSystem == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
return new FileSystem.FileEntry(
|
||||
localFileSystem,
|
||||
file.toString(),
|
||||
Files.getLastModifiedTime(file).toInstant(),
|
||||
Files.isHidden(file),
|
||||
Files.isExecutable(file),
|
||||
Files.size(file),
|
||||
null,
|
||||
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||
}
|
||||
|
||||
public static BrowserEntry getLocalBrowserEntry(Path file) throws Exception {
|
||||
var e = getLocalFileEntry(file);
|
||||
return new BrowserEntry(e,null);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.browser.BrowserSavedState;
|
|||
import io.xpipe.app.browser.BrowserTransferProgress;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.file.BrowserFileListModel;
|
||||
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
|
||||
import io.xpipe.app.browser.file.FileSystemHelper;
|
||||
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
|
||||
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||
|
@ -297,11 +298,17 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
loadFilesSync(path);
|
||||
}
|
||||
|
||||
public void withFiles(String dir, FailableConsumer<Stream<FileSystem.FileEntry>, Exception> consumer) throws Exception {
|
||||
public void withFiles(String dir, FailableConsumer<Stream<FileSystem.FileEntry>, Exception> consumer)
|
||||
throws Exception {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
if (dir != null) {
|
||||
startIfNeeded();
|
||||
var stream = getFileSystem().listFiles(dir);
|
||||
var fs = getFileSystem();
|
||||
if (fs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stream = fs.listFiles(dir);
|
||||
consumer.accept(stream);
|
||||
} else {
|
||||
consumer.accept(Stream.of());
|
||||
|
@ -311,9 +318,10 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
|
||||
private boolean loadFilesSync(String dir) {
|
||||
try {
|
||||
if (dir != null) {
|
||||
startIfNeeded();
|
||||
var stream = getFileSystem().listFiles(dir);
|
||||
startIfNeeded();
|
||||
var fs = getFileSystem();
|
||||
if (dir != null && fs != null) {
|
||||
var stream = fs.listFiles(dir);
|
||||
fileList.setAll(stream);
|
||||
} else {
|
||||
fileList.setAll(Stream.of());
|
||||
|
@ -334,7 +342,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
}
|
||||
|
||||
startIfNeeded();
|
||||
FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue, true);
|
||||
var op = BrowserFileTransferOperation.ofLocal(entry, files,false,true, progress::setValue);
|
||||
op.execute();
|
||||
refreshSync();
|
||||
});
|
||||
});
|
||||
|
@ -354,9 +363,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
}
|
||||
|
||||
startIfNeeded();
|
||||
FileSystemHelper.dropFilesInto(target, files, explicitCopy, true, browserTransferProgress -> {
|
||||
progress.setValue(browserTransferProgress);
|
||||
});
|
||||
var op = new BrowserFileTransferOperation(target, files,false,true, progress::setValue);
|
||||
op.execute();
|
||||
refreshSync();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,8 @@ public class FileIconManager {
|
|||
|
||||
public static synchronized void loadIfNecessary() {
|
||||
if (!loaded) {
|
||||
BrowserIconFileType.loadDefinitions();
|
||||
BrowserIconDirectoryType.loadDefinitions();
|
||||
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons", true, false);
|
||||
loaded = true;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
|
|||
import io.xpipe.app.fxcomps.CompStructure;
|
||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
@ -46,7 +47,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
|
|||
public CompStructure<Button> createBase() {
|
||||
var button = new Button(null);
|
||||
if (name != null) {
|
||||
button.textProperty().bind(name);
|
||||
button.textProperty().bind(PlatformThread.sync(name));
|
||||
}
|
||||
var graphic = getGraphic();
|
||||
if (graphic instanceof FontIcon f) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package io.xpipe.app.comp.base;
|
|||
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.AppLogs;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.CompStructure;
|
||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||
|
@ -10,8 +9,6 @@ import io.xpipe.app.fxcomps.augment.Augment;
|
|||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.UserReportComp;
|
||||
import io.xpipe.app.update.UpdateAvailableAlert;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
|
@ -125,27 +122,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
noneBorder));
|
||||
};
|
||||
|
||||
{
|
||||
var b = new IconButtonComp("mdal-bug_report", () -> {
|
||||
var event = ErrorEvent.fromMessage("User Report");
|
||||
if (AppLogs.get().isWriteToFile()) {
|
||||
event.attachment(AppLogs.get().getSessionLogsDirectory());
|
||||
}
|
||||
UserReportComp.show(event.build());
|
||||
})
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
|
||||
.apply(new TooltipAugment<>("reportIssue"))
|
||||
.apply(simpleBorders)
|
||||
.accessibleTextKey("reportIssue");
|
||||
b.apply(struc -> {
|
||||
AppFont.setSize(struc.get(), 2);
|
||||
});
|
||||
vbox.getChildren().add(b.createRegion());
|
||||
}
|
||||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
|
||||
.apply(new TooltipAugment<>("visitGithubRepository"))
|
||||
.apply(simpleBorders)
|
||||
.accessibleTextKey("visitGithubRepository");
|
||||
|
@ -157,7 +136,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
|
||||
.apply(new TooltipAugment<>("discord"))
|
||||
.apply(simpleBorders)
|
||||
.accessibleTextKey("discord");
|
||||
|
@ -169,7 +148,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3]))
|
||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
|
||||
.apply(new TooltipAugment<>("translate"))
|
||||
.apply(simpleBorders)
|
||||
.accessibleTextKey("translate");
|
||||
|
|
|
@ -6,11 +6,13 @@ import io.xpipe.app.fxcomps.SimpleComp;
|
|||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
|
@ -29,17 +31,29 @@ public class StoreToggleComp extends SimpleComp {
|
|||
@Setter
|
||||
private ObservableBooleanValue customVisibility = new SimpleBooleanProperty(true);
|
||||
|
||||
public static <T extends DataStore> StoreToggleComp simpleToggle(String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||
return new StoreToggleComp(nameKey, section,new SimpleBooleanProperty(initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),v -> {
|
||||
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(),v);
|
||||
});
|
||||
public static <T extends DataStore> StoreToggleComp simpleToggle(
|
||||
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||
return new StoreToggleComp(
|
||||
nameKey,
|
||||
section,
|
||||
new SimpleBooleanProperty(
|
||||
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
|
||||
v -> {
|
||||
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
|
||||
});
|
||||
}
|
||||
|
||||
public static <T extends DataStore> StoreToggleComp childrenToggle(String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||
return new StoreToggleComp(nameKey, section,new SimpleBooleanProperty(initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),v -> {
|
||||
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(),v);
|
||||
section.getWrapper().refreshChildren();
|
||||
});
|
||||
public static <T extends DataStore> StoreToggleComp childrenToggle(
|
||||
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||
return new StoreToggleComp(
|
||||
nameKey,
|
||||
section,
|
||||
new SimpleBooleanProperty(
|
||||
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
|
||||
v -> {
|
||||
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
|
||||
section.getWrapper().refreshChildren();
|
||||
});
|
||||
}
|
||||
|
||||
public StoreToggleComp(String nameKey, StoreSection section, boolean initial, Consumer<Boolean> onChange) {
|
||||
|
@ -65,8 +79,7 @@ public class StoreToggleComp extends SimpleComp {
|
|||
return false;
|
||||
}
|
||||
|
||||
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE
|
||||
&& section.getShowDetails().get();
|
||||
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE;
|
||||
},
|
||||
section.getWrapper().getValidity(),
|
||||
section.getShowDetails(),
|
||||
|
|
|
@ -5,10 +5,12 @@ import io.xpipe.app.ext.DataStoreProvider;
|
|||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
import io.xpipe.app.util.ScanAlert;
|
||||
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
public class StoreCreationMenu {
|
||||
|
@ -83,7 +85,9 @@ public class StoreCreationMenu {
|
|||
category);
|
||||
event.consume();
|
||||
});
|
||||
var providers = sub.stream().sorted((o1, o2) -> -o1.getModuleName().compareTo(o2.getModuleName())).toList();
|
||||
var providers = sub.stream()
|
||||
.sorted((o1, o2) -> -o1.getModuleName().compareTo(o2.getModuleName()))
|
||||
.toList();
|
||||
for (int i = 0; i < providers.size(); i++) {
|
||||
var dataStoreProvider = providers.get(i);
|
||||
if (i > 0 && !providers.get(i - 1).getModuleName().equals(dataStoreProvider.getModuleName())) {
|
||||
|
|
|
@ -116,7 +116,9 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
var loading = LoadingOverlayComp.noProgress(
|
||||
Comp.of(() -> button),
|
||||
wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper)));
|
||||
wrapper.getEntry().getValidity().isUsable()
|
||||
? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper))
|
||||
: wrapper.getBusy());
|
||||
return loading.createRegion();
|
||||
}
|
||||
|
||||
|
@ -377,7 +379,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
if (AppPrefs.get().developerMode().getValue()) {
|
||||
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
|
||||
browse.setOnAction(
|
||||
event -> DesktopHelper.browsePath(wrapper.getEntry().getDirectory()));
|
||||
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
|
||||
contextMenu.getItems().add(browse);
|
||||
}
|
||||
|
||||
|
|
|
@ -68,6 +68,10 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
|
|||
m.getItems().setAll(items);
|
||||
m.setOnAction(event -> {
|
||||
if (event.getTarget() == m) {
|
||||
if (m.getItems().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
action.accept(w);
|
||||
contextMenu.hide();
|
||||
event.consume();
|
||||
|
|
|
@ -2,12 +2,10 @@ package io.xpipe.app.core;
|
|||
|
||||
import io.xpipe.app.comp.AppLayoutComp;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
@ -16,8 +14,6 @@ import javafx.stage.Stage;
|
|||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
@Getter
|
||||
public class App extends Application {
|
||||
|
||||
|
@ -35,25 +31,6 @@ public class App extends Application {
|
|||
APP = this;
|
||||
stage = primaryStage;
|
||||
stage.opacityProperty().bind(AppPrefs.get().windowOpacity());
|
||||
|
||||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||
Desktop.getDesktop().setPreferencesHandler(e -> {
|
||||
AppLayoutModel.get().selectSettings();
|
||||
});
|
||||
}
|
||||
|
||||
if (OsType.getLocal().equals(OsType.LINUX)) {
|
||||
try {
|
||||
Toolkit xToolkit = Toolkit.getDefaultToolkit();
|
||||
java.lang.reflect.Field awtAppClassNameField =
|
||||
xToolkit.getClass().getDeclaredField("awtAppClassName");
|
||||
awtAppClassNameField.setAccessible(true);
|
||||
awtAppClassNameField.set(xToolkit, "XPipe");
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
}
|
||||
}
|
||||
|
||||
AppWindowHelper.addIcons(stage);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.xpipe.core.process.ProcessControlProvider;
|
|||
import io.xpipe.core.util.ModuleHelper;
|
||||
import io.xpipe.core.util.ModuleLayerLoader;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
|
@ -95,12 +96,16 @@ public class AppExtensionManager {
|
|||
}
|
||||
|
||||
var iv = getLocalInstallVersion();
|
||||
var installVersion = AppVersion.parse(iv).orElseThrow(() -> new IllegalArgumentException("Invalid installation version: " + iv));
|
||||
var sv = !AppProperties.get().isImage() ? Files.readString(Path.of("version")).trim() : AppProperties.get().getVersion();
|
||||
var sourceVersion = AppVersion.parse(sv).orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
|
||||
var installVersion = AppVersion.parse(iv)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid installation version: " + iv));
|
||||
var sv = !AppProperties.get().isImage()
|
||||
? Files.readString(Path.of("version")).trim()
|
||||
: AppProperties.get().getVersion();
|
||||
var sourceVersion = AppVersion.parse(sv)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
|
||||
if (!installVersion.equals(sourceVersion)) {
|
||||
throw new IllegalStateException(
|
||||
"Incompatible development version. Source: " + iv + ", Installation: " + sv + "\n\nPlease try to check out the matching release version in the repository.");
|
||||
throw new IllegalStateException("Incompatible development version. Source: " + iv + ", Installation: "
|
||||
+ sv + "\n\nPlease try to check out the matching release version in the repository.");
|
||||
}
|
||||
|
||||
var extensions = XPipeInstallation.getLocalExtensionsDirectory(p);
|
||||
|
@ -171,8 +176,8 @@ public class AppExtensionManager {
|
|||
|
||||
if (loadedExtensions.stream().anyMatch(extension -> dir.equals(extension.dir))
|
||||
|| loadedExtensions.stream()
|
||||
.anyMatch(extension ->
|
||||
extension.id.equals(dir.getFileName().toString()))) {
|
||||
.anyMatch(extension ->
|
||||
extension.id.equals(dir.getFileName().toString()))) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,21 +6,17 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
|||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.prefs.SupportedLocale;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.app.util.Translatable;
|
||||
import io.xpipe.core.util.ModuleHelper;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.Value;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.ocpsoft.prettytime.PrettyTime;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
@ -90,6 +86,10 @@ public class AppI18n {
|
|||
private static String getCallerModuleName() {
|
||||
var callers = CallingClass.INSTANCE.getCallingClasses();
|
||||
for (Class<?> caller : callers) {
|
||||
if (caller.isSynthetic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (caller.equals(CallingClass.class)
|
||||
|| caller.equals(ModuleHelper.class)
|
||||
|| caller.equals(ModalOverlayComp.class)
|
||||
|
@ -127,7 +127,7 @@ public class AppI18n {
|
|||
}
|
||||
}
|
||||
|
||||
private LoadedTranslations getLoaded() {
|
||||
public LoadedTranslations getLoaded() {
|
||||
return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
|
||||
}
|
||||
|
||||
|
@ -274,21 +274,15 @@ public class AppI18n {
|
|||
});
|
||||
}
|
||||
|
||||
var prettyTime = new PrettyTime(
|
||||
AppPrefs.get() != null
|
||||
? AppPrefs.get().language().getValue().getLocale()
|
||||
: SupportedLocale.getEnglish().getLocale());
|
||||
|
||||
return new LoadedTranslations(l, translations, markdownDocumentations, prettyTime);
|
||||
return new LoadedTranslations(l, translations, markdownDocumentations);
|
||||
}
|
||||
|
||||
@Value
|
||||
static class LoadedTranslations {
|
||||
public static class LoadedTranslations {
|
||||
|
||||
Locale locale;
|
||||
Map<String, String> translations;
|
||||
Map<String, String> markdownDocumentations;
|
||||
PrettyTime prettyTime;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
|
|
97
app/src/main/java/io/xpipe/app/core/AppIntegration.java
Normal file
97
app/src/main/java/io/xpipe/app/core/AppIntegration.java
Normal file
|
@ -0,0 +1,97 @@
|
|||
package io.xpipe.app.core;
|
||||
|
||||
import io.xpipe.app.Main;
|
||||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.launcher.LauncherInput;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.desktop.*;
|
||||
import java.util.List;
|
||||
|
||||
public class AppIntegration {
|
||||
|
||||
public static void setupDesktopIntegrations() {
|
||||
try {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
Desktop.getDesktop().addAppEventListener(new SystemSleepListener() {
|
||||
@Override
|
||||
public void systemAboutToSleep(SystemSleepEvent e) {
|
||||
if (AppPrefs.get() != null &&
|
||||
AppPrefs.get().lockVaultOnHibernation().get() &&
|
||||
AppPrefs.get().getLockCrypt().get() != null &&
|
||||
!AppPrefs.get().getLockCrypt().get().isBlank()) {
|
||||
// If we run this at the same time as the system is sleeping, there might be exceptions
|
||||
// because the platform does not like being shut down while sleeping
|
||||
// This hopefully assures that it will be run later, probably on system wake
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ThreadHelper.sleep(1000);
|
||||
OperationMode.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void systemAwoke(SystemSleepEvent e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// This will initialize the toolkit on macos and create the dock icon
|
||||
// macOS does not like applications that run fully in the background, so always do it
|
||||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||
Desktop.getDesktop().setPreferencesHandler(e -> {
|
||||
AppLayoutModel.get().selectSettings();
|
||||
});
|
||||
|
||||
// URL open operations have to be handled in a special way on macOS!
|
||||
Desktop.getDesktop().setOpenURIHandler(e -> {
|
||||
LauncherInput.handle(List.of(e.getURI().toString()));
|
||||
});
|
||||
|
||||
// Do it this way to prevent IDE inspections from complaining
|
||||
var c = Class.forName(
|
||||
ModuleLayer.boot().findModule("java.desktop").orElseThrow(), "com.apple.eawt.Application");
|
||||
var m = c.getDeclaredMethod("addAppEventListener", SystemEventListener.class);
|
||||
m.invoke(c.getMethod("getApplication").invoke(null), new AppReopenedListener() {
|
||||
@Override
|
||||
public void appReopened(AppReopenedEvent e) {
|
||||
OperationMode.switchToAsync(OperationMode.GUI);
|
||||
}
|
||||
});
|
||||
|
||||
// Set dock icon explicitly on mac
|
||||
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
|
||||
if (AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
|
||||
try {
|
||||
var iconUrl = Main.class.getResourceAsStream("resources/img/logo/padded/logo_128x128.png");
|
||||
if (iconUrl != null) {
|
||||
var awtIcon = ImageIO.read(iconUrl);
|
||||
Taskbar.getTaskbar().setIconImage(awtIcon);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (OsType.getLocal().equals(OsType.LINUX)) {
|
||||
try {
|
||||
Toolkit xToolkit = Toolkit.getDefaultToolkit();
|
||||
java.lang.reflect.Field awtAppClassNameField =
|
||||
xToolkit.getClass().getDeclaredField("awtAppClassName");
|
||||
awtAppClassNameField.setAccessible(true);
|
||||
awtAppClassNameField.set(xToolkit, "XPipe");
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable ex) {
|
||||
ErrorEvent.fromThrowable(ex).term().handle();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,7 +55,9 @@ public class AppProperties {
|
|||
.orElse(UUID.randomUUID());
|
||||
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
|
||||
arch = System.getProperty("io.xpipe.app.arch");
|
||||
languages = Arrays.stream(System.getProperty("io.xpipe.app.languages").split(";")).sorted().toList();
|
||||
languages = Arrays.stream(System.getProperty("io.xpipe.app.languages").split(","))
|
||||
.sorted()
|
||||
.toList();
|
||||
staging = XPipeInstallation.isStaging();
|
||||
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
|
||||
.map(Boolean::parseBoolean)
|
||||
|
|
|
@ -65,7 +65,7 @@ public class AppAvCheck {
|
|||
alert.getDialogPane().setPadding(new Insets(15));
|
||||
});
|
||||
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
|
||||
});
|
||||
a.filter(b -> b.getButtonData().isDefaultButton())
|
||||
.ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1));
|
||||
|
|
|
@ -54,8 +54,8 @@ public class AppShellCheck {
|
|||
The most likely causes are:
|
||||
- On Windows, an AntiVirus program might block required programs and commands
|
||||
- The system shell is restricted or blocked
|
||||
- Your PATH environment variable is corrupt / incomplete. You can check this by manually trying to run some commands in a terminal
|
||||
- Some elementary command-line tools are not available or not working correctly
|
||||
- Your PATH environment variable is corrupt / incomplete
|
||||
|
||||
%s
|
||||
"""
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.app.core.mode;
|
||||
|
||||
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.app.browser.file.LocalFileSystem;
|
||||
import io.xpipe.app.browser.icon.FileIconManager;
|
||||
import io.xpipe.app.core.App;
|
||||
import io.xpipe.app.core.AppGreetings;
|
||||
|
@ -12,7 +11,6 @@ import io.xpipe.app.issue.ErrorEvent;
|
|||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.update.UpdateChangelogAlert;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class GuiMode extends PlatformMode {
|
||||
|
@ -52,11 +50,13 @@ public class GuiMode extends PlatformMode {
|
|||
});
|
||||
TrackEvent.info("Window setup complete");
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
BrowserIconFileType.loadDefinitions();
|
||||
BrowserIconDirectoryType.loadDefinitions();
|
||||
// Can be loaded async
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
FileIconManager.loadIfNecessary();
|
||||
});
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
LocalFileSystem.init();
|
||||
});
|
||||
|
||||
UpdateChangelogAlert.showIfNeeded();
|
||||
}
|
||||
|
|
|
@ -1,32 +1,23 @@
|
|||
package io.xpipe.app.core.mode;
|
||||
|
||||
import io.xpipe.app.Main;
|
||||
import io.xpipe.app.core.App;
|
||||
import io.xpipe.app.core.AppLogs;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.core.AppState;
|
||||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.core.check.AppDebugModeCheck;
|
||||
import io.xpipe.app.core.check.AppTempCheck;
|
||||
import io.xpipe.app.core.check.AppUserDirectoryCheck;
|
||||
import io.xpipe.app.issue.*;
|
||||
import io.xpipe.app.launcher.LauncherCommand;
|
||||
import io.xpipe.app.launcher.LauncherInput;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.app.util.XPipeSession;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.util.FailableRunnable;
|
||||
import io.xpipe.core.util.XPipeDaemonMode;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.desktop.AppReopenedEvent;
|
||||
import java.awt.desktop.AppReopenedListener;
|
||||
import java.awt.desktop.SystemEventListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -130,47 +121,7 @@ public abstract class OperationMode {
|
|||
setup(args);
|
||||
LauncherCommand.runLauncher(usedArgs);
|
||||
inStartup = false;
|
||||
postInit(args);
|
||||
}
|
||||
|
||||
public static void postInit(String[] args) {
|
||||
try {
|
||||
// This will initialize the toolkit on macos and create the dock icon
|
||||
// macOS does not like applications that run fully in the background, so always do it
|
||||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||
// URL open operations have to be handled in a special way on macOS!
|
||||
Desktop.getDesktop().setOpenURIHandler(e -> {
|
||||
LauncherInput.handle(List.of(e.getURI().toString()));
|
||||
});
|
||||
|
||||
// Do it this way to prevent IDE inspections from complaining
|
||||
var c = Class.forName(
|
||||
ModuleLayer.boot().findModule("java.desktop").orElseThrow(), "com.apple.eawt.Application");
|
||||
var m = c.getDeclaredMethod("addAppEventListener", SystemEventListener.class);
|
||||
m.invoke(c.getMethod("getApplication").invoke(null), new AppReopenedListener() {
|
||||
@Override
|
||||
public void appReopened(AppReopenedEvent e) {
|
||||
OperationMode.switchToAsync(OperationMode.GUI);
|
||||
}
|
||||
});
|
||||
|
||||
// Set dock icon explicitly on mac
|
||||
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
|
||||
if (AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
|
||||
try {
|
||||
var iconUrl = Main.class.getResourceAsStream("resources/img/logo/padded/logo_128x128.png");
|
||||
if (iconUrl != null) {
|
||||
var awtIcon = ImageIO.read(iconUrl);
|
||||
Taskbar.getTaskbar().setIconImage(awtIcon);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
ErrorEvent.fromThrowable(ex).term().handle();
|
||||
}
|
||||
AppIntegration.setupDesktopIntegrations();
|
||||
}
|
||||
|
||||
public static void switchToAsync(OperationMode newMode) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.xpipe.app.fxcomps.augment.Augment;
|
|||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.fxcomps.util.Shortcuts;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
@ -148,13 +149,15 @@ public abstract class Comp<S extends CompStructure<?>> {
|
|||
var region = struc.get();
|
||||
BindingsHelper.preserve(region, o);
|
||||
o.subscribe(n -> {
|
||||
if (!n) {
|
||||
region.setVisible(true);
|
||||
region.setManaged(true);
|
||||
} else {
|
||||
region.setVisible(false);
|
||||
region.setManaged(false);
|
||||
}
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
if (!n) {
|
||||
region.setVisible(true);
|
||||
region.setManaged(true);
|
||||
} else {
|
||||
region.setVisible(false);
|
||||
region.setManaged(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -206,7 +206,12 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
16);
|
||||
struc.get().setGraphic(graphic.createRegion());
|
||||
struc.get().setOnAction(event -> {
|
||||
getPopover().show(struc.get());
|
||||
if (popover == null || !popover.isShowing()) {
|
||||
var p = getPopover();
|
||||
p.show(struc.get());
|
||||
} else {
|
||||
popover.hide();
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
|
|
|
@ -28,6 +28,15 @@ public class PrettyImageHelper {
|
|||
return ofFixedSize(img, size, size);
|
||||
}
|
||||
|
||||
public static Comp<?> ofFixedRasterized(String img, int w, int h) {
|
||||
if (img == null) {
|
||||
return new PrettyImageComp(new SimpleStringProperty(null), w, h);
|
||||
}
|
||||
|
||||
var rasterized = rasterizedImageIfExists(img, w, h);
|
||||
return new PrettyImageComp(new SimpleStringProperty(rasterized.orElse(null)), w, h);
|
||||
}
|
||||
|
||||
public static Comp<?> ofFixedSize(String img, int w, int h) {
|
||||
if (img == null) {
|
||||
return new PrettyImageComp(new SimpleStringProperty(null), w, h);
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
package io.xpipe.app.fxcomps.impl;
|
||||
|
||||
import atlantafx.base.layout.InputGroup;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.CompStructure;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.util.ClipboardHelper;
|
||||
import io.xpipe.core.util.InPlaceSecretValue;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
import atlantafx.base.layout.InputGroup;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
@ -27,6 +29,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
public static class Structure implements CompStructure<InputGroup> {
|
||||
|
||||
private final InputGroup inputGroup;
|
||||
|
||||
@Getter
|
||||
private final TextField field;
|
||||
|
||||
|
@ -85,8 +88,11 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
|
|||
HBox.setHgrow(text, Priority.ALWAYS);
|
||||
|
||||
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
|
||||
ClipboardHelper.copyPassword(value.getValue());
|
||||
}).grow(false, true).tooltipKey("copyPassword").createRegion();
|
||||
ClipboardHelper.copyPassword(value.getValue());
|
||||
})
|
||||
.grow(false, true)
|
||||
.tooltipKey("copyPassword")
|
||||
.createRegion();
|
||||
|
||||
var ig = new InputGroup(text);
|
||||
ig.getStyleClass().add("secret-field-comp");
|
||||
|
|
|
@ -23,6 +23,9 @@ public class ErrorEvent {
|
|||
@Builder.Default
|
||||
private final boolean reportable = true;
|
||||
|
||||
@Setter
|
||||
private boolean disableDefaultActions;
|
||||
|
||||
private final Throwable throwable;
|
||||
|
||||
@Singular
|
||||
|
@ -153,6 +156,10 @@ public class ErrorEvent {
|
|||
return omit().expected();
|
||||
}
|
||||
|
||||
public ErrorEventBuilder noDefaultActions() {
|
||||
return disableDefaultActions(true);
|
||||
}
|
||||
|
||||
public void handle() {
|
||||
build().handle();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import io.xpipe.app.fxcomps.SimpleComp;
|
|||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.app.util.LicenseRequiredException;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
@ -25,7 +24,6 @@ import javafx.scene.layout.Region;
|
|||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -243,25 +241,24 @@ public class ErrorHandlerComp extends SimpleComp {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
event.setDisableDefaultActions(true);
|
||||
}
|
||||
|
||||
var custom = event.getCustomActions();
|
||||
for (var c : custom) {
|
||||
var ac = createActionComp(c);
|
||||
ac.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
|
||||
actionBox.getChildren().add(ac);
|
||||
}
|
||||
|
||||
for (var action : List.of(ErrorAction.automaticallyReport(), ErrorAction.reportOnGithub())) {
|
||||
var ac = createActionComp(action);
|
||||
actionBox.getChildren().add(ac);
|
||||
if (!event.isDisableDefaultActions() || event.getCustomActions().isEmpty()) {
|
||||
for (var action : List.of(ErrorAction.automaticallyReport(), ErrorAction.reportOnGithub(), ErrorAction.ignore())) {
|
||||
var ac = createActionComp(action);
|
||||
actionBox.getChildren().add(ac);
|
||||
}
|
||||
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
|
||||
}
|
||||
|
||||
for (var action : List.of(ErrorAction.ignore())) {
|
||||
var ac = createActionComp(action);
|
||||
actionBox.getChildren().add(ac);
|
||||
}
|
||||
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
|
||||
|
||||
content.getChildren().addAll(actionBox, new Separator(Orientation.HORIZONTAL));
|
||||
|
||||
var details = createDetails();
|
||||
|
|
|
@ -4,8 +4,8 @@ import io.xpipe.app.core.*;
|
|||
import io.xpipe.app.core.mode.OperationMode;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package io.xpipe.app.issue;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.ListSelectorComp;
|
||||
import io.xpipe.app.comp.base.MarkdownComp;
|
||||
|
@ -9,6 +7,7 @@ import io.xpipe.app.comp.base.TitledPaneComp;
|
|||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
@ -23,6 +22,9 @@ import javafx.scene.control.TextField;
|
|||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
|
|
|
@ -96,6 +96,8 @@ public class AppPrefs {
|
|||
map(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class);
|
||||
final BooleanProperty showChildCategoriesInParentCategory =
|
||||
map(new SimpleBooleanProperty(true), "showChildrenConnectionsInParentCategory", Boolean.class);
|
||||
final BooleanProperty lockVaultOnHibernation =
|
||||
map(new SimpleBooleanProperty(false), "lockVaultOnHibernation", Boolean.class);
|
||||
final BooleanProperty openConnectionSearchWindowOnConnectionCreation =
|
||||
map(new SimpleBooleanProperty(true), "openConnectionSearchWindowOnConnectionCreation", Boolean.class);
|
||||
final ObjectProperty<Path> storageDirectory =
|
||||
|
@ -139,21 +141,22 @@ public class AppPrefs {
|
|||
|
||||
private AppPrefs() {
|
||||
this.categories = Stream.of(
|
||||
new AboutCategory(),
|
||||
new SystemCategory(),
|
||||
new AppearanceCategory(),
|
||||
new SyncCategory(),
|
||||
new VaultCategory(),
|
||||
new PasswordManagerCategory(),
|
||||
new TerminalCategory(),
|
||||
new EditorCategory(),
|
||||
new RdpCategory(),
|
||||
new SshCategory(),
|
||||
new LocalShellCategory(),
|
||||
new SecurityCategory(),
|
||||
new TroubleshootCategory(),
|
||||
new DeveloperCategory())
|
||||
.filter(appPrefsCategory -> appPrefsCategory.show()).toList();
|
||||
new AboutCategory(),
|
||||
new SystemCategory(),
|
||||
new AppearanceCategory(),
|
||||
new SyncCategory(),
|
||||
new VaultCategory(),
|
||||
new PasswordManagerCategory(),
|
||||
new TerminalCategory(),
|
||||
new EditorCategory(),
|
||||
new RdpCategory(),
|
||||
new SshCategory(),
|
||||
new LocalShellCategory(),
|
||||
new SecurityCategory(),
|
||||
new TroubleshootCategory(),
|
||||
new DeveloperCategory())
|
||||
.filter(appPrefsCategory -> appPrefsCategory.show())
|
||||
.toList();
|
||||
var selected = AppCache.get("selectedPrefsCategory", Integer.class, () -> 0);
|
||||
if (selected == null) {
|
||||
selected = 0;
|
||||
|
@ -262,6 +265,10 @@ public class AppPrefs {
|
|||
return disableTerminalRemotePasswordPreparation;
|
||||
}
|
||||
|
||||
public ObservableBooleanValue lockVaultOnHibernation() {
|
||||
return lockVaultOnHibernation;
|
||||
}
|
||||
|
||||
public ObservableValue<Boolean> alwaysConfirmElevation() {
|
||||
return alwaysConfirmElevation;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import io.xpipe.core.process.ShellControl;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class ExternalApplicationType implements PrefsChoiceValue {
|
||||
|
@ -43,46 +42,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
|
|||
this.applicationName = applicationName;
|
||||
}
|
||||
|
||||
protected Optional<Path> getApplicationPath() {
|
||||
try (ShellControl pc = LocalShell.getShell().start()) {
|
||||
try (var c = pc.command(String.format(
|
||||
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister "
|
||||
+ "-dump | grep -o \"/.*%s.app\" | grep -v -E \"Caches|TimeMachine|Temporary|.Trash|/Volumes/%s\" | uniq",
|
||||
applicationName, applicationName))
|
||||
.start()) {
|
||||
var path = c.readStdoutDiscardErr();
|
||||
if (c.getExitCode() != 0 || path.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Check if returned paths are actually valid
|
||||
// Also sort them by length to prevent finding a deeply buried app
|
||||
var valid = path.lines()
|
||||
.filter(s -> {
|
||||
try {
|
||||
return Files.exists(Path.of(s));
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.sorted(Comparator.comparingInt(value -> value.length()))
|
||||
.toList();
|
||||
|
||||
// Require app in proper applications directory
|
||||
var app = valid.stream()
|
||||
.filter(s -> s.contains("Applications"))
|
||||
.findFirst();
|
||||
return app.map(Path::of);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
return getApplicationPath().isPresent();
|
||||
try (ShellControl pc = LocalShell.getShell().start()) {
|
||||
return pc.command(String.format(
|
||||
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications",
|
||||
applicationName))
|
||||
.executeAndCheck();
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -194,14 +194,9 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
|
||||
@Override
|
||||
public void launch(Path file) throws Exception {
|
||||
var execFile = getApplicationPath();
|
||||
if (execFile.isEmpty()) {
|
||||
throw new IOException("Application " + applicationName + ".app not found");
|
||||
}
|
||||
|
||||
ExternalApplicationHelper.startAsync(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addFile(execFile.orElseThrow().toString())
|
||||
.addQuoted(applicationName)
|
||||
.addFile(file.toString()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import io.xpipe.app.issue.ErrorEvent;
|
|||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.nio.file.Files;
|
||||
|
@ -59,8 +59,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
|
|||
|
||||
private String encrypt(SecretValue password) throws Exception {
|
||||
var ps = LocalShell.getLocalPowershell();
|
||||
var cmd = ps.command(
|
||||
"(\"" + password.getSecretValue() + "\" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString;");
|
||||
var cmd = ps.command("(\"" + password.getSecretValue()
|
||||
+ "\" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString;");
|
||||
cmd.setSensitive();
|
||||
return cmd.readStdoutOrThrow();
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
.addComp(prefs.getCustomComp("gitVaultIdentityStrategy"))
|
||||
.nameAndDescription("openDataDir")
|
||||
.addComp(new ButtonComp(AppI18n.observable("openDataDirButton"), () -> {
|
||||
DesktopHelper.browsePath(DataStorage.get().getDataDir());
|
||||
DesktopHelper.browsePathLocal(DataStorage.get().getDataDir());
|
||||
})));
|
||||
return builder.buildComp();
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ public class TroubleshootCategory extends AppPrefsCategory {
|
|||
"openInstallationDirectoryDescription",
|
||||
"mdomz-snippet_folder",
|
||||
e -> {
|
||||
DesktopHelper.browsePath(
|
||||
DesktopHelper.browsePathLocal(
|
||||
XPipeInstallation.getCurrentInstallationBasePath());
|
||||
e.consume();
|
||||
})
|
||||
|
|
|
@ -52,7 +52,12 @@ public class VaultCategory extends AppPrefsCategory {
|
|||
},
|
||||
prefs.getLockCrypt()),
|
||||
LockChangeAlert::show),
|
||||
prefs.getLockCrypt()));
|
||||
prefs.getLockCrypt())
|
||||
.nameAndDescription("lockVaultOnHibernation")
|
||||
.addToggle(prefs.lockVaultOnHibernation)
|
||||
.hide(prefs.getLockCrypt()
|
||||
.isNull()
|
||||
.or(prefs.getLockCrypt().isEmpty())));
|
||||
return builder.buildComp();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ import io.xpipe.app.util.FixedHierarchyStore;
|
|||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.*;
|
||||
import io.xpipe.core.util.UuidHelper;
|
||||
|
||||
import javafx.util.Pair;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
|
@ -311,8 +313,8 @@ public abstract class DataStorage {
|
|||
DataStoreCategory p = category;
|
||||
if (share) {
|
||||
while ((p = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(p.getParentCategory())
|
||||
.orElse(null))
|
||||
.getStoreCategoryIfPresent(p.getParentCategory())
|
||||
.orElse(null))
|
||||
!= null) {
|
||||
p.setShare(true);
|
||||
}
|
||||
|
@ -428,7 +430,8 @@ public abstract class DataStorage {
|
|||
// Children classes might not be the same, the same goes for state classes
|
||||
// This can happen when there are multiple child classes and the ids got switched around
|
||||
if (classMatch) {
|
||||
DataStore merged = ((FixedChildStore) pair.getKey().getStore()).merge(pair.getValue().getStore().asNeeded());
|
||||
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
|
||||
.merge(pair.getValue().getStore().asNeeded());
|
||||
if (merged != pair.getKey().getStore()) {
|
||||
pair.getKey().setStoreInternal(merged, false);
|
||||
}
|
||||
|
@ -726,7 +729,7 @@ public abstract class DataStorage {
|
|||
return children;
|
||||
}
|
||||
|
||||
private List<DataStoreEntry> getHierarchy(DataStoreEntry entry) {
|
||||
public List<DataStoreEntry> getStoreParentHierarchy(DataStoreEntry entry) {
|
||||
var es = new ArrayList<DataStoreEntry>();
|
||||
es.add(entry);
|
||||
|
||||
|
@ -743,7 +746,7 @@ public abstract class DataStorage {
|
|||
}
|
||||
|
||||
public DataStoreId getId(DataStoreEntry entry) {
|
||||
return DataStoreId.create(getHierarchy(entry).stream()
|
||||
return DataStoreId.create(getStoreParentHierarchy(entry).stream()
|
||||
.filter(e -> !(e.getStore() instanceof LocalStore))
|
||||
.map(e -> e.getName().replaceAll(":", "_"))
|
||||
.toArray(String[]::new));
|
||||
|
@ -781,9 +784,11 @@ public abstract class DataStorage {
|
|||
public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) {
|
||||
return storeEntriesSet.stream()
|
||||
.filter(n -> n.getStore() == store
|
||||
|| (!identityOnly && (n.getStore() != null
|
||||
&& Objects.equals(store.getClass(), n.getStore().getClass())
|
||||
&& store.equals(n.getStore()))))
|
||||
|| (!identityOnly
|
||||
&& (n.getStore() != null
|
||||
&& Objects.equals(
|
||||
store.getClass(), n.getStore().getClass())
|
||||
&& store.equals(n.getStore()))))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
package io.xpipe.app.storage;
|
||||
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.core.store.*;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.core.store.*;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
import lombok.*;
|
||||
import lombok.experimental.NonFinal;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
|
|
@ -166,7 +166,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
ShellDialect systemDialect) {
|
||||
return launchConfiguration -> {
|
||||
var toExecute = CommandBuilder.of()
|
||||
.add(executable, "-v", "--title")
|
||||
|
@ -549,33 +550,23 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var app = this.getApplicationPath();
|
||||
if (app.isEmpty()) {
|
||||
throw new IllegalStateException("iTerm installation not found");
|
||||
}
|
||||
|
||||
try (ShellControl pc = LocalShell.getShell()) {
|
||||
var a = app.get().toString();
|
||||
pc.osascriptCommand(String.format(
|
||||
"""
|
||||
if application "%s" is not running then
|
||||
launch application "%s"
|
||||
if application "iTerm" is not running then
|
||||
launch application "iTerm"
|
||||
delay 1
|
||||
tell application "%s"
|
||||
tell application "iTerm"
|
||||
tell current tab of current window
|
||||
close
|
||||
end tell
|
||||
end tell
|
||||
end if
|
||||
tell application "%s"
|
||||
tell application "iTerm"
|
||||
activate
|
||||
create window with default profile command "%s"
|
||||
end tell
|
||||
""",
|
||||
a,
|
||||
a,
|
||||
a,
|
||||
a,
|
||||
configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"")))
|
||||
.execute();
|
||||
}
|
||||
|
@ -618,7 +609,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
ShellDialect systemDialect) {
|
||||
return launchConfiguration -> {
|
||||
var toExecute = CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
@ -816,7 +808,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
ShellDialect systemDialect) {
|
||||
return launchConfiguration -> {
|
||||
var args = toCommand(launchConfiguration);
|
||||
args.add(0, executable);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.CommandSupport;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
|
@ -11,6 +10,8 @@ import io.xpipe.core.process.ShellControl;
|
|||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
|
||||
public interface KittyTerminalType extends ExternalTerminalType {
|
||||
|
||||
ExternalTerminalType KITTY_LINUX = new Linux();
|
||||
|
|
|
@ -36,23 +36,23 @@ public interface TabbyTerminalType extends ExternalTerminalType {
|
|||
|
||||
@Override
|
||||
default TerminalInitFunction additionalInitCommands() {
|
||||
// return TerminalInitFunction.of(sc -> {
|
||||
// if (sc.getShellDialect() == ShellDialects.ZSH) {
|
||||
// return "export PS1=\"$PS1\\[\\e]1337;CurrentDir=\"'$(pwd)\\a\\]'";
|
||||
// }
|
||||
// if (sc.getShellDialect() == ShellDialects.BASH) {
|
||||
// return "precmd () { echo -n \"\\x1b]1337;CurrentDir=$(pwd)\\x07\" }";
|
||||
// }
|
||||
// if (sc.getShellDialect() == ShellDialects.FISH) {
|
||||
// return """
|
||||
// function __tabby_working_directory_reporting --on-event fish_prompt
|
||||
// echo -en "\\e]1337;CurrentDir=$PWD\\x7"
|
||||
// end
|
||||
// """;
|
||||
// }
|
||||
// return null;
|
||||
// });
|
||||
return null;
|
||||
// return TerminalInitFunction.of(sc -> {
|
||||
// if (sc.getShellDialect() == ShellDialects.ZSH) {
|
||||
// return "export PS1=\"$PS1\\[\\e]1337;CurrentDir=\"'$(pwd)\\a\\]'";
|
||||
// }
|
||||
// if (sc.getShellDialect() == ShellDialects.BASH) {
|
||||
// return "precmd () { echo -n \"\\x1b]1337;CurrentDir=$(pwd)\\x07\" }";
|
||||
// }
|
||||
// if (sc.getShellDialect() == ShellDialects.FISH) {
|
||||
// return """
|
||||
// function __tabby_working_directory_reporting --on-event fish_prompt
|
||||
// echo -en "\\e]1337;CurrentDir=$PWD\\x7"
|
||||
// end
|
||||
// """;
|
||||
// }
|
||||
// return null;
|
||||
// });
|
||||
return TerminalInitFunction.none();
|
||||
}
|
||||
|
||||
class Windows extends ExternalTerminalType.WindowsType implements TabbyTerminalType {
|
||||
|
|
|
@ -81,9 +81,13 @@ public interface WezTerminalType extends ExternalTerminalType {
|
|||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
var path = LocalShell.getShell()
|
||||
.command(String.format(
|
||||
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
|
||||
applicationName))
|
||||
.readStdoutOrThrow();
|
||||
var c = CommandBuilder.of()
|
||||
.addFile(getApplicationPath()
|
||||
.orElseThrow()
|
||||
.addFile(Path.of(path)
|
||||
.resolve("Contents")
|
||||
.resolve("MacOS")
|
||||
.resolve("wezterm-gui")
|
||||
|
|
|
@ -49,7 +49,7 @@ public class UpdateChangelogAlert {
|
|||
var markdown = new MarkdownComp(update.getRawDescription(), s -> " " + s).createRegion();
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
|
||||
((Stage) alert.getDialogPane().getScene().getWindow()).setAlwaysOnTop(true);
|
||||
},
|
||||
r -> r.filter(b -> b.getButtonData().isDefaultButton()).ifPresent(t -> {}));
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.util;
|
|||
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.core.util.SecretValue;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
|
@ -21,9 +22,16 @@ public class ClipboardHelper {
|
|||
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
Map<DataFormat, Object> previous = Stream.of(DataFormat.PLAIN_TEXT, DataFormat.URL, DataFormat.RTF, DataFormat.HTML, DataFormat.IMAGE, DataFormat.FILES)
|
||||
.map(dataFormat -> new AbstractMap.SimpleEntry<>(dataFormat, clipboard.getContent(dataFormat))).filter(o -> o.getValue() != null)
|
||||
.collect(HashMap::new, (m,v)->m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
||||
Map<DataFormat, Object> previous = Stream.of(
|
||||
DataFormat.PLAIN_TEXT,
|
||||
DataFormat.URL,
|
||||
DataFormat.RTF,
|
||||
DataFormat.HTML,
|
||||
DataFormat.IMAGE,
|
||||
DataFormat.FILES)
|
||||
.map(dataFormat -> new AbstractMap.SimpleEntry<>(dataFormat, clipboard.getContent(dataFormat)))
|
||||
.filter(o -> o.getValue() != null)
|
||||
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
||||
|
||||
var withPassword = new HashMap<>(previous);
|
||||
withPassword.put(DataFormat.PLAIN_TEXT, pass.getSecretValue());
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.xpipe.core.process.ShellDialects;
|
|||
import io.xpipe.core.process.ShellStoreState;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.function.IntFunction;
|
||||
|
@ -32,7 +33,8 @@ public class DataStoreFormatter {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (s.getShellDialect() != null && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
|
||||
if (s.getShellDialect() != null
|
||||
&& !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
|
||||
if (s.getOsName() != null) {
|
||||
return formattedOsName(s.getOsName());
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package io.xpipe.app.util;
|
|||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
|
||||
import java.awt.*;
|
||||
import java.nio.file.Files;
|
||||
|
@ -26,7 +28,37 @@ public class DesktopHelper {
|
|||
return Path.of(System.getProperty("user.home") + "/Desktop");
|
||||
}
|
||||
|
||||
public static void browsePath(Path file) {
|
||||
public static void browsePathRemote(ShellControl sc, String path, FileKind kind) throws Exception {
|
||||
var d = sc.getShellDialect();
|
||||
switch (sc.getOsType()) {
|
||||
case OsType.Windows windows -> {
|
||||
// Explorer does not support single quotes, so use normal quotes
|
||||
if (kind == FileKind.DIRECTORY) {
|
||||
sc.executeSimpleCommand("explorer " + d.quoteArgument(path));
|
||||
} else {
|
||||
sc.executeSimpleCommand("explorer /select," + d.quoteArgument(path));
|
||||
}
|
||||
}
|
||||
case OsType.Linux linux -> {
|
||||
var action = kind == FileKind.DIRECTORY
|
||||
? "org.freedesktop.FileManager1.ShowFolders"
|
||||
: "org.freedesktop.FileManager1.ShowItems";
|
||||
var dbus = String.format(
|
||||
"""
|
||||
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:"file://%s" string:""
|
||||
""",
|
||||
action, path);
|
||||
sc.executeSimpleCommand(dbus);
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
sc.executeSimpleCommand("open " + (kind == FileKind.DIRECTORY ? "" : "-R ") + d.fileArgument(path));
|
||||
}
|
||||
case OsType.Bsd bsd -> {}
|
||||
case OsType.Solaris solaris -> {}
|
||||
}
|
||||
}
|
||||
|
||||
public static void browsePathLocal(Path file) {
|
||||
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.util;
|
|||
|
||||
import java.text.CharacterIterator;
|
||||
import java.text.StringCharacterIterator;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
@ -80,4 +81,13 @@ public final class HumanReadableFormat {
|
|||
private static int getWeekNumber(LocalDateTime date) {
|
||||
return date.get(WeekFields.of(Locale.getDefault()).weekOfYear());
|
||||
}
|
||||
|
||||
|
||||
public static String duration(Duration duration) {
|
||||
return duration.toString()
|
||||
.substring(2)
|
||||
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
|
||||
.replaceAll("\\.\\d+", "")
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,6 @@ public enum PlatformState {
|
|||
try {
|
||||
// Weird fix to ensure that macOS quit operation works while in tray.
|
||||
// Maybe related to https://bugs.openjdk.org/browse/JDK-8318129 as it prints the same error if not called
|
||||
// The headless check is not needed though but still done
|
||||
GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
|
||||
|
||||
// Catch more than just the headless exception in case the graphics environment initialization completely
|
||||
|
@ -84,9 +83,9 @@ public enum PlatformState {
|
|||
+ " You don't have to install XPipe on any system like a server, a WSL distribution, a hypervisor, etc.,"
|
||||
+ " to have full access to that system, a shell connection to it is enough for XPipe to work from your local machine."
|
||||
: h.getMessage();
|
||||
TrackEvent.warn(h.getMessage());
|
||||
TrackEvent.warn(msg);
|
||||
PlatformState.setCurrent(PlatformState.EXITED);
|
||||
return Optional.of(ErrorEvent.expected(new HeadlessException(msg)));
|
||||
return Optional.of(ErrorEvent.expected(new UnsupportedOperationException(msg)));
|
||||
} catch (Throwable t) {
|
||||
TrackEvent.warn(t.getMessage());
|
||||
PlatformState.setCurrent(PlatformState.EXITED);
|
||||
|
|
|
@ -90,12 +90,12 @@ public class SecretRetrievalStrategyHelper {
|
|||
new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.CustomCommand i ? i : null);
|
||||
var map = new LinkedHashMap<String, OptionsBuilder>();
|
||||
if (allowNone) {
|
||||
map.put("none", new OptionsBuilder());
|
||||
map.put("app.none", new OptionsBuilder());
|
||||
}
|
||||
map.put("password", inPlace(inPlace));
|
||||
map.put("passwordManager", passwordManager(passwordManager));
|
||||
map.put("customCommand", customCommand(customCommand));
|
||||
map.put("prompt", new OptionsBuilder());
|
||||
map.put("app.password", inPlace(inPlace));
|
||||
map.put("app.passwordManager", passwordManager(passwordManager));
|
||||
map.put("app.customCommand", customCommand(customCommand));
|
||||
map.put("app.prompt", new OptionsBuilder());
|
||||
|
||||
int offset = allowNone ? 0 : -1;
|
||||
var selected = new SimpleIntegerProperty(
|
||||
|
|
|
@ -32,7 +32,7 @@ public class TerminalLauncher {
|
|||
title,
|
||||
type.shouldClear()
|
||||
&& AppPrefs.get().clearTerminalOnInit().get(),
|
||||
null),
|
||||
TerminalInitFunction.none()),
|
||||
true);
|
||||
var config = new ExternalTerminalType.LaunchConfiguration(null, title, title, script, sc.getShellDialect());
|
||||
type.launch(config);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package io.xpipe.app.util;
|
||||
|
||||
import com.sun.jna.platform.win32.Advapi32Util;
|
||||
import com.sun.jna.platform.win32.WinReg;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
import com.sun.jna.platform.win32.Advapi32Util;
|
||||
import com.sun.jna.platform.win32.WinReg;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class WindowsRegistry {
|
||||
|
@ -56,7 +57,8 @@ public class WindowsRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
public static Optional<String> findRemoteValuesRecursive(ShellControl shellControl, int hkey, String key, String valueName) throws Exception {
|
||||
public static Optional<String> findRemoteValuesRecursive(
|
||||
ShellControl shellControl, int hkey, String key, String valueName) throws Exception {
|
||||
var command = CommandBuilder.of()
|
||||
.add("reg", "query")
|
||||
.addQuoted((hkey == HKEY_LOCAL_MACHINE ? "HKEY_LOCAL_MACHINE" : "HKEY_CURRENT_USER") + "\\" + key)
|
||||
|
|
|
@ -49,7 +49,6 @@ open module io.xpipe.app {
|
|||
requires org.slf4j;
|
||||
requires org.slf4j.jdk.platform.logging;
|
||||
requires atlantafx.base;
|
||||
requires org.ocpsoft.prettytime;
|
||||
requires com.vladsch.flexmark;
|
||||
requires com.fasterxml.jackson.core;
|
||||
requires com.fasterxml.jackson.databind;
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
}
|
||||
|
||||
.store-header-bar .menu-button .context-menu {
|
||||
-fx-padding: 3px;
|
||||
-fx-background-radius: 4px;
|
||||
-fx-border-radius: 4px;
|
||||
-fx-border-color: -color-neutral-muted;
|
||||
-fx-padding: 0;
|
||||
-fx-background-radius: 0;
|
||||
-fx-border-radius: 0;
|
||||
-fx-border-color: -color-border-default;
|
||||
}
|
||||
|
||||
|
||||
.context-menu > * > * {
|
||||
-fx-padding: 3px 10px 3px 10px;
|
||||
-fx-background-radius: 1px;
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
/* Section */
|
||||
|
||||
.store-entry-section-comp .separator {
|
||||
.store-entry-section-comp > .separator {
|
||||
-fx-padding: 0 12px 0 35px;
|
||||
-fx-border-insets: 0px;
|
||||
}
|
||||
|
@ -103,7 +103,7 @@
|
|||
-fx-padding: 5px 0 5px 25px;
|
||||
}
|
||||
|
||||
.store-entry-section-comp .separator .line {
|
||||
.store-entry-section-comp > .separator .line {
|
||||
-fx-padding: 0;
|
||||
-fx-border-insets: 0px;
|
||||
-fx-background-color: -color-border-subtle;
|
||||
|
@ -111,7 +111,7 @@
|
|||
}
|
||||
|
||||
.root:pretty .top > .store-entry-section-comp {
|
||||
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2px, 0.5, 0, 1);
|
||||
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2, 0.5, 0, 1);
|
||||
}
|
||||
|
||||
.store-entry-section-comp:root {
|
||||
|
|
16
build.gradle
16
build.gradle
|
@ -2,6 +2,15 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
|||
|
||||
import java.util.stream.Stream
|
||||
|
||||
buildscript {
|
||||
configurations.classpath {
|
||||
resolutionStrategy {
|
||||
// Fix https://github.com/jreleaser/jreleaser/issues/1643
|
||||
force 'org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "io.codearte.nexus-staging" version "0.30.0"
|
||||
id 'org.gradlex.extra-java-module-info' version '1.8' apply false
|
||||
|
@ -85,11 +94,12 @@ project.ext {
|
|||
changelog = file("dist/changelogs/${canonicalVersionString}.md").exists() ? file("dist/changelogs/${canonicalVersionString}.md").text.trim() + '\n' : ""
|
||||
productName = isStage ? 'XPipe PTB' : 'XPipe'
|
||||
kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'
|
||||
flatcaseProductName = isStage ? 'xpipeptb' : 'xpipe'
|
||||
publisher = 'XPipe UG (haftungsbeschränkt)'
|
||||
shortDescription = 'Your entire server infrastructure at your fingertips'
|
||||
shortDescription = isStage ? 'XPipe PTB (Public Test Build)' : 'Your entire server infrastructure at your fingertips'
|
||||
longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'
|
||||
website = 'https://xpipe.io'
|
||||
sourceWebsite = 'https://github.com/xpipe-io/xpipe'
|
||||
sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'
|
||||
authors = 'Christopher Schnick'
|
||||
javafxVersion = '22.0.1'
|
||||
platformName = getPlatformName()
|
||||
|
@ -107,7 +117,7 @@ project.ext {
|
|||
"--add-opens", "javafx.graphics/com.sun.javafx.tk.quantum=io.xpipe.app",
|
||||
"-Xmx8g",
|
||||
"-Dio.xpipe.app.arch=$rootProject.arch",
|
||||
"-Dio.xpipe.app.languages=${String.join(";", languages)}",
|
||||
"-Dio.xpipe.app.languages=${String.join(",", languages)}",
|
||||
"-Dfile.encoding=UTF-8",
|
||||
// Disable this for now as it requires Windows 10+
|
||||
// '-XX:+UseZGC',
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.core.process;
|
||||
|
||||
import io.xpipe.core.util.FailableConsumer;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
@ -64,11 +63,7 @@ public interface CommandControl extends ProcessControl {
|
|||
|
||||
long getExitCode();
|
||||
|
||||
default CommandControl elevated(String message) {
|
||||
return elevated(message, (v) -> true);
|
||||
}
|
||||
|
||||
CommandControl elevated(String message, FailableFunction<ShellControl, Boolean, Exception> elevationFunction);
|
||||
CommandControl elevated(ElevationFunction function);
|
||||
|
||||
void withStdoutOrThrow(FailableConsumer<InputStreamReader, Exception> c);
|
||||
|
||||
|
@ -88,8 +83,12 @@ public interface CommandControl extends ProcessControl {
|
|||
|
||||
String readStdoutOrThrow() throws Exception;
|
||||
|
||||
String readStderrOrThrow() throws Exception;
|
||||
|
||||
String readStdoutAndWait() throws Exception;
|
||||
|
||||
String readStderrAndWait() throws Exception;
|
||||
|
||||
Optional<String> readStdoutIfPossible() throws Exception;
|
||||
|
||||
default boolean discardAndCheckExit() throws ProcessOutputException {
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package io.xpipe.core.process;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class ElevationConfig {
|
||||
|
||||
boolean requiresPassword;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package io.xpipe.core.process;
|
||||
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
public interface ElevationFunction {
|
||||
|
||||
static ElevationFunction of(String prefix, FailableFunction<ShellControl, Boolean, Exception> f) {
|
||||
return new ElevationFunction() {
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpecified() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(ShellControl shellControl) throws Exception {
|
||||
return f.apply(shellControl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static ElevationFunction elevated(String prefix) {
|
||||
return new ElevationFunction() {
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpecified() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(ShellControl shellControl) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static ElevationFunction none() {
|
||||
return new ElevationFunction() {
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpecified() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean apply(ShellControl shellControl) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
String getPrefix();
|
||||
|
||||
boolean isSpecified();
|
||||
|
||||
boolean apply(ShellControl shellControl) throws Exception;
|
||||
}
|
|
@ -171,7 +171,7 @@ public interface ShellControl extends ProcessControl {
|
|||
|
||||
OsType.Any getOsType();
|
||||
|
||||
ShellControl elevated(String message, FailableFunction<ShellControl, Boolean, Exception> elevationFunction);
|
||||
ShellControl elevated(ElevationFunction elevationFunction);
|
||||
|
||||
ShellControl withInitSnippet(ScriptSnippet snippet);
|
||||
|
||||
|
|
10
core/src/main/java/io/xpipe/core/store/Session.java
Normal file
10
core/src/main/java/io/xpipe/core/store/Session.java
Normal file
|
@ -0,0 +1,10 @@
|
|||
package io.xpipe.core.store;
|
||||
|
||||
public abstract class Session {
|
||||
|
||||
public abstract boolean isRunning();
|
||||
|
||||
public abstract void start() throws Exception;
|
||||
|
||||
public abstract void stop() throws Exception;
|
||||
}
|
|
@ -1,16 +1,6 @@
|
|||
package io.xpipe.core.store;
|
||||
|
||||
public interface SingletonSessionStore<T extends SingletonSessionStore.Session>
|
||||
extends ExpandedLifecycleStore, InternalCacheDataStore {
|
||||
|
||||
abstract class Session {
|
||||
|
||||
public abstract boolean isRunning();
|
||||
|
||||
public abstract void start() throws Exception;
|
||||
|
||||
public abstract void stop() throws Exception;
|
||||
}
|
||||
public interface SingletonSessionStore<T extends Session> extends ExpandedLifecycleStore, InternalCacheDataStore {
|
||||
|
||||
@Override
|
||||
default void finalizeValidate() throws Exception {
|
||||
|
|
|
@ -21,7 +21,8 @@ public interface StatefulDataStore<T extends DataStoreState> extends DataStore {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
default T getState() {
|
||||
return (T) DataStateProvider.get().getState(this, this::createDefaultState).deepCopy();
|
||||
return (T)
|
||||
DataStateProvider.get().getState(this, this::createDefaultState).deepCopy();
|
||||
}
|
||||
|
||||
default void setState(T val) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package io.xpipe.core.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import io.xpipe.core.dialog.BaseQueryElement;
|
||||
import io.xpipe.core.dialog.BusyElement;
|
||||
import io.xpipe.core.dialog.ChoiceElement;
|
||||
|
@ -14,6 +13,7 @@ import com.fasterxml.jackson.annotation.JsonIdentityInfo;
|
|||
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.jsontype.NamedType;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
|
2
dist/build.gradle
vendored
2
dist/build.gradle
vendored
|
@ -2,7 +2,7 @@
|
|||
plugins {
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
id "org.asciidoctor.jvm.convert" version "4.0.2"
|
||||
id 'org.jreleaser' version '1.8.0'
|
||||
id 'org.jreleaser' version '1.11.0'
|
||||
id("com.netflix.nebula.ospackage") version "11.8.1"
|
||||
id 'org.gradle.crypto.checksum' version '1.4.0'
|
||||
id 'signing'
|
||||
|
|
59
dist/changelogs/9.0.1.md
vendored
Normal file
59
dist/changelogs/9.0.1.md
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
## Coherent desktops
|
||||
|
||||
XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.
|
||||
|
||||
With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.
|
||||
|
||||
The general implementation and concept will be refined over the next updates.
|
||||
|
||||
## SSH connection improvements
|
||||
|
||||
- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs
|
||||
|
||||
- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice.
|
||||
|
||||
- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.
|
||||
|
||||
- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.
|
||||
|
||||
- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.
|
||||
|
||||
- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys
|
||||
|
||||
## SSH for unknown shells (Professional feature)
|
||||
|
||||
There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.
|
||||
|
||||
## SSH X11 Forwarding on Windows via WSL
|
||||
|
||||
You can now enable X11 forwarding for an SSH connection.
|
||||
|
||||
XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.
|
||||
|
||||
This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.
|
||||
|
||||
## Translations
|
||||
|
||||
XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.
|
||||
|
||||
## Terminal improvements
|
||||
|
||||
The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe.
|
||||
|
||||
The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.
|
||||
|
||||
## Password manager improvements
|
||||
|
||||
The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.
|
||||
|
||||
## Improved keyboard control
|
||||
|
||||
It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.
|
||||
|
||||
## Improved logo
|
||||
|
||||
The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.
|
||||
|
||||
## Other changes
|
||||
|
||||
There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.
|
4
dist/changelogs/9.0.1_incremental.md
vendored
Normal file
4
dist/changelogs/9.0.1_incremental.md
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
- Fix errors when trying to launch Tabby terminal
|
||||
- Fix terminal sessions closing instantly after completion
|
||||
- Fix choco package not being built
|
||||
- Fix a few broken translation strings
|
59
dist/changelogs/9.1.md
vendored
Normal file
59
dist/changelogs/9.1.md
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
## Coherent desktops
|
||||
|
||||
XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.
|
||||
|
||||
With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.
|
||||
|
||||
The general implementation and concept will be refined over the next updates.
|
||||
|
||||
## SSH connection improvements
|
||||
|
||||
- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs
|
||||
|
||||
- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice.
|
||||
|
||||
- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.
|
||||
|
||||
- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.
|
||||
|
||||
- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.
|
||||
|
||||
- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys
|
||||
|
||||
## SSH for unknown shells (Professional feature)
|
||||
|
||||
There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.
|
||||
|
||||
## SSH X11 Forwarding on Windows via WSL
|
||||
|
||||
You can now enable X11 forwarding for an SSH connection.
|
||||
|
||||
XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.
|
||||
|
||||
This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.
|
||||
|
||||
## Translations
|
||||
|
||||
XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.
|
||||
|
||||
## Terminal improvements
|
||||
|
||||
The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe.
|
||||
|
||||
The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.
|
||||
|
||||
## Password manager improvements
|
||||
|
||||
The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.
|
||||
|
||||
## Improved keyboard control
|
||||
|
||||
It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.
|
||||
|
||||
## Improved logo
|
||||
|
||||
The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.
|
||||
|
||||
## Other changes
|
||||
|
||||
There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.
|
14
dist/changelogs/9.1_incremental.md
vendored
Normal file
14
dist/changelogs/9.1_incremental.md
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
- Add ability to drop files into an active VNC session to transfer them to the system
|
||||
- Rework VNC connections to distinguish between VNC server host and actual target system host for cases like VMs where the server host and controlled target system might be different
|
||||
- Add new setting to automatically lock vault when the local system goes into hibernation/sleep mode and a custom vault passphrase was set
|
||||
- Fix sudo elevation password not being filled automatically when launching some remote connection in a terminal
|
||||
- Fix git sometimes complaining about an unknown author identity when cloning on new systems
|
||||
- Fix search for connections dialog sometimes throwing errors
|
||||
- Fix NullPointer when launching an SFTP Client/Termius/VSCode for an SSH connection without a password set
|
||||
- Fix macOS terminal and editor app recognition to be more accurate
|
||||
- Fix file browser overview buttons both opening the same directory
|
||||
- Fix exception on Linux when desktop directory did not exist
|
||||
- Fix out of bounds error for certain VNC key input
|
||||
- Fix NullPointers when launching a desktop environment for an X11 SSH connection
|
||||
- Fix some NullPointers in the file browser
|
||||
- Fix some styling issues
|
59
dist/changelogs/9.2.md
vendored
Normal file
59
dist/changelogs/9.2.md
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
## Coherent desktops
|
||||
|
||||
XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.
|
||||
|
||||
With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.
|
||||
|
||||
The general implementation and concept will be refined over the next updates.
|
||||
|
||||
## SSH connection improvements
|
||||
|
||||
- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs
|
||||
|
||||
- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice.
|
||||
|
||||
- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.
|
||||
|
||||
- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.
|
||||
|
||||
- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.
|
||||
|
||||
- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys
|
||||
|
||||
## SSH for unknown shells (Professional feature)
|
||||
|
||||
There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.
|
||||
|
||||
## SSH X11 Forwarding on Windows via WSL
|
||||
|
||||
You can now enable X11 forwarding for an SSH connection.
|
||||
|
||||
XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.
|
||||
|
||||
This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.
|
||||
|
||||
## Translations
|
||||
|
||||
XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.
|
||||
|
||||
## Terminal improvements
|
||||
|
||||
The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe.
|
||||
|
||||
The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.
|
||||
|
||||
## Password manager improvements
|
||||
|
||||
The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.
|
||||
|
||||
## Improved keyboard control
|
||||
|
||||
It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.
|
||||
|
||||
## Improved logo
|
||||
|
||||
The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.
|
||||
|
||||
## Other changes
|
||||
|
||||
There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.
|
1
dist/changelogs/9.2_incremental.md
vendored
Normal file
1
dist/changelogs/9.2_incremental.md
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
- Fix offline licenses not properly applying
|
3
dist/debug/linux/debug_attach_arguments.txt
vendored
3
dist/debug/linux/debug_attach_arguments.txt
vendored
|
@ -1,2 +1,3 @@
|
|||
-javaagent:$HOME/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost
|
||||
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
|
||||
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
|
||||
-XX:+EnableDynamicAgentLoading
|
3
dist/debug/mac/debug_attach_arguments.txt
vendored
3
dist/debug/mac/debug_attach_arguments.txt
vendored
|
@ -1,2 +1,3 @@
|
|||
-javaagent:$HOME/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost
|
||||
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
|
||||
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
|
||||
-XX:+EnableDynamicAgentLoading
|
|
@ -1,2 +1,3 @@
|
|||
-javaagent:%userprofile%\\.attachme\\attachme-agent-1.2.4.jar=port:7857,host:localhost
|
||||
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
|
||||
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
|
||||
-XX:+EnableDynamicAgentLoading
|
|
@ -8,8 +8,10 @@ import io.xpipe.app.storage.DataStoreEntry;
|
|||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.process.ShellStoreState;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class BrowseStoreAction implements ActionProvider {
|
||||
|
@ -22,8 +24,8 @@ public class BrowseStoreAction implements ActionProvider {
|
|||
public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {
|
||||
var state = o.get().getStorePersistentState();
|
||||
if (state instanceof ShellStoreState shellStoreState) {
|
||||
return shellStoreState.getShellDialect() == null ||
|
||||
shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
|
||||
return shellStoreState.getShellDialect() == null
|
||||
|| shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -43,8 +43,9 @@ public class LaunchAction implements ActionProvider {
|
|||
|
||||
@Override
|
||||
public boolean isApplicable(DataStoreEntryRef<DataStore> o) {
|
||||
return o.get().getValidity().isUsable() && (o.getStore() instanceof LaunchableStore || o.get().getProvider().launchAction(o.get()) !=
|
||||
null);
|
||||
return o.get().getValidity().isUsable()
|
||||
&& (o.getStore() instanceof LaunchableStore
|
||||
|| o.get().getProvider().launchAction(o.get()) != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -75,8 +76,9 @@ public class LaunchAction implements ActionProvider {
|
|||
|
||||
@Override
|
||||
public boolean isApplicable(DataStoreEntryRef<DataStore> o) {
|
||||
return o.get().getValidity().isUsable() && (o.getStore() instanceof LaunchableStore || o.get().getProvider().launchAction(o.get()) !=
|
||||
null);
|
||||
return o.get().getValidity().isUsable()
|
||||
&& (o.getStore() instanceof LaunchableStore
|
||||
|| o.get().getProvider().launchAction(o.get()) != null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.xpipe.app.ext.ActionProvider;
|
|||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.process.CommandControl;
|
||||
import io.xpipe.core.process.ElevationFunction;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import io.xpipe.core.store.LocalStore;
|
||||
|
@ -125,7 +126,7 @@ public class SampleAction implements ActionProvider {
|
|||
// by using the information from the connection store.
|
||||
// You can also set a custom working directory.
|
||||
try (CommandControl cc = sc.command("kill <pid>")
|
||||
.elevated("kill")
|
||||
.elevated(ElevationFunction.elevated("kill"))
|
||||
.withWorkingDirectory("/")
|
||||
.start()) {
|
||||
// Discard any output but throw an exception with the stderr contents if the exit code is not 0
|
||||
|
|
|
@ -7,7 +7,9 @@ import io.xpipe.app.storage.DataStoreEntryRef;
|
|||
import io.xpipe.app.util.ScanAlert;
|
||||
import io.xpipe.core.process.ShellStoreState;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
public class ScanAction implements ActionProvider {
|
||||
|
@ -39,8 +41,8 @@ public class ScanAction implements ActionProvider {
|
|||
|
||||
var state = o.get().getStorePersistentState();
|
||||
if (state instanceof ShellStoreState shellStoreState) {
|
||||
return shellStoreState.getShellDialect() == null ||
|
||||
shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
|
||||
return shellStoreState.getShellDialect() == null
|
||||
|| shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,10 @@ import io.xpipe.app.browser.action.LeafAction;
|
|||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.process.ShellDialect;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
|
@ -19,34 +18,12 @@ public class BrowseInNativeManagerAction implements LeafAction {
|
|||
@Override
|
||||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
|
||||
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
|
||||
ShellDialect d = sc.getShellDialect();
|
||||
for (BrowserEntry entry : entries) {
|
||||
var e = entry.getRawFileEntry().getPath();
|
||||
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
|
||||
try (var local = LocalShell.getShell().start()) {
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Windows windows -> {
|
||||
// Explorer does not support single quotes, so use normal quotes
|
||||
if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY) {
|
||||
local.executeSimpleCommand("explorer " + d.quoteArgument(localFile));
|
||||
} else {
|
||||
local.executeSimpleCommand("explorer /select," + d.quoteArgument(localFile));
|
||||
}
|
||||
}
|
||||
case OsType.Linux linux -> {
|
||||
var action = entry.getRawFileEntry().getKind() == FileKind.DIRECTORY ?
|
||||
"org.freedesktop.FileManager1.ShowFolders" :
|
||||
"org.freedesktop.FileManager1.ShowItems";
|
||||
var dbus = String.format("""
|
||||
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:"file://%s" string:""
|
||||
""", action, localFile);
|
||||
local.executeSimpleCommand(dbus);
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
local.executeSimpleCommand(
|
||||
"open " + (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY ? "" : "-R ") + d.fileArgument(localFile));
|
||||
}
|
||||
}
|
||||
DesktopHelper.browsePathRemote(
|
||||
local, localFile, entry.getRawFileEntry().getKind());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ public class CopyAction implements LeafAction {
|
|||
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
BrowserClipboard.startCopy(
|
||||
model.getCurrentDirectory(),
|
||||
entries.stream().map(entry -> entry.getRawFileEntry()).toList());
|
||||
entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -37,7 +37,8 @@ public class EditFileAction implements LeafAction {
|
|||
@Override
|
||||
public ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
var e = AppPrefs.get().externalEditor().getValue();
|
||||
return AppI18n.observable("editWithEditor", e.toTranslatedString().getValue());
|
||||
return AppI18n.observable(
|
||||
"editWithEditor", e != null ? e.toTranslatedString().getValue() : "?");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -9,6 +9,7 @@ import io.xpipe.app.prefs.AppPrefs;
|
|||
import io.xpipe.app.util.TerminalLauncher;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
|
|
@ -34,7 +34,7 @@ public class PasteAction implements LeafAction {
|
|||
return;
|
||||
}
|
||||
|
||||
model.dropFilesIntoAsync(target, files, true);
|
||||
model.dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -74,7 +74,8 @@ public class DesktopCommandStoreProvider implements DataStoreProvider {
|
|||
entry,
|
||||
env,
|
||||
DesktopEnvironmentStore.class,
|
||||
desktopStoreDataStoreEntryRef -> desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(),
|
||||
desktopStoreDataStoreEntryRef ->
|
||||
desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(),
|
||||
StoreViewState.get().getAllConnectionsCategory()),
|
||||
env)
|
||||
.nonNull()
|
||||
|
|
|
@ -62,7 +62,7 @@ public class DesktopEnvironmentStore extends JacksonizedValue
|
|||
initCommands.add(command);
|
||||
}
|
||||
var joined = String.join(dialect.getNewLine().getNewLineString(), initCommands);
|
||||
return !joined.isBlank() ? joined : null;
|
||||
return joined;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -95,9 +95,9 @@ public class DesktopEnvironmentStore extends JacksonizedValue
|
|||
: getMergedInitCommands(null));
|
||||
var scriptFile = base.getStore().createScript(dialect, toExecute);
|
||||
var launchScriptFile = base.getStore()
|
||||
.createScript(dialect, dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString()));
|
||||
var launchConfig =
|
||||
new ExternalTerminalType.LaunchConfiguration(null, name, name, launchScriptFile, dialect);
|
||||
.createScript(
|
||||
dialect, dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString()));
|
||||
var launchConfig = new ExternalTerminalType.LaunchConfiguration(null, name, name, launchScriptFile, dialect);
|
||||
base.getStore().runDesktopScript(name, launchCommand.apply(launchConfig));
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue