This commit is contained in:
crschnick 2024-07-16 08:49:22 +00:00
parent d7055a435a
commit 781e1b3c84
62 changed files with 451 additions and 236 deletions

View file

@ -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;
}

View file

@ -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);

View file

@ -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))

View file

@ -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;
}
});

View file

@ -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));

View file

@ -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,

View file

@ -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();
}

View file

@ -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(
() -> {

View file

@ -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);
}

View file

@ -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);

View file

@ -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())) {

View file

@ -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());
});
});

View file

@ -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();
}
});
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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");

View file

@ -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() {

View file

@ -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")

View file

@ -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")

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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 {

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -166,7 +166,7 @@
}
.browser .path-text, .browser .browser-filter .text-field {
-fx-padding: 6 12;
-fx-padding: 5 12;
}
.browser .path-text:invisible {

View file

@ -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;
}

View file

@ -67,7 +67,7 @@
}
.toggle-switch:has-graphic {
-fx-font-size: 0.8em;
-fx-font-size: 0.75em;
}
.store-layout .split-pane-divider {

View file

@ -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());

View file

@ -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);

View file

@ -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

View file

@ -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;
}
}

View file

@ -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(

View file

@ -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
View 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
View file

@ -0,0 +1 @@
- Rework UI sizing to allow more content to be shown

View file

@ -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());
}

View file

@ -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() + " "
: "")

View file

@ -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));

View file

@ -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 {

View file

@ -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());
}

View file

@ -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());
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -156,3 +156,6 @@ customService.displayName=サービス
customService.displayDescription=トンネルとオープンにカスタムサービスを追加する
fixedService.displayName=サービス
fixedService.displayDescription=定義済みのサービスを使う
noServices=利用可能なサービスはない
hasServices=$COUNT$ 利用可能なサービス
hasService=$COUNT$ 利用可能なサービス

View file

@ -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

View file

@ -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

View file

@ -156,3 +156,6 @@ customService.displayName=Сервис
customService.displayDescription=Добавьте пользовательский сервис для туннелирования и открытия
fixedService.displayName=Сервис
fixedService.displayDescription=Использовать предопределенный сервис
noServices=Нет доступных сервисов
hasServices=$COUNT$ доступные сервисы
hasService=$COUNT$ доступный сервис

View file

@ -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

View file

@ -156,3 +156,6 @@ customService.displayName=服务
customService.displayDescription=为隧道和开放添加自定义服务
fixedService.displayName=服务
fixedService.displayDescription=使用预定义服务
noServices=无可用服务
hasServices=$COUNT$ 可用服务
hasService=$COUNT$ 可用服务

View file

@ -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

View file

@ -1 +1 @@
10.1-8
10.2-1