diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index 7e6cb7f14..2d654c0f3 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java index d231c96a0..11ac2de4f 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -124,10 +124,9 @@ public class BeaconRequestHandler 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); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java index 2c59091b1..8aaef604c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java @@ -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)) diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java index b350ea074..f326e635e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -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; } }); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index 2d5f26135..5454ab7f7 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -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)); diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java index 526699132..93f4c60b1 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java @@ -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, diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java index d2b6ae678..3809c2341 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java @@ -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., ObservableValue>of( - Comp.of(() -> createTabPane()), - Bindings.isNotEmpty(model.getSessionEntries()), - new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), + var map = new LinkedHashMap, ObservableValue>(); + 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(); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java index 0c26d5b09..d1cf12b38 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java @@ -75,7 +75,7 @@ public class SideMenuBarComp extends Comp> { } 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> { .tooltipKey("updateAvailableTooltip") .accessibleTextKey("updateAvailableTooltip"); b.apply(struc -> { - AppFont.setSize(struc.get(), 2); + AppFont.setSize(struc.get(), 1); }); b.hide(PlatformThread.sync(Bindings.createBooleanBinding( () -> { diff --git a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java index a6c5db926..0b0b5f756 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java @@ -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); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java index 595e7bc56..074426dee 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java index c5ad100c6..414e8ac48 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java @@ -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())) { diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index 045d05579..8ea05c3dc 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -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()); }); }); diff --git a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java index 5d1318b7a..a7c39905c 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java +++ b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java @@ -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(); } }); } diff --git a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java index 0c954941e..ca3efcdd4 100644 --- a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java +++ b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java @@ -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 list = (ObservableList) windowsField.get(null); + ObservableList list = Window.getWindows(); list.addListener((ListChangeListener) c -> { if (c.next() && c.wasAdded()) { var added = c.getAddedSubList().getFirst(); diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index 9f27e842b..e3bca12be 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -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 informationString(StoreEntryWrapper wrapper) { + default ObservableValue informationString(StoreSection section) { return new SimpleStringProperty(null); } diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java index 22e2c1a4d..21911e239 100644 --- a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java @@ -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.>enableToggle( @@ -35,6 +35,6 @@ public interface EnabledParentStoreProvider extends DataStoreProvider { })); } - return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge); + return StoreEntryComp.create(sec, enabled, preferLarge); } } diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java index cf236e9ad..35a11f689 100644 --- a/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java @@ -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.>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); } } diff --git a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java index 0dc04c93d..c856c0121 100644 --- a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java @@ -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) { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java index f10f1de61..34a8ca7e2 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java @@ -39,7 +39,7 @@ public class FilterComp extends Comp> { 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"); diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java index 689125423..e587374be 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -132,6 +132,14 @@ public class LauncherCommand implements Callable { + " 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() { diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index 52ee01170..5d5893fc8 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -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 WINDOWS_EDITORS = List.of(VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD); List LINUX_EDITORS = - List.of(VSCODIUM_LINUX, VSCODE_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME); - List 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 MACOS_EDITORS = List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT); List CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION); @SuppressWarnings("TrivialFunctionalExpressionUsage") diff --git a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java index 407c38928..23d359472 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java @@ -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") diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java index 86e98c7d4..cba1d6179 100644 --- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java @@ -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(); } } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 2573e747c..55dc5b8e9 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -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> 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(); } diff --git a/app/src/main/java/io/xpipe/app/update/AppDownloads.java b/app/src/main/java/io/xpipe/app/update/AppDownloads.java index 27fa9550b..9e5f03064 100644 --- a/app/src/main/java/io/xpipe/app/update/AppDownloads.java +++ b/app/src/main/java/io/xpipe/app/update/AppDownloads.java @@ -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 { diff --git a/app/src/main/java/io/xpipe/app/util/FileBridge.java b/app/src/main/java/io/xpipe/app/util/FileBridge.java index f726e764e..dd24cb5d9 100644 --- a/app/src/main/java/io/xpipe/app/util/FileBridge.java +++ b/app/src/main/java/io/xpipe/app/util/FileBridge.java @@ -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 writer; private Instant lastModified; + private long lastSize; public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer 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; } } } diff --git a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java index e13427780..3f69f5279 100644 --- a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java +++ b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java @@ -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(); - 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; } } diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/api.md b/app/src/main/resources/io/xpipe/app/resources/misc/api.md index 032417a5d..e0361cee1 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/api.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md @@ -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.

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

ApiKey

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

Properties

|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 /xpipe_auth. This file is automatically generated when XPipe starts.|

ClientInformation

diff --git a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css index 068cb1a08..d6d7fc33d 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css @@ -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; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index f349e7241..a91d22222 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -166,7 +166,7 @@ } .browser .path-text, .browser .browser-filter .text-field { - -fx-padding: 6 12; + -fx-padding: 5 12; } .browser .path-text:invisible { diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css index 10a77eb29..d14b23437 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css @@ -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; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css index aac574754..2c4ddece8 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/style.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css @@ -67,7 +67,7 @@ } .toggle-switch:has-graphic { - -fx-font-size: 0.8em; + -fx-font-size: 0.75em; } .store-layout .split-pane-divider { diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index be1e2dae7..26867ef94 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -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()); diff --git a/core/src/main/java/io/xpipe/core/process/CommandControl.java b/core/src/main/java/io/xpipe/core/process/CommandControl.java index 7849cf087..1e4dc6070 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandControl.java +++ b/core/src/main/java/io/xpipe/core/process/CommandControl.java @@ -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); diff --git a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java index 6073bd7be..56dc9fdf0 100644 --- a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java @@ -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 diff --git a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java index ccb028425..481b5e626 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java @@ -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(""); + } + + @Override + public void serializeWithType(SecretValue value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws + IOException { + gen.writeString(""); + } + }); + super.setupModule(context); + } + }); + return c; + } } diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java index b5557c39a..09b0e8994 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -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( diff --git a/dist/changelogs/10.1_incremental.md b/dist/changelogs/10.1_incremental.md index 12ceddf1f..e0b96df2d 100644 --- a/dist/changelogs/10.1_incremental.md +++ b/dist/changelogs/10.1_incremental.md @@ -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 \ No newline at end of file +- Add support for zed editor \ No newline at end of file diff --git a/dist/changelogs/10.2.md b/dist/changelogs/10.2.md new file mode 100644 index 000000000..25037ee47 --- /dev/null +++ b/dist/changelogs/10.2.md @@ -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 ""` +- 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 diff --git a/dist/changelogs/10.2_incremental.md b/dist/changelogs/10.2_incremental.md new file mode 100644 index 000000000..15f2cb4ff --- /dev/null +++ b/dist/changelogs/10.2_incremental.md @@ -0,0 +1 @@ +- Rework UI sizing to allow more content to be shown \ No newline at end of file diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java index ebbe867f0..2cc604ff4 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java @@ -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 informationString(StoreEntryWrapper wrapper) { - ScriptGroupStore scriptStore = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + ScriptGroupStore scriptStore = section.getWrapper().getEntry().getStore().asNeeded(); return new SimpleStringProperty(scriptStore.getDescription()); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java index 2a11010a8..5d1fd3801 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java @@ -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 informationString(StoreEntryWrapper wrapper) { - SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + SimpleScriptStore scriptStore = section.getWrapper().getEntry().getStore().asNeeded(); return new SimpleStringProperty((scriptStore.getMinimumDialect() != null ? scriptStore.getMinimumDialect().getDisplayName() + " " : "") diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java index 164927050..cdd79d56e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java @@ -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 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)); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java index eb7053f33..f577849d7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java @@ -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 informationString(StoreEntryWrapper wrapper) { - AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + AbstractServiceStore s = section.getWrapper().getEntry().getStore().asNeeded(); if (s.getLocalPort() != null) { return new SimpleStringProperty("Port " + s.getLocalPort() + " <- " + s.getRemotePort()); } else { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java index a7203c83b..d4237eb4d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java @@ -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()); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java index dedc7212c..341d5c5cd 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java @@ -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 informationString(StoreEntryWrapper wrapper) { - FixedServiceStore s = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + FixedServiceStore s = section.getWrapper().getEntry().getStore().asNeeded(); return new SimpleStringProperty("Port " + s.getRemotePort()); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java index 1fb94c6c3..c057051b7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java @@ -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 informationString(StoreEntryWrapper wrapper) { - MappedServiceStore s = wrapper.getEntry().getStore().asNeeded(); - return new SimpleStringProperty("Port " + s.getContainerPort() + " -> " + s.getRemotePort()); + public ObservableValue informationString(StoreSection section) { + MappedServiceStore s = section.getWrapper().getEntry().getStore().asNeeded(); + return new SimpleStringProperty("Port " + s.getRemotePort() + " -> " + s.getContainerPort()); } @Override diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f7a6eb33..dedd5d1e6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/lang/base/strings/translations_da.properties b/lang/base/strings/translations_da.properties index 57d850e2b..a8c3ea780 100644 --- a/lang/base/strings/translations_da.properties +++ b/lang/base/strings/translations_da.properties @@ -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 diff --git a/lang/base/strings/translations_de.properties b/lang/base/strings/translations_de.properties index 55f04ac61..9669d8ec6 100644 --- a/lang/base/strings/translations_de.properties +++ b/lang/base/strings/translations_de.properties @@ -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 diff --git a/lang/base/strings/translations_en.properties b/lang/base/strings/translations_en.properties index 08f77e1d2..38afa7820 100644 --- a/lang/base/strings/translations_en.properties +++ b/lang/base/strings/translations_en.properties @@ -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 diff --git a/lang/base/strings/translations_es.properties b/lang/base/strings/translations_es.properties index bbcd05db6..1281887b1 100644 --- a/lang/base/strings/translations_es.properties +++ b/lang/base/strings/translations_es.properties @@ -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 diff --git a/lang/base/strings/translations_fr.properties b/lang/base/strings/translations_fr.properties index 476f18079..80a30b1bb 100644 --- a/lang/base/strings/translations_fr.properties +++ b/lang/base/strings/translations_fr.properties @@ -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 diff --git a/lang/base/strings/translations_it.properties b/lang/base/strings/translations_it.properties index 11cdd26e5..79e3e065e 100644 --- a/lang/base/strings/translations_it.properties +++ b/lang/base/strings/translations_it.properties @@ -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 diff --git a/lang/base/strings/translations_ja.properties b/lang/base/strings/translations_ja.properties index 0dfaae5d6..e33d06fcc 100644 --- a/lang/base/strings/translations_ja.properties +++ b/lang/base/strings/translations_ja.properties @@ -156,3 +156,6 @@ customService.displayName=サービス customService.displayDescription=トンネルとオープンにカスタムサービスを追加する fixedService.displayName=サービス fixedService.displayDescription=定義済みのサービスを使う +noServices=利用可能なサービスはない +hasServices=$COUNT$ 利用可能なサービス +hasService=$COUNT$ 利用可能なサービス diff --git a/lang/base/strings/translations_nl.properties b/lang/base/strings/translations_nl.properties index ea34666bc..634935425 100644 --- a/lang/base/strings/translations_nl.properties +++ b/lang/base/strings/translations_nl.properties @@ -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 diff --git a/lang/base/strings/translations_pt.properties b/lang/base/strings/translations_pt.properties index 58740c2d5..bdf43d927 100644 --- a/lang/base/strings/translations_pt.properties +++ b/lang/base/strings/translations_pt.properties @@ -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 diff --git a/lang/base/strings/translations_ru.properties b/lang/base/strings/translations_ru.properties index a9ecfcb5a..d88194bdb 100644 --- a/lang/base/strings/translations_ru.properties +++ b/lang/base/strings/translations_ru.properties @@ -156,3 +156,6 @@ customService.displayName=Сервис customService.displayDescription=Добавьте пользовательский сервис для туннелирования и открытия fixedService.displayName=Сервис fixedService.displayDescription=Использовать предопределенный сервис +noServices=Нет доступных сервисов +hasServices=$COUNT$ доступные сервисы +hasService=$COUNT$ доступный сервис diff --git a/lang/base/strings/translations_tr.properties b/lang/base/strings/translations_tr.properties index 6a665bff3..2b43eb831 100644 --- a/lang/base/strings/translations_tr.properties +++ b/lang/base/strings/translations_tr.properties @@ -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 diff --git a/lang/base/strings/translations_zh.properties b/lang/base/strings/translations_zh.properties index e9973d8ba..0cd88e0b8 100644 --- a/lang/base/strings/translations_zh.properties +++ b/lang/base/strings/translations_zh.properties @@ -156,3 +156,6 @@ customService.displayName=服务 customService.displayDescription=为隧道和开放添加自定义服务 fixedService.displayName=服务 fixedService.displayDescription=使用预定义服务 +noServices=无可用服务 +hasServices=$COUNT$ 可用服务 +hasService=$COUNT$ 可用服务 diff --git a/openapi.yaml b/openapi.yaml index d8547f806..1d77e7a5f 100644 --- a/openapi.yaml +++ b/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": "" }, "client": { "type": "Api", "name": "My client name" } } local: summary: Local application handshake - value: { "auth": { "type": "Local", "authFileContent": "" }, "client": { "type": "Api", "name": "My client name" } } + value: { "auth": { "type": "Local", "authFileContent": "/xpipe_auth>" }, "client": { "type": "Api", "name": "My client name" } } + local-ptb: + summary: Local PTB application handshake + value: { "auth": { "type": "Local", "authFileContent": "/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 /xpipe_auth. This file is automatically generated when XPipe starts. required: - authFileContent - type diff --git a/version b/version index 9eea5c0ae..6bb790913 100644 --- a/version +++ b/version @@ -1 +1 @@ -10.1-8 +10.2-1