mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Rework
This commit is contained in:
parent
d7055a435a
commit
781e1b3c84
62 changed files with 451 additions and 236 deletions
|
@ -6,6 +6,7 @@ import io.xpipe.app.issue.TrackEvent;
|
|||
import io.xpipe.app.util.MarkdownHelper;
|
||||
import io.xpipe.beacon.BeaconConfig;
|
||||
import io.xpipe.beacon.BeaconInterface;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
@ -17,6 +18,7 @@ import java.net.InetAddress;
|
|||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -110,6 +112,9 @@ public class AppBeaconServer {
|
|||
var file = XPipeInstallation.getLocalBeaconAuthFile();
|
||||
var id = UUID.randomUUID().toString();
|
||||
Files.writeString(file, id);
|
||||
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
|
||||
}
|
||||
localAuthSecret = id;
|
||||
}
|
||||
|
||||
|
|
|
@ -124,10 +124,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
|||
try {
|
||||
var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;
|
||||
if (!emptyResponseClass && response != null) {
|
||||
TrackEvent.trace("Sending response:\n" + object);
|
||||
var tree = JacksonMapper.getDefault().valueToTree(response);
|
||||
TrackEvent.trace("Sending raw response:\n" + tree.toPrettyString());
|
||||
var bytes = tree.toPrettyString().getBytes(StandardCharsets.UTF_8);
|
||||
TrackEvent.trace("Sending response:\n" + response);
|
||||
TrackEvent.trace("Sending raw response:\n" + JacksonMapper.getCensored().valueToTree(response).toPrettyString());
|
||||
var bytes = JacksonMapper.getDefault().valueToTree(response).toPrettyString().getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.browser;
|
|||
|
||||
import io.xpipe.app.comp.store.StoreCategoryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.FilterComp;
|
||||
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||
|
@ -30,8 +31,13 @@ public final class BrowserBookmarkHeaderComp extends SimpleComp {
|
|||
StoreViewState.get().getAllConnectionsCategory(),
|
||||
StoreViewState.get().getActiveCategory(),
|
||||
this.category)
|
||||
.styleClass(Styles.LEFT_PILL);
|
||||
var filter = new FilterComp(this.filter).styleClass(Styles.RIGHT_PILL).minWidth(0).hgrow();
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.apply(struc -> {
|
||||
AppFont.medium(struc.get());
|
||||
});
|
||||
var filter = new FilterComp(this.filter).styleClass(Styles.RIGHT_PILL).minWidth(0).hgrow().apply(struc -> {
|
||||
AppFont.medium(struc.get());
|
||||
});
|
||||
|
||||
var top = new HorizontalComp(List.of(category, filter))
|
||||
.apply(struc -> struc.get().setFillHeight(true))
|
||||
|
|
|
@ -32,9 +32,9 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
@Override
|
||||
protected Region createSimple() {
|
||||
var bar = new HorizontalComp(List.of(
|
||||
createProgressEstimateStatus(),
|
||||
createProgressNameStatus(),
|
||||
createProgressStatus(),
|
||||
createProgressEstimateStatus(),
|
||||
Comp.hspacer(),
|
||||
createClipboardStatus(),
|
||||
createSelectionStatus()
|
||||
|
@ -58,8 +58,8 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
return null;
|
||||
} else {
|
||||
var expected = p.expectedTimeRemaining();
|
||||
var show = (p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0) || expected.toMillis() > 5000;
|
||||
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "...";
|
||||
var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0 && (p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
|
||||
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
|
||||
return time;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -128,9 +128,6 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
var selected = items.stream()
|
||||
.map(item -> item.getBrowserEntry())
|
||||
.toList();
|
||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||
|
||||
var cc = new ClipboardContent();
|
||||
var files = items.stream()
|
||||
.filter(item -> item.downloadFinished().get())
|
||||
.map(item -> {
|
||||
|
@ -148,7 +145,13 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
})
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
if (files.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cc = new ClipboardContent();
|
||||
cc.putFiles(files);
|
||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||
db.setContent(cc);
|
||||
|
||||
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
|
||||
|
|
|
@ -9,6 +9,7 @@ import io.xpipe.app.browser.file.BrowserContextMenu;
|
|||
import io.xpipe.app.browser.file.BrowserFileListComp;
|
||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||
import io.xpipe.app.comp.base.MultiContentComp;
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||
|
@ -88,6 +89,7 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
topBar.setAlignment(Pos.CENTER);
|
||||
topBar.getStyleClass().add("top-bar");
|
||||
var navBar = new BrowserNavBar(model).createStructure();
|
||||
AppFont.medium(navBar.get());
|
||||
topBar.getChildren()
|
||||
.setAll(
|
||||
overview,
|
||||
|
|
|
@ -30,6 +30,7 @@ import javafx.scene.layout.Region;
|
|||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -48,17 +49,18 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
|||
}
|
||||
|
||||
public Region createSimple() {
|
||||
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
|
||||
Comp.of(() -> createTabPane()),
|
||||
Bindings.isNotEmpty(model.getSessionEntries()),
|
||||
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
|
||||
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
|
||||
map.put(Comp.hspacer().styleClass("top-spacer"),
|
||||
new SimpleBooleanProperty(true));
|
||||
map.put(Comp.of(() -> createTabPane()),
|
||||
Bindings.isNotEmpty(model.getSessionEntries()));
|
||||
map.put(new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
|
||||
Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return model.getSessionEntries().size() == 0;
|
||||
},
|
||||
model.getSessionEntries()),
|
||||
Comp.hspacer().styleClass("top-spacer"),
|
||||
new SimpleBooleanProperty(true)));
|
||||
model.getSessionEntries()));
|
||||
var multi = new MultiContentComp(map);
|
||||
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
|
||||
return multi.createRegion();
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
}
|
||||
b.apply(new TooltipAugment<>(e.name(), shortcut));
|
||||
b.apply(struc -> {
|
||||
AppFont.setSize(struc.get(), 2);
|
||||
AppFont.setSize(struc.get(), 1);
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
@ -118,7 +118,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
b.apply(struc -> {
|
||||
AppFont.setSize(struc.get(), 2);
|
||||
AppFont.setSize(struc.get(), 1);
|
||||
});
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.HPos;
|
||||
|
@ -17,8 +15,8 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
|
||||
private final boolean showIcon;
|
||||
|
||||
public DenseStoreEntryComp(StoreEntryWrapper entry, boolean showIcon, Comp<?> content) {
|
||||
super(entry, content);
|
||||
public DenseStoreEntryComp(StoreSection section, boolean showIcon, Comp<?> content) {
|
||||
super(section, content);
|
||||
this.showIcon = showIcon;
|
||||
}
|
||||
|
||||
|
@ -26,16 +24,15 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
var information = new Label();
|
||||
information.setGraphicTextGap(7);
|
||||
information.getStyleClass().add("information");
|
||||
AppFont.header(information);
|
||||
|
||||
var state = wrapper.getEntry().getProvider() != null
|
||||
? wrapper.getEntry().getProvider().stateDisplay(wrapper)
|
||||
var state = getWrapper().getEntry().getProvider() != null
|
||||
? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
|
||||
: Comp.empty();
|
||||
information.setGraphic(state.createRegion());
|
||||
|
||||
var info = wrapper.getEntry().getProvider() != null ? wrapper.getEntry().getProvider().informationString(wrapper) : new SimpleStringProperty();
|
||||
var summary = wrapper.getSummary();
|
||||
if (wrapper.getEntry().getProvider() != null) {
|
||||
var info = getWrapper().getEntry().getProvider() != null ? getWrapper().getEntry().getProvider().informationString(section) : new SimpleStringProperty();
|
||||
var summary = getWrapper().getSummary();
|
||||
if (getWrapper().getEntry().getProvider() != null) {
|
||||
information
|
||||
.textProperty()
|
||||
.bind(PlatformThread.sync(Bindings.createStringBinding(
|
||||
|
@ -43,7 +40,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
var val = summary.getValue();
|
||||
if (val != null
|
||||
&& grid.isHover()
|
||||
&& wrapper.getEntry().getProvider().alwaysShowSummary()) {
|
||||
&& getWrapper().getEntry().getProvider().alwaysShowSummary()) {
|
||||
return val;
|
||||
} else {
|
||||
return info.getValue();
|
||||
|
@ -73,11 +70,11 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
return grid.getWidth() / 2.5;
|
||||
},
|
||||
grid.widthProperty()));
|
||||
var notes = new StoreNotesComp(wrapper).createRegion();
|
||||
var notes = new StoreNotesComp(getWrapper()).createRegion();
|
||||
|
||||
if (showIcon) {
|
||||
var storeIcon = createIcon(30, 24);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(46));
|
||||
var storeIcon = createIcon(28, 24);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(38));
|
||||
grid.add(storeIcon, 0, 0);
|
||||
GridPane.setHalignment(storeIcon, HPos.CENTER);
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import io.xpipe.app.fxcomps.Comp;
|
|||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
public class StandardStoreEntryComp extends StoreEntryComp {
|
||||
|
||||
public StandardStoreEntryComp(StoreEntryWrapper entry, Comp<?> content) {
|
||||
super(entry, content);
|
||||
public StandardStoreEntryComp(StoreSection section, Comp<?> content) {
|
||||
super(section, content);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,20 +21,21 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
|
||||
protected Region createContent() {
|
||||
var name = createName().createRegion();
|
||||
var notes = new StoreNotesComp(wrapper).createRegion();
|
||||
var notes = new StoreNotesComp(getWrapper()).createRegion();
|
||||
|
||||
var grid = new GridPane();
|
||||
grid.setHgap(7);
|
||||
grid.setHgap(6);
|
||||
grid.setVgap(0);
|
||||
|
||||
var storeIcon = createIcon(50, 40);
|
||||
var storeIcon = createIcon(46, 40);
|
||||
grid.add(storeIcon, 0, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(66));
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(56));
|
||||
|
||||
var nameAndNotes = new HBox(name, notes);
|
||||
nameAndNotes.setSpacing(1);
|
||||
nameAndNotes.setAlignment(Pos.CENTER_LEFT);
|
||||
grid.add(nameAndNotes, 1, 0);
|
||||
GridPane.setValignment(nameAndNotes, VPos.CENTER);
|
||||
grid.add(createSummary(), 1, 1);
|
||||
var nameCC = new ColumnConstraints();
|
||||
nameCC.setMinWidth(100);
|
||||
|
|
|
@ -112,6 +112,11 @@ public class StoreCategoryWrapper {
|
|||
}
|
||||
|
||||
public void update() {
|
||||
// We are probably in shutdown then
|
||||
if (StoreViewState.get() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid reupdating name when changed from the name property!
|
||||
var catName = translatedName(category.getName());
|
||||
if (!catName.equals(name.getValue())) {
|
||||
|
|
|
@ -51,21 +51,25 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
App.getApp().getStage().widthProperty().divide(2.1).add(-100);
|
||||
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
|
||||
App.getApp().getStage().widthProperty().divide(2.1).add(-200);
|
||||
protected final StoreEntryWrapper wrapper;
|
||||
protected final StoreSection section;
|
||||
protected final Comp<?> content;
|
||||
|
||||
public StoreEntryComp(StoreEntryWrapper wrapper, Comp<?> content) {
|
||||
this.wrapper = wrapper;
|
||||
public StoreEntryComp(StoreSection section, Comp<?> content) {
|
||||
this.section = section;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public StoreEntryWrapper getWrapper() {
|
||||
return section.getWrapper();
|
||||
}
|
||||
|
||||
public static StoreEntryComp create(StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) {
|
||||
public static StoreEntryComp create(StoreSection section, Comp<?> content, boolean preferLarge) {
|
||||
var forceCondensed = AppPrefs.get() != null
|
||||
&& AppPrefs.get().condenseConnectionDisplay().get();
|
||||
if (!preferLarge || forceCondensed) {
|
||||
return new DenseStoreEntryComp(entry, true, content);
|
||||
return new DenseStoreEntryComp(section, true, content);
|
||||
} else {
|
||||
return new StandardStoreEntryComp(entry, content);
|
||||
return new StandardStoreEntryComp(section, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,8 +81,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var forceCondensed = AppPrefs.get() != null
|
||||
&& AppPrefs.get().condenseConnectionDisplay().get();
|
||||
return forceCondensed
|
||||
? new DenseStoreEntryComp(e.getWrapper(), true, null)
|
||||
: new StandardStoreEntryComp(e.getWrapper(), null);
|
||||
? new DenseStoreEntryComp(e, true, null)
|
||||
: new StandardStoreEntryComp(e, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,11 +99,11 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
button.setPadding(Insets.EMPTY);
|
||||
button.setMaxWidth(5000);
|
||||
button.setFocusTraversable(true);
|
||||
button.accessibleTextProperty().bind(wrapper.nameProperty());
|
||||
button.accessibleTextProperty().bind(getWrapper().nameProperty());
|
||||
button.setOnAction(event -> {
|
||||
event.consume();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.executeDefaultAction();
|
||||
getWrapper().executeDefaultAction();
|
||||
});
|
||||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
|
@ -132,9 +136,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
var loading = LoadingOverlayComp.noProgress(
|
||||
Comp.of(() -> button),
|
||||
wrapper.getEntry().getValidity().isUsable()
|
||||
? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper))
|
||||
: wrapper.getBusy());
|
||||
getWrapper().getEntry().getValidity().isUsable()
|
||||
? getWrapper().getBusy().or(getWrapper().getEntry().getProvider().busy(getWrapper()))
|
||||
: getWrapper().getBusy());
|
||||
AppFont.normal(button);
|
||||
return loading.createRegion();
|
||||
}
|
||||
|
||||
|
@ -146,15 +151,14 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
information
|
||||
.textProperty()
|
||||
.bind(
|
||||
wrapper.getEntry().getProvider() != null
|
||||
getWrapper().getEntry().getProvider() != null
|
||||
? PlatformThread.sync(
|
||||
wrapper.getEntry().getProvider().informationString(wrapper))
|
||||
getWrapper().getEntry().getProvider().informationString(section))
|
||||
: new SimpleStringProperty());
|
||||
information.getStyleClass().add("information");
|
||||
AppFont.header(information);
|
||||
|
||||
var state = wrapper.getEntry().getProvider() != null
|
||||
? wrapper.getEntry().getProvider().stateDisplay(wrapper)
|
||||
var state = getWrapper().getEntry().getProvider() != null
|
||||
? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
|
||||
: Comp.empty();
|
||||
information.setGraphic(state.createRegion());
|
||||
|
||||
|
@ -163,14 +167,14 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
protected Label createSummary() {
|
||||
var summary = new Label();
|
||||
summary.textProperty().bind(wrapper.getSummary());
|
||||
summary.textProperty().bind(getWrapper().getSummary());
|
||||
summary.getStyleClass().add("summary");
|
||||
AppFont.small(summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
protected void applyState(Node node) {
|
||||
PlatformThread.sync(wrapper.getValidity()).subscribe(val -> {
|
||||
PlatformThread.sync(getWrapper().getValidity()).subscribe(val -> {
|
||||
switch (val) {
|
||||
case LOAD_FAILED -> {
|
||||
node.pseudoClassStateChanged(FAILED, true);
|
||||
|
@ -189,24 +193,22 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
}
|
||||
|
||||
protected Comp<?> createName() {
|
||||
LabelComp name = new LabelComp(wrapper.nameProperty());
|
||||
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS))
|
||||
.apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0)));
|
||||
name.apply(s -> AppFont.header(s.get()));
|
||||
LabelComp name = new LabelComp(getWrapper().nameProperty());
|
||||
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS));
|
||||
name.styleClass("name");
|
||||
return name;
|
||||
}
|
||||
|
||||
protected Node createIcon(int w, int h) {
|
||||
var img = wrapper.disabledProperty().get()
|
||||
var img = getWrapper().disabledProperty().get()
|
||||
? "disabled_icon.png"
|
||||
: wrapper.getEntry()
|
||||
: getWrapper().getEntry()
|
||||
.getProvider()
|
||||
.getDisplayIconFileName(wrapper.getEntry().getStore());
|
||||
.getDisplayIconFileName(getWrapper().getEntry().getStore());
|
||||
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
|
||||
var storeIcon = imageComp.createRegion();
|
||||
if (wrapper.getValidity().getValue().isUsable()) {
|
||||
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon);
|
||||
if (getWrapper().getValidity().getValue().isUsable()) {
|
||||
new TooltipAugment<>(getWrapper().getEntry().getProvider().displayName(), null).augment(storeIcon);
|
||||
}
|
||||
|
||||
var stack = new StackPane(storeIcon);
|
||||
|
@ -220,7 +222,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
}
|
||||
|
||||
protected Region createButtonBar() {
|
||||
var list = new DerivedObservableList<>(wrapper.getActionProviders(), false);
|
||||
var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false);
|
||||
var buttons = list.mapped(actionProvider -> {
|
||||
var button = buildButton(actionProvider);
|
||||
return button != null ? button.createRegion() : null;
|
||||
|
@ -239,8 +241,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
buttons.subscribe(update);
|
||||
update.run();
|
||||
ig.setAlignment(Pos.CENTER_RIGHT);
|
||||
ig.setPadding(new Insets(5));
|
||||
ig.getStyleClass().add("button-bar");
|
||||
AppFont.medium(ig);
|
||||
return ig;
|
||||
}
|
||||
|
||||
|
@ -249,17 +251,17 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var branch = p.getBranchDataStoreCallSite();
|
||||
var cs = leaf != null ? leaf : branch;
|
||||
|
||||
if (cs == null || !cs.isMajor(wrapper.getEntry().ref())) {
|
||||
if (cs == null || !cs.isMajor(getWrapper().getEntry().ref())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var button = new IconButtonComp(
|
||||
cs.getIcon(wrapper.getEntry().ref()),
|
||||
cs.getIcon(getWrapper().getEntry().ref()),
|
||||
leaf != null
|
||||
? () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.runAction(
|
||||
leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
|
||||
getWrapper().runAction(
|
||||
leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
|
||||
});
|
||||
}
|
||||
: null);
|
||||
|
@ -276,8 +278,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
return cm;
|
||||
}));
|
||||
}
|
||||
button.accessibleText(cs.getName(wrapper.getEntry().ref()).getValue());
|
||||
button.apply(new TooltipAugment<>(cs.getName(wrapper.getEntry().ref()), null));
|
||||
button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue());
|
||||
button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null));
|
||||
return button;
|
||||
}
|
||||
|
||||
|
@ -298,7 +300,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
AppFont.normal(contextMenu.getStyleableNode());
|
||||
|
||||
var hasSep = false;
|
||||
for (var p : wrapper.getActionProviders()) {
|
||||
for (var p : getWrapper().getActionProviders()) {
|
||||
var item = buildMenuItemForAction(p);
|
||||
if (item == null) {
|
||||
continue;
|
||||
|
@ -321,36 +323,36 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
|
||||
notes.setOnAction(event -> {
|
||||
wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
|
||||
getWrapper().getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
|
||||
event.consume();
|
||||
});
|
||||
notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null));
|
||||
notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s.getCommited() == null));
|
||||
contextMenu.getItems().add(notes);
|
||||
|
||||
if (AppPrefs.get().developerMode().getValue()) {
|
||||
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
|
||||
browse.setOnAction(
|
||||
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
|
||||
event -> DesktopHelper.browsePathLocal(getWrapper().getEntry().getDirectory()));
|
||||
contextMenu.getItems().add(browse);
|
||||
|
||||
var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy"));
|
||||
copyId.setOnAction(event ->
|
||||
ClipboardHelper.copyText(wrapper.getEntry().getUuid().toString()));
|
||||
ClipboardHelper.copyText(getWrapper().getEntry().getUuid().toString()));
|
||||
contextMenu.getItems().add(copyId);
|
||||
}
|
||||
|
||||
if (DataStorage.get().isRootEntry(wrapper.getEntry())) {
|
||||
if (DataStorage.get().isRootEntry(getWrapper().getEntry())) {
|
||||
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
|
||||
var none = new MenuItem("None");
|
||||
none.setOnAction(event -> {
|
||||
wrapper.getEntry().setColor(null);
|
||||
getWrapper().getEntry().setColor(null);
|
||||
event.consume();
|
||||
});
|
||||
color.getItems().add(none);
|
||||
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
|
||||
MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId()));
|
||||
m.setOnAction(event -> {
|
||||
wrapper.getEntry().setColor(dataStoreColor);
|
||||
getWrapper().getEntry().setColor(dataStoreColor);
|
||||
event.consume();
|
||||
});
|
||||
color.getItems().add(m);
|
||||
|
@ -358,10 +360,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
contextMenu.getItems().add(color);
|
||||
}
|
||||
|
||||
if (wrapper.getEntry().getProvider() != null) {
|
||||
if (getWrapper().getEntry().getProvider() != null) {
|
||||
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
|
||||
StoreViewState.get()
|
||||
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
|
||||
.getSortedCategories(getWrapper().getCategory().getValue().getRoot())
|
||||
.getList()
|
||||
.forEach(storeCategoryWrapper -> {
|
||||
MenuItem m = new MenuItem();
|
||||
|
@ -369,12 +371,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
.setValue(" ".repeat(storeCategoryWrapper.getDepth())
|
||||
+ storeCategoryWrapper.getName().getValue());
|
||||
m.setOnAction(event -> {
|
||||
wrapper.moveTo(storeCategoryWrapper.getCategory());
|
||||
getWrapper().moveTo(storeCategoryWrapper.getCategory());
|
||||
event.consume();
|
||||
});
|
||||
if (storeCategoryWrapper.getParent() == null
|
||||
|| storeCategoryWrapper.equals(
|
||||
wrapper.getCategory().getValue())) {
|
||||
getWrapper().getCategory().getValue())) {
|
||||
m.setDisable(true);
|
||||
}
|
||||
|
||||
|
@ -386,10 +388,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks"));
|
||||
var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal"));
|
||||
noOrder.setOnAction(event -> {
|
||||
wrapper.setOrder(null);
|
||||
getWrapper().setOrder(null);
|
||||
event.consume();
|
||||
});
|
||||
if (wrapper.getEntry().getExplicitOrder() == null) {
|
||||
if (getWrapper().getEntry().getExplicitOrder() == null) {
|
||||
noOrder.setDisable(true);
|
||||
}
|
||||
order.getItems().add(noOrder);
|
||||
|
@ -397,20 +399,20 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
var top = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending"));
|
||||
top.setOnAction(event -> {
|
||||
wrapper.setOrder(DataStoreEntry.Order.TOP);
|
||||
getWrapper().setOrder(DataStoreEntry.Order.TOP);
|
||||
event.consume();
|
||||
});
|
||||
if (DataStoreEntry.Order.TOP.equals(wrapper.getEntry().getExplicitOrder())) {
|
||||
if (DataStoreEntry.Order.TOP.equals(getWrapper().getEntry().getExplicitOrder())) {
|
||||
top.setDisable(true);
|
||||
}
|
||||
order.getItems().add(top);
|
||||
|
||||
var bottom = new MenuItem(AppI18n.get("stickToBottom"), new FontIcon("mdi2o-order-bool-ascending"));
|
||||
bottom.setOnAction(event -> {
|
||||
wrapper.setOrder(DataStoreEntry.Order.BOTTOM);
|
||||
getWrapper().setOrder(DataStoreEntry.Order.BOTTOM);
|
||||
event.consume();
|
||||
});
|
||||
if (DataStoreEntry.Order.BOTTOM.equals(wrapper.getEntry().getExplicitOrder())) {
|
||||
if (DataStoreEntry.Order.BOTTOM.equals(getWrapper().getEntry().getExplicitOrder())) {
|
||||
bottom.setDisable(true);
|
||||
}
|
||||
order.getItems().add(bottom);
|
||||
|
@ -423,14 +425,14 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
del.disableProperty()
|
||||
.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return !wrapper.getDeletable().get()
|
||||
return !getWrapper().getDeletable().get()
|
||||
&& !AppPrefs.get()
|
||||
.developerDisableGuiRestrictions()
|
||||
.get();
|
||||
},
|
||||
wrapper.getDeletable(),
|
||||
getWrapper().getDeletable(),
|
||||
AppPrefs.get().developerDisableGuiRestrictions()));
|
||||
del.setOnAction(event -> wrapper.delete());
|
||||
del.setOnAction(event -> getWrapper().delete());
|
||||
contextMenu.getItems().add(del);
|
||||
|
||||
return contextMenu;
|
||||
|
@ -441,12 +443,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var branch = p.getBranchDataStoreCallSite();
|
||||
var cs = leaf != null ? leaf : branch;
|
||||
|
||||
if (cs == null || cs.isMajor(wrapper.getEntry().ref())) {
|
||||
if (cs == null || cs.isMajor(getWrapper().getEntry().ref())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = cs.getName(wrapper.getEntry().ref());
|
||||
var icon = cs.getIcon(wrapper.getEntry().ref());
|
||||
var name = cs.getName(getWrapper().getEntry().ref());
|
||||
var icon = cs.getIcon(getWrapper().getEntry().ref());
|
||||
var item = (leaf != null && leaf.canLinkTo()) || branch != null
|
||||
? new Menu(null, new FontIcon(icon))
|
||||
: new MenuItem(null, new FontIcon(icon));
|
||||
|
@ -472,21 +474,21 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
run.textProperty().bind(AppI18n.observable("base.execute"));
|
||||
run.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
|
||||
getWrapper().runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
|
||||
});
|
||||
});
|
||||
menu.getItems().add(run);
|
||||
|
||||
var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than"));
|
||||
var url = "xpipe://action/" + p.getId() + "/" + wrapper.getEntry().getUuid();
|
||||
var url = "xpipe://action/" + p.getId() + "/" + getWrapper().getEntry().getUuid();
|
||||
sc.textProperty().bind(AppI18n.observable("base.createShortcut"));
|
||||
sc.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
DesktopShortcuts.create(
|
||||
url,
|
||||
wrapper.nameProperty().getValue() + " ("
|
||||
getWrapper().nameProperty().getValue() + " ("
|
||||
+ p.getLeafDataStoreCallSite()
|
||||
.getName(wrapper.getEntry().ref())
|
||||
.getName(getWrapper().getEntry().ref())
|
||||
.getValue() + ")");
|
||||
});
|
||||
});
|
||||
|
@ -516,7 +518,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
event.consume();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
|
||||
getWrapper().runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ import io.xpipe.app.core.mode.OperationMode;
|
|||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URL;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
public class AppTrayIcon {
|
||||
|
||||
|
@ -90,7 +90,8 @@ public class AppTrayIcon {
|
|||
tray.add(this.trayIcon);
|
||||
fixBackground();
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable("Unable to add TrayIcon", e).handle();
|
||||
// This can sometimes fail on Linux
|
||||
ErrorEvent.fromThrowable("Unable to add TrayIcon", e).expected().handle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.core.window;
|
|||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
|
@ -13,8 +12,6 @@ import javafx.stage.Stage;
|
|||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
public class ModifiedStage extends Stage {
|
||||
|
@ -23,12 +20,8 @@ public class ModifiedStage extends Stage {
|
|||
return SystemUtils.IS_OS_WINDOWS_11 || SystemUtils.IS_OS_MAC;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void init() {
|
||||
var windowsField = Window.class.getDeclaredField("windows");
|
||||
windowsField.setAccessible(true);
|
||||
ObservableList<Window> list = (ObservableList<Window>) windowsField.get(null);
|
||||
ObservableList<Window> list = Window.getWindows();
|
||||
list.addListener((ListChangeListener<Window>) c -> {
|
||||
if (c.next() && c.wasAdded()) {
|
||||
var added = c.getAddedSubList().getFirst();
|
||||
|
|
|
@ -31,6 +31,8 @@ public interface DataStoreProvider {
|
|||
return true;
|
||||
}
|
||||
|
||||
default void onParentRefresh(DataStoreEntry entry) {}
|
||||
|
||||
default void onChildrenRefresh(DataStoreEntry entry) {}
|
||||
|
||||
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
|
||||
|
@ -85,7 +87,7 @@ public interface DataStoreProvider {
|
|||
}
|
||||
|
||||
default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) {
|
||||
return StoreEntryComp.create(s.getWrapper(), null, preferLarge);
|
||||
return StoreEntryComp.create(s, null, preferLarge);
|
||||
}
|
||||
|
||||
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
|
||||
|
@ -191,7 +193,7 @@ public interface DataStoreProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
default ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
default ObservableValue<String> informationString(StoreSection section) {
|
||||
return new SimpleStringProperty(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
|
|||
@Override
|
||||
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) {
|
||||
return StoreEntryComp.create(sec.getWrapper(), null, preferLarge);
|
||||
return StoreEntryComp.create(sec, null, preferLarge);
|
||||
}
|
||||
|
||||
var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle(
|
||||
|
@ -35,6 +35,6 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
|
|||
}));
|
||||
}
|
||||
|
||||
return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge);
|
||||
return StoreEntryComp.create(sec, enabled, preferLarge);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ public interface EnabledStoreProvider extends DataStoreProvider {
|
|||
@Override
|
||||
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) {
|
||||
return StoreEntryComp.create(sec.getWrapper(), null, preferLarge);
|
||||
return StoreEntryComp.create(sec, null, preferLarge);
|
||||
}
|
||||
|
||||
var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle(
|
||||
|
@ -20,6 +20,6 @@ public interface EnabledStoreProvider extends DataStoreProvider {
|
|||
var state = s.getState().toBuilder().enabled(aBoolean).build();
|
||||
s.setState(state);
|
||||
});
|
||||
return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge);
|
||||
return StoreEntryComp.create(sec, enabled, preferLarge);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider {
|
|||
@Override
|
||||
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
var t = createToggleComp(sec);
|
||||
return StoreEntryComp.create(sec.getWrapper(), t, preferLarge);
|
||||
return StoreEntryComp.create(sec, t, preferLarge);
|
||||
}
|
||||
|
||||
default StoreToggleComp createToggleComp(StoreSection sec) {
|
||||
|
|
|
@ -39,7 +39,7 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
|
|||
filter.getStyleClass().add("filter-comp");
|
||||
filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
|
||||
filter.rightProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return filter.isFocused() ? clear : fi;
|
||||
return filter.isFocused() || (filter.getText() != null && !filter.getText().isEmpty()) ? clear : fi;
|
||||
}, filter.focusedProperty()));
|
||||
filter.setAccessibleText("Filter");
|
||||
|
||||
|
|
|
@ -132,6 +132,14 @@ public class LauncherCommand implements Callable<Integer> {
|
|||
+ " is already locked. Is another instance running?");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
|
||||
// If an instance is running as another user, we cannot connect to it as the xpipe_auth file is inaccessible
|
||||
// Therefore the beacon client is not present.
|
||||
// We still should check whether it is somehow occupied, otherwise beacon server startup will fail
|
||||
if (BeaconClient.isOccupied(port)) {
|
||||
TrackEvent.info("Another instance is already running on this port as another user. Quitting ...");
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
}
|
||||
|
||||
private XPipeDaemonMode getEffectiveMode() {
|
||||
|
|
|
@ -78,6 +78,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
|
||||
LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code");
|
||||
|
||||
LinuxPathType ZED_LINUX = new LinuxPathType("app.zed", "zed");
|
||||
|
||||
ExternalEditorType ZED_MACOS = new MacOsEditor("app.zed", "Zed");
|
||||
|
||||
LinuxPathType VSCODIUM_LINUX = new LinuxPathType("app.vscodium", "codium");
|
||||
|
||||
LinuxPathType GNOME = new LinuxPathType("app.gnomeTextEditor", "gnome-text-editor");
|
||||
|
@ -124,8 +128,8 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
|||
List<ExternalEditorType> WINDOWS_EDITORS =
|
||||
List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
|
||||
List<LinuxPathType> LINUX_EDITORS =
|
||||
List.of(VSCODIUM_LINUX, VSCODE_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
|
||||
List<ExternalEditorType> MACOS_EDITORS = List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, TEXT_EDIT);
|
||||
List.of(VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
|
||||
List<ExternalEditorType> MACOS_EDITORS = List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
|
||||
List<ExternalEditorType> CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION);
|
||||
|
||||
@SuppressWarnings("TrivialFunctionalExpressionUsage")
|
||||
|
|
|
@ -22,7 +22,7 @@ public class SyncCategory extends AppPrefsCategory {
|
|||
.sub(new OptionsBuilder()
|
||||
.name("enableGitStorage")
|
||||
.description(
|
||||
AppProperties.get().isStaging() && !prefs.developerMode().getValue() ? "enableGitStoragePtbDisabled" : "enableGitStorage")
|
||||
AppProperties.get().isStaging() && !prefs.developerMode().getValue() ? "enableGitStoragePtbDisabled" : "enableGitStorageDescription")
|
||||
.addToggle(prefs.enableGitStorage)
|
||||
.disable(AppProperties.get().isStaging() && !prefs.developerMode().getValue())
|
||||
.nameAndDescription("storageGitRemote")
|
||||
|
|
|
@ -36,8 +36,6 @@ public class VaultCategory extends AppPrefsCategory {
|
|||
}
|
||||
builder.addTitle("vaultSecurity")
|
||||
.sub(new OptionsBuilder()
|
||||
.nameAndDescription("encryptAllVaultData")
|
||||
.addToggle(prefs.encryptAllVaultData)
|
||||
.nameAndDescription("workspaceLock")
|
||||
.addComp(
|
||||
new ButtonComp(
|
||||
|
@ -57,7 +55,9 @@ public class VaultCategory extends AppPrefsCategory {
|
|||
.addToggle(prefs.lockVaultOnHibernation)
|
||||
.hide(prefs.getLockCrypt()
|
||||
.isNull()
|
||||
.or(prefs.getLockCrypt().isEmpty())));
|
||||
.or(prefs.getLockCrypt().isEmpty()))
|
||||
.nameAndDescription("encryptAllVaultData")
|
||||
.addToggle(prefs.encryptAllVaultData));
|
||||
return builder.buildComp();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,7 @@ import io.xpipe.core.store.FixedChildStore;
|
|||
import io.xpipe.core.store.LocalStore;
|
||||
import io.xpipe.core.store.StorePath;
|
||||
import io.xpipe.core.util.UuidHelper;
|
||||
|
||||
import javafx.util.Pair;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
|
@ -345,14 +343,14 @@ public abstract class DataStorage {
|
|||
}
|
||||
|
||||
public boolean refreshChildren(DataStoreEntry e, boolean throwOnFail) throws Exception {
|
||||
if (!(e.getStore() instanceof FixedHierarchyStore)) {
|
||||
if (!(e.getStore() instanceof FixedHierarchyStore h)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
e.incrementBusyCounter();
|
||||
List<? extends DataStoreEntryRef<? extends FixedChildStore>> newChildren;
|
||||
try {
|
||||
newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e).stream().filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null).toList();
|
||||
newChildren = h.listChildren(e).stream().filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null).toList();
|
||||
} catch (Exception ex) {
|
||||
if (throwOnFail) {
|
||||
throw ex;
|
||||
|
@ -368,6 +366,10 @@ public abstract class DataStorage {
|
|||
var toRemove = oldChildren.stream()
|
||||
.filter(oc -> oc.getStore() instanceof FixedChildStore)
|
||||
.filter(oc -> {
|
||||
if (!oc.getValidity().isUsable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
|
||||
if (oid.isEmpty()) {
|
||||
return false;
|
||||
|
@ -394,6 +396,7 @@ public abstract class DataStorage {
|
|||
|
||||
return oldChildren.stream()
|
||||
.filter(oc -> oc.getStore() instanceof FixedChildStore)
|
||||
.filter(oc -> oc.getValidity().isUsable())
|
||||
.filter(oc -> ((FixedChildStore) oc.getStore())
|
||||
.getFixedId()
|
||||
.isPresent())
|
||||
|
@ -407,6 +410,7 @@ public abstract class DataStorage {
|
|||
.toList();
|
||||
var toUpdate = oldChildren.stream()
|
||||
.filter(oc -> oc.getStore() instanceof FixedChildStore)
|
||||
.filter(oc -> oc.getValidity().isUsable())
|
||||
.map(oc -> {
|
||||
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
|
||||
if (oid.isEmpty()) {
|
||||
|
@ -460,10 +464,11 @@ public abstract class DataStorage {
|
|||
});
|
||||
refreshEntries();
|
||||
saveAsync();
|
||||
e.getProvider().onChildrenRefresh(e);
|
||||
toAdd.forEach(dataStoreEntryRef ->
|
||||
dataStoreEntryRef.get().getProvider().onChildrenRefresh(dataStoreEntryRef.getEntry()));
|
||||
dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.getEntry()));
|
||||
toUpdate.forEach(dataStoreEntryRef ->
|
||||
dataStoreEntryRef.getKey().getProvider().onChildrenRefresh(dataStoreEntryRef.getKey()));
|
||||
dataStoreEntryRef.getKey().getProvider().onParentRefresh(dataStoreEntryRef.getKey()));
|
||||
return !newChildren.isEmpty();
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ public class AppDownloads {
|
|||
var changelog = json.required("changelog").asText();
|
||||
return Optional.of(changelog);
|
||||
} catch (Throwable t) {
|
||||
ErrorEvent.fromThrowable(t).omit().handle();
|
||||
ErrorEvent.fromThrowable(t).omit().expected().handle();
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -94,7 +94,7 @@ public class FileBridge {
|
|||
event("File " + TEMP.relativize(e.file) + " is probably still writing ...");
|
||||
ThreadHelper.sleep(AppPrefs.get().editorReloadTimeout().getValue());
|
||||
|
||||
// If still no read lock after 500ms, just don't parse it
|
||||
// If still no read lock after some time, just don't parse it
|
||||
if (!Files.exists(changed)) {
|
||||
event("Could not obtain read lock even after timeout. Ignoring change ...");
|
||||
return;
|
||||
|
@ -105,9 +105,8 @@ public class FileBridge {
|
|||
event("Registering modification for file " + TEMP.relativize(e.file));
|
||||
event("Last modification for file: " + e.lastModified.toString() + " vs current one: "
|
||||
+ e.getLastModified());
|
||||
if (e.hasChanged()) {
|
||||
if (e.registerChange()) {
|
||||
event("Registering change for file " + TEMP.relativize(e.file) + " for editor entry " + e.getName());
|
||||
e.registerChange();
|
||||
try (var in = Files.newInputStream(e.file)) {
|
||||
var actualSize = (long) in.available();
|
||||
var started = Instant.now();
|
||||
|
@ -219,6 +218,7 @@ public class FileBridge {
|
|||
private final BooleanScope scope;
|
||||
private final BiConsumer<InputStream, Long> writer;
|
||||
private Instant lastModified;
|
||||
private long lastSize;
|
||||
|
||||
public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer<InputStream, Long> writer) {
|
||||
this.file = file;
|
||||
|
@ -228,15 +228,6 @@ public class FileBridge {
|
|||
this.writer = writer;
|
||||
}
|
||||
|
||||
public boolean hasChanged() {
|
||||
try {
|
||||
var newDate = Files.getLastModifiedTime(file).toInstant();
|
||||
return !newDate.equals(lastModified);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Instant getLastModified() {
|
||||
try {
|
||||
return Files.getLastModifiedTime(file).toInstant();
|
||||
|
@ -245,8 +236,26 @@ public class FileBridge {
|
|||
}
|
||||
}
|
||||
|
||||
public void registerChange() {
|
||||
lastModified = getLastModified();
|
||||
public long getSize() {
|
||||
try {
|
||||
return Files.size(file);
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean registerChange() {
|
||||
var newSize = getSize();
|
||||
var newDate = getLastModified();
|
||||
// The size check is intended for cases in which editors first clear a file prior to writing it
|
||||
// In that case, multiple watch events are sent. If these happened very fast, it might be possible that
|
||||
// the modified time is the same for both write operations due to the file system modified time resolution being limited
|
||||
// We then can't identify changes purely based on the modified time, so the file size is the next best option
|
||||
// This might result in double change detection in rare cases, but that is irrelevant as it prevents files from being blanked
|
||||
var changed = !newDate.equals(getLastModified()) || newSize > lastSize;
|
||||
lastSize = newSize;
|
||||
lastModified = newDate;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,8 @@
|
|||
package io.xpipe.app.util;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppStyle;
|
||||
import io.xpipe.app.core.AppTheme;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.core.util.InPlaceSecretValue;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class UnlockAlert {
|
||||
|
||||
|
@ -28,40 +16,9 @@ public class UnlockAlert {
|
|||
return;
|
||||
}
|
||||
|
||||
PlatformState.initPlatformOrThrow();
|
||||
AppI18n.init();
|
||||
AppStyle.init();
|
||||
AppTheme.init();
|
||||
|
||||
while (true) {
|
||||
var pw = new SimpleObjectProperty<InPlaceSecretValue>();
|
||||
var canceled = new SimpleBooleanProperty();
|
||||
AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("unlockAlertTitle"));
|
||||
alert.setHeaderText(AppI18n.get("unlockAlertHeader"));
|
||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||
|
||||
var text = new SecretFieldComp(pw, false).createRegion();
|
||||
text.setStyle("-fx-border-width: 1px");
|
||||
|
||||
var content = new VBox(text);
|
||||
content.setSpacing(5);
|
||||
alert.getDialogPane().setContent(content);
|
||||
|
||||
var stage = (Stage) alert.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
|
||||
alert.setOnShown(event -> {
|
||||
stage.requestFocus();
|
||||
// Wait 1 pulse before focus so that the scene can be assigned to text
|
||||
Platform.runLater(text::requestFocus);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
.filter(b -> b.getButtonData().isDefaultButton())
|
||||
.ifPresentOrElse(t -> {}, () -> canceled.set(true));
|
||||
|
||||
if (canceled.get()) {
|
||||
var r = AskpassAlert.queryRaw(AppI18n.get("unlockAlertHeader"), null);
|
||||
if (r.getState() == SecretQueryState.CANCELLED) {
|
||||
ErrorEvent.fromMessage("Unlock cancelled")
|
||||
.expected()
|
||||
.term()
|
||||
|
@ -70,7 +27,7 @@ public class UnlockAlert {
|
|||
return;
|
||||
}
|
||||
|
||||
if (AppPrefs.get().unlock(pw.get())) {
|
||||
if (AppPrefs.get().unlock(r.getSecret().inPlace())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3535,7 +3535,7 @@ xor
|
|||
|
||||
|Name|Type|Required|Restrictions|Description|
|
||||
|---|---|---|---|---|
|
||||
|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.|
|
||||
|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.<br><br>You can find the authentication file at:<br>- %TEMP%\xpipe_auth on Windows<br>- $TMP/xpipe_auth on Linux<br>- $TMPDIR/xpipe_auth on macOS<br><br>For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions.<br><br>As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file.|
|
||||
|
||||
<h2 id="tocS_ApiKey">ApiKey</h2>
|
||||
|
||||
|
@ -3578,12 +3578,21 @@ API key authentication
|
|||
|
||||
Authentication method for local applications. Uses file system access as proof of authentication.
|
||||
|
||||
You can find the authentication file at:
|
||||
- %TEMP%\xpipe_auth on Windows
|
||||
- $TMP/xpipe_auth on Linux
|
||||
- $TMPDIR/xpipe_auth on macOS
|
||||
|
||||
For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions.
|
||||
|
||||
As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file.
|
||||
|
||||
<h3>Properties</h3>
|
||||
|
||||
|Name|Type|Required|Restrictions|Description|
|
||||
|---|---|---|---|---|
|
||||
|type|string|true|none|none|
|
||||
|authFileContent|string|true|none|The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.|
|
||||
|authFileContent|string|true|none|The contents of the local file <temp dir>/xpipe_auth. This file is automatically generated when XPipe starts.|
|
||||
|
||||
<h2 id="tocS_ClientInformation">ClientInformation</h2>
|
||||
|
||||
|
|
|
@ -36,5 +36,5 @@
|
|||
-fx-min-height: 3.5em;
|
||||
-fx-pref-height: 3.5em;
|
||||
-fx-max-height: 3.5em;
|
||||
-fx-padding: 9 6 9 8;
|
||||
-fx-padding: 10 6 10 8;
|
||||
}
|
||||
|
|
|
@ -166,7 +166,7 @@
|
|||
}
|
||||
|
||||
.browser .path-text, .browser .browser-filter .text-field {
|
||||
-fx-padding: 6 12;
|
||||
-fx-padding: 5 12;
|
||||
}
|
||||
|
||||
.browser .path-text:invisible {
|
||||
|
|
|
@ -37,6 +37,14 @@
|
|||
-fx-opacity: 0.5;
|
||||
}
|
||||
|
||||
.store-entry-grid .name {
|
||||
-fx-padding: 2 0 0 0;
|
||||
}
|
||||
|
||||
.store-entry-grid.dense .name {
|
||||
-fx-padding: 0 0 0 0;
|
||||
}
|
||||
|
||||
.store-entry-grid .icon {
|
||||
-fx-background-color: -color-bg-overlay;
|
||||
-fx-background-radius: 5px;
|
||||
|
@ -90,6 +98,14 @@
|
|||
-fx-opacity: 0.2;
|
||||
}
|
||||
|
||||
.store-entry-comp .button-bar {
|
||||
-fx-padding: 5;
|
||||
}
|
||||
|
||||
.store-entry-grid.dense .button-bar {
|
||||
-fx-padding: 3;
|
||||
}
|
||||
|
||||
.store-entry-comp .button-bar .button {
|
||||
-fx-padding: 6px;
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
}
|
||||
|
||||
.toggle-switch:has-graphic {
|
||||
-fx-font-size: 0.8em;
|
||||
-fx-font-size: 0.75em;
|
||||
}
|
||||
|
||||
.store-layout .split-pane-divider {
|
||||
|
|
|
@ -24,6 +24,15 @@ public class BeaconClient {
|
|||
this.port = port;
|
||||
}
|
||||
|
||||
public static boolean isOccupied(int port) {
|
||||
var file = XPipeInstallation.getLocalBeaconAuthFile();
|
||||
var reachable = BeaconServer.isReachable(port);
|
||||
if (!Files.exists(file) && !reachable) {
|
||||
return false;
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
|
||||
public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception {
|
||||
var client = new BeaconClient(port);
|
||||
var auth = Files.readString(XPipeInstallation.getLocalBeaconAuthFile());
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.io.InputStream;
|
|||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
@ -59,6 +60,8 @@ public interface CommandControl extends ProcessControl {
|
|||
|
||||
OutputStream startExternalStdin() throws Exception;
|
||||
|
||||
public void setExitTimeout(Duration duration);
|
||||
|
||||
boolean waitFor();
|
||||
|
||||
CommandControl withCustomCharset(Charset charset);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package io.xpipe.core.store;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
@ -53,10 +53,9 @@ public class ConnectionFileSystem implements FileSystem {
|
|||
|
||||
@Override
|
||||
public OutputStream openOutput(String file, long totalBytes) throws Exception {
|
||||
return shellControl
|
||||
.getShellDialect()
|
||||
.createStreamFileWriteCommand(shellControl, file, totalBytes)
|
||||
.startExternalStdin();
|
||||
var cmd = shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file, totalBytes);
|
||||
cmd.setExitTimeout(Duration.ofMillis(Long.MAX_VALUE));
|
||||
return cmd.startExternalStdin();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package io.xpipe.core.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ServiceLoader;
|
||||
|
@ -88,4 +90,32 @@ public class JacksonMapper {
|
|||
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
|
||||
public static ObjectMapper getCensored() {
|
||||
if (!JacksonMapper.isInit()) {
|
||||
return BASE;
|
||||
}
|
||||
|
||||
var c = INSTANCE.copy();
|
||||
c.registerModule(new SimpleModule() {
|
||||
@Override
|
||||
public void setupModule(SetupContext context) {
|
||||
addSerializer(SecretValue.class, new JsonSerializer<>() {
|
||||
@Override
|
||||
public void serialize(SecretValue value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeString("<secret>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeWithType(SecretValue value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws
|
||||
IOException {
|
||||
gen.writeString("<secret>");
|
||||
}
|
||||
});
|
||||
super.setupModule(context);
|
||||
}
|
||||
});
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ public class XPipeInstallation {
|
|||
}
|
||||
|
||||
public static Path getLocalBeaconAuthFile() {
|
||||
return Path.of(System.getProperty("java.io.tmpdir"), "xpipe_auth");
|
||||
return Path.of(System.getProperty("java.io.tmpdir"), isStaging() ? "xpipe_ptb_auth" : "xpipe_auth");
|
||||
}
|
||||
|
||||
public static String createExternalAsyncLaunchCommand(
|
||||
|
|
16
dist/changelogs/10.1_incremental.md
vendored
16
dist/changelogs/10.1_incremental.md
vendored
|
@ -1,3 +1,14 @@
|
|||
## Browser improvements
|
||||
|
||||
Feedback showed that the file browser transfer pane in the bottom left was confusing and unintuitive to use. Therefore, it has now been changed to be a more straightforward download area. You can drag files into it to automatically download them. From there you can either drag them directly where you want them to be in your local desktop environment or move them into the downloads directory.
|
||||
|
||||
There is now the possibility to jump to a file in a directory by typing the first few characters of its name.
|
||||
|
||||
There were also a couple of bug fixes:
|
||||
- Fix file transfers on Windows systems failing for files > 2GB due to overflow
|
||||
- Fix remote file editing sometimes creating blank file when using vscode
|
||||
- Fix file transfers failing at the end with a timeout when the connection speed was very slow
|
||||
|
||||
## API additions
|
||||
|
||||
Several new endpoints have been added to widen the capabilities for external clients:
|
||||
|
@ -11,7 +22,10 @@ Several new endpoints have been added to widen the capabilities for external cli
|
|||
|
||||
## Other
|
||||
|
||||
- Fix xpipe not starting up when changing user on Linux
|
||||
- Fix some editors and terminals not launching when using the fallback sh system shell due to missing disown command
|
||||
- Fix csh sudo elevation not working
|
||||
- Implement various application performance improvements
|
||||
- Rework sidebar styling
|
||||
- Improve transparency styling on Windows 11
|
||||
- Fix csh sudo elevation not working
|
||||
- Add support for zed editor
|
67
dist/changelogs/10.2.md
vendored
Normal file
67
dist/changelogs/10.2.md
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
## A new HTTP API
|
||||
|
||||
There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages.
|
||||
|
||||
To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files.
|
||||
|
||||
There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects.
|
||||
|
||||
## Service integration
|
||||
|
||||
Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port.
|
||||
|
||||
You can use an unlimited amount of local services and one active tunneled service in the community edition.
|
||||
|
||||
## Script rework
|
||||
|
||||
The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types:
|
||||
- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently
|
||||
- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments.
|
||||
- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files
|
||||
|
||||
If you have existing scripts, they will have to be manually adjusted by setting their execution types.
|
||||
|
||||
## Docker improvements
|
||||
|
||||
The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition.
|
||||
|
||||
There's now support for Windows docker containers running on HyperV.
|
||||
|
||||
Note that old docker container connections will be removed as they are incompatible with the new version.
|
||||
|
||||
## Proxmox improvements
|
||||
|
||||
You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers.
|
||||
|
||||
You can now open VNC sessions to Proxmox VMs.
|
||||
|
||||
The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition.
|
||||
|
||||
## Better connection organization
|
||||
|
||||
The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general.
|
||||
|
||||
You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left.
|
||||
|
||||
The UI has also been streamlined to make common actions and toggles more easily accessible.
|
||||
|
||||
## Other
|
||||
|
||||
- The title bar on Windows will now follow the appearance theme
|
||||
- Several more actions have been added for podman containers
|
||||
- Support VMs for tunneling
|
||||
- Searching for connections has been improved to show children as well
|
||||
- There is now an AppImage portable release
|
||||
- The welcome screen will now also contain the option to straight up jump to the synchronization settings
|
||||
- You can now launch xpipe in another data directory with `xpipe open -d "<dir>"`
|
||||
- Add option to use double clicks to open connections instead of single clicks
|
||||
- Add support for foot terminal
|
||||
- Fix rare null pointers and freezes in file browser
|
||||
- Fix PowerShell remote session file editing not transferring file correctly
|
||||
- Fix elementary terminal not launching correctly
|
||||
- Fix windows jumping around when created
|
||||
- Fix kubernetes not elevating correctly for non-default contexts
|
||||
- Fix ohmyzsh update notification freezing shell
|
||||
- Fix file browser icons being broken for links
|
||||
- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality
|
||||
- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues
|
1
dist/changelogs/10.2_incremental.md
vendored
Normal file
1
dist/changelogs/10.2_incremental.md
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
- Rework UI sizing to allow more content to be shown
|
|
@ -2,6 +2,7 @@ package io.xpipe.ext.base.script;
|
|||
|
||||
import io.xpipe.app.comp.base.SystemStateComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.ext.*;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
|
@ -76,8 +77,8 @@ public class ScriptGroupStoreProvider implements EnabledStoreProvider, DataStore
|
|||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
ScriptGroupStore scriptStore = wrapper.getEntry().getStore().asNeeded();
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
ScriptGroupStore scriptStore = section.getWrapper().getEntry().getStore().asNeeded();
|
||||
return new SimpleStringProperty(scriptStore.getDescription());
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.IntegratedTextAreaComp;
|
|||
import io.xpipe.app.comp.base.ListSelectorComp;
|
||||
import io.xpipe.app.comp.base.SystemStateComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppExtensionManager;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
|
@ -206,8 +207,8 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da
|
|||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded();
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
SimpleScriptStore scriptStore = section.getWrapper().getEntry().getStore().asNeeded();
|
||||
return new SimpleStringProperty((scriptStore.getMinimumDialect() != null
|
||||
? scriptStore.getMinimumDialect().getDisplayName() + " "
|
||||
: "")
|
||||
|
|
|
@ -6,9 +6,11 @@ import io.xpipe.app.comp.store.StoreEntryComp;
|
|||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreUsageCategory;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
@ -16,6 +18,7 @@ import io.xpipe.core.store.DataStore;
|
|||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
public abstract class AbstractServiceGroupStoreProvider implements DataStoreProvider {
|
||||
|
||||
|
@ -27,7 +30,7 @@ public abstract class AbstractServiceGroupStoreProvider implements DataStoreProv
|
|||
@Override
|
||||
public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
var t = createToggleComp(sec);
|
||||
return StoreEntryComp.create(sec.getWrapper(), t, preferLarge);
|
||||
return StoreEntryComp.create(sec, t, preferLarge);
|
||||
}
|
||||
|
||||
private StoreToggleComp createToggleComp(StoreSection sec) {
|
||||
|
@ -62,6 +65,16 @@ public abstract class AbstractServiceGroupStoreProvider implements DataStoreProv
|
|||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
return Bindings.createStringBinding(() -> {
|
||||
var all = section.getAllChildren().getList();
|
||||
var shown = section.getShownChildren().getList();
|
||||
var string = all.size() == shown.size() ? all.size() : shown.size() + "/" + all.size();
|
||||
return all.size() > 0 ? (all.size() == 1 ? AppI18n.get("hasService", string) : AppI18n.get("hasServices", string)) : AppI18n.get("noServices");
|
||||
}, section.getShownChildren().getList(), section.getAllChildren().getList(), AppPrefs.get().language());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> stateDisplay(StoreEntryWrapper w) {
|
||||
return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.ext.base.service;
|
||||
|
||||
import io.xpipe.app.comp.base.SystemStateComp;
|
||||
import io.xpipe.app.comp.store.DenseStoreEntryComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryComp;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
|
@ -80,7 +81,7 @@ public abstract class AbstractServiceStoreProvider implements SingletonSessionSt
|
|||
return true;
|
||||
},
|
||||
sec.getWrapper().getCache()));
|
||||
return StoreEntryComp.create(sec.getWrapper(), toggle, preferLarge);
|
||||
return new DenseStoreEntryComp(sec, true, toggle);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -98,8 +99,8 @@ public abstract class AbstractServiceStoreProvider implements SingletonSessionSt
|
|||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
AbstractServiceStore s = section.getWrapper().getEntry().getStore().asNeeded();
|
||||
if (s.getLocalPort() != null) {
|
||||
return new SimpleStringProperty("Port " + s.getLocalPort() + " <- " + s.getRemotePort());
|
||||
} else {
|
||||
|
|
|
@ -24,6 +24,7 @@ public class FixedServiceStore extends AbstractServiceStore implements FixedChil
|
|||
|
||||
@Override
|
||||
public void checkComplete() throws Throwable {
|
||||
super.checkComplete();
|
||||
Validators.nonNull(displayParent);
|
||||
Validators.nonNull(displayParent.getStore());
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.xpipe.ext.base.service;
|
||||
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
|
||||
|
@ -29,8 +29,8 @@ public class FixedServiceStoreProvider extends AbstractServiceStoreProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
FixedServiceStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
FixedServiceStore s = section.getWrapper().getEntry().getStore().asNeeded();
|
||||
return new SimpleStringProperty("Port " + s.getRemotePort());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.xpipe.ext.base.service;
|
||||
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
|
||||
import io.xpipe.app.comp.store.StoreSection;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
|
@ -15,9 +14,9 @@ public class MappedServiceStoreProvider extends FixedServiceStoreProvider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
MappedServiceStore s = wrapper.getEntry().getStore().asNeeded();
|
||||
return new SimpleStringProperty("Port " + s.getContainerPort() + " -> " + s.getRemotePort());
|
||||
public ObservableValue<String> informationString(StoreSection section) {
|
||||
MappedServiceStore s = section.getWrapper().getEntry().getStore().asNeeded();
|
||||
return new SimpleStringProperty("Port " + s.getRemotePort() + " -> " + s.getContainerPort());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -167,3 +167,6 @@ customService.displayName=Service
|
|||
customService.displayDescription=Tilføj en brugerdefineret tjeneste til tunnel og åben
|
||||
fixedService.displayName=Service
|
||||
fixedService.displayDescription=Brug en foruddefineret tjeneste
|
||||
noServices=Ingen tilgængelige tjenester
|
||||
hasServices=$COUNT$ tilgængelige tjenester
|
||||
hasService=$COUNT$ tilgængelig tjeneste
|
||||
|
|
|
@ -158,3 +158,6 @@ customService.displayName=Dienst
|
|||
customService.displayDescription=Einen benutzerdefinierten Dienst zum Tunnel hinzufügen und öffnen
|
||||
fixedService.displayName=Dienst
|
||||
fixedService.displayDescription=Einen vordefinierten Dienst verwenden
|
||||
noServices=Keine verfügbaren Dienste
|
||||
hasServices=$COUNT$ verfügbare Dienste
|
||||
hasService=$COUNT$ verfügbarer Dienst
|
||||
|
|
|
@ -156,5 +156,8 @@ customService.displayName=Service
|
|||
customService.displayDescription=Add a custom service to tunnel and open
|
||||
fixedService.displayName=Service
|
||||
fixedService.displayDescription=Use a predefined service
|
||||
noServices=No available services
|
||||
hasServices=$COUNT$ available services
|
||||
hasService=$COUNT$ available service
|
||||
|
||||
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Servicio
|
|||
customService.displayDescription=Añade un servicio personalizado para tunelizar y abrir
|
||||
fixedService.displayName=Servicio
|
||||
fixedService.displayDescription=Utilizar un servicio predefinido
|
||||
noServices=No hay servicios disponibles
|
||||
hasServices=$COUNT$ servicios disponibles
|
||||
hasService=$COUNT$ servicio disponible
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Service
|
|||
customService.displayDescription=Ajouter un service personnalisé au tunnel et à l'ouverture
|
||||
fixedService.displayName=Service
|
||||
fixedService.displayDescription=Utiliser un service prédéfini
|
||||
noServices=Aucun service disponible
|
||||
hasServices=$COUNT$ services disponibles
|
||||
hasService=$COUNT$ service disponible
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Servizio
|
|||
customService.displayDescription=Aggiungi un servizio personalizzato per il tunnel e l'apertura
|
||||
fixedService.displayName=Servizio
|
||||
fixedService.displayDescription=Utilizzare un servizio predefinito
|
||||
noServices=Nessun servizio disponibile
|
||||
hasServices=$COUNT$ servizi disponibili
|
||||
hasService=$COUNT$ servizio disponibile
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=サービス
|
|||
customService.displayDescription=トンネルとオープンにカスタムサービスを追加する
|
||||
fixedService.displayName=サービス
|
||||
fixedService.displayDescription=定義済みのサービスを使う
|
||||
noServices=利用可能なサービスはない
|
||||
hasServices=$COUNT$ 利用可能なサービス
|
||||
hasService=$COUNT$ 利用可能なサービス
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Service
|
|||
customService.displayDescription=Een aangepaste service toevoegen aan tunnel en openen
|
||||
fixedService.displayName=Service
|
||||
fixedService.displayDescription=Een vooraf gedefinieerde service gebruiken
|
||||
noServices=Geen beschikbare diensten
|
||||
hasServices=$COUNT$ beschikbare diensten
|
||||
hasService=$COUNT$ beschikbare dienst
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Serviço
|
|||
customService.displayDescription=Adiciona um serviço personalizado ao túnel e abre
|
||||
fixedService.displayName=Serviço
|
||||
fixedService.displayDescription=Utiliza um serviço predefinido
|
||||
noServices=Não há serviços disponíveis
|
||||
hasServices=$COUNT$ serviços disponíveis
|
||||
hasService=$COUNT$ serviço disponível
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Сервис
|
|||
customService.displayDescription=Добавьте пользовательский сервис для туннелирования и открытия
|
||||
fixedService.displayName=Сервис
|
||||
fixedService.displayDescription=Использовать предопределенный сервис
|
||||
noServices=Нет доступных сервисов
|
||||
hasServices=$COUNT$ доступные сервисы
|
||||
hasService=$COUNT$ доступный сервис
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=Hizmet
|
|||
customService.displayDescription=Tünele özel bir hizmet ekleyin ve açın
|
||||
fixedService.displayName=Hizmet
|
||||
fixedService.displayDescription=Önceden tanımlanmış bir hizmet kullanın
|
||||
noServices=Mevcut hizmet yok
|
||||
hasServices=$COUNT$ mevcut hi̇zmetler
|
||||
hasService=$COUNT$ mevcut hizmet
|
||||
|
|
|
@ -156,3 +156,6 @@ customService.displayName=服务
|
|||
customService.displayDescription=为隧道和开放添加自定义服务
|
||||
fixedService.displayName=服务
|
||||
fixedService.displayDescription=使用预定义服务
|
||||
noServices=无可用服务
|
||||
hasServices=$COUNT$ 可用服务
|
||||
hasService=$COUNT$ 可用服务
|
||||
|
|
21
openapi.yaml
21
openapi.yaml
|
@ -55,11 +55,14 @@ paths:
|
|||
$ref: '#/components/schemas/HandshakeRequest'
|
||||
examples:
|
||||
standard:
|
||||
summary: Standard handshake
|
||||
summary: API key handshake
|
||||
value: { "auth": { "type": "ApiKey", "key": "<API key>" }, "client": { "type": "Api", "name": "My client name" } }
|
||||
local:
|
||||
summary: Local application handshake
|
||||
value: { "auth": { "type": "Local", "authFileContent": "<Contents of the local file $TEMP/xpipe_auth>" }, "client": { "type": "Api", "name": "My client name" } }
|
||||
value: { "auth": { "type": "Local", "authFileContent": "<Contents of the local file <temp dir>/xpipe_auth>" }, "client": { "type": "Api", "name": "My client name" } }
|
||||
local-ptb:
|
||||
summary: Local PTB application handshake
|
||||
value: { "auth": { "type": "Local", "authFileContent": "<Contents of the local file <temp dir>/xpipe_ptb_auth>" }, "client": { "type": "Api", "name": "My client name" } }
|
||||
responses:
|
||||
'200':
|
||||
description: The handshake was successful. The returned token can be used for authentication in this session. The token is valid as long as XPipe is running.
|
||||
|
@ -966,14 +969,24 @@ components:
|
|||
- key
|
||||
- type
|
||||
Local:
|
||||
description: Authentication method for local applications. Uses file system access as proof of authentication.
|
||||
description: |
|
||||
Authentication method for local applications. Uses file system access as proof of authentication.
|
||||
|
||||
You can find the authentication file at:
|
||||
- %TEMP%\xpipe_auth on Windows
|
||||
- $TMP/xpipe_auth on Linux
|
||||
- $TMPDIR/xpipe_auth on macOS
|
||||
|
||||
For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions.
|
||||
|
||||
As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file.
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
authFileContent:
|
||||
type: string
|
||||
description: The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.
|
||||
description: The contents of the local file <temp dir>/xpipe_auth. This file is automatically generated when XPipe starts.
|
||||
required:
|
||||
- authFileContent
|
||||
- type
|
||||
|
|
2
version
2
version
|
@ -1 +1 @@
|
|||
10.1-8
|
||||
10.2-1
|
||||
|
|
Loading…
Reference in a new issue