Rework pinning

This commit is contained in:
crschnick 2024-11-04 17:59:33 +00:00
parent 6dabe53011
commit 54fce7248f
212 changed files with 2800 additions and 1980 deletions

View file

@ -23,8 +23,8 @@ dependencies {
api project(':beacon') api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:3.0' compileOnly 'org.hamcrest:hamcrest:3.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.0' compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.3'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.0' compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.3'
api 'com.vladsch.flexmark:flexmark:0.64.8' api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8' api 'com.vladsch.flexmark:flexmark-util:0.64.8'
@ -58,8 +58,8 @@ dependencies {
api 'org.apache.commons:commons-lang3:3.17.0' api 'org.apache.commons:commons-lang3:3.17.0'
api 'io.sentry:sentry:7.14.0' api 'io.sentry:sentry:7.14.0'
api 'commons-io:commons-io:2.16.1' api 'commons-io:commons-io:2.16.1'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2" api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.1"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2" api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.1"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"

View file

@ -20,7 +20,7 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
throw new BeaconClientException("Not a file system connection"); throw new BeaconClientException("Not a file system connection");
} }
BrowserSessionModel.DEFAULT.openFileSystemSync( BrowserSessionModel.DEFAULT.openFileSystemSync(
e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null); e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null, true);
AppLayoutModel.get().selectBrowser(); AppLayoutModel.get().selectBrowser();
return Response.builder().build(); return Response.builder().build();
} }

View file

@ -15,9 +15,9 @@ public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
.getStoreEntryIfPresent(msg.getConnection()) .getStoreEntryIfPresent(msg.getConnection())
.orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection())); .orElseThrow(() -> new BeaconClientException("Unknown connection: " + msg.getConnection()));
if (e.getStore() instanceof FixedHierarchyStore) { if (e.getStore() instanceof FixedHierarchyStore) {
DataStorage.get().refreshChildren(e, null, true); DataStorage.get().refreshChildren(e, true);
} else { } else {
e.validateOrThrowAndClose(null); e.validateOrThrow();
} }
return Response.builder().build(); return Response.builder().build();
} }

View file

@ -1,10 +1,10 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange; import io.xpipe.beacon.api.ConnectionTerminalExchange;
import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
@ -18,9 +18,8 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
if (!(e.getStore() instanceof ShellStore shellStore)) { if (!(e.getStore() instanceof ShellStore shellStore)) {
throw new BeaconClientException("Not a shell connection"); throw new BeaconClientException("Not a shell connection");
} }
try (var sc = shellStore.control().start()) { var sc = shellStore.getOrStartSession();
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc); TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
}
return Response.builder().build(); return Response.builder().build();
} }
} }

View file

@ -2,10 +2,10 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer; import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BeaconShellSession; import io.xpipe.app.beacon.BeaconShellSession;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ShellStartExchange; import io.xpipe.beacon.api.ShellStartExchange;
import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows; import lombok.SneakyThrows;
@ -25,7 +25,9 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
var existing = AppBeaconServer.get().getCache().getShellSessions().stream() var existing = AppBeaconServer.get().getCache().getShellSessions().stream()
.filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)) .filter(beaconShellSession -> beaconShellSession.getEntry().equals(e))
.findFirst(); .findFirst();
var control = (existing.isPresent() ? existing.get().getControl() : s.control()); var control = (existing.isPresent()
? existing.get().getControl()
: s.standaloneControl().start());
control.setNonInteractive(); control.setNonInteractive();
control.start(); control.start();

View file

@ -0,0 +1,49 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.core.store.*;
import javafx.beans.property.*;
public final class BrowserHomeModel extends BrowserSessionTab {
public BrowserHomeModel(BrowserAbstractSessionModel<?> browserModel) {
super(browserModel, AppI18n.get("overview"), null);
}
@Override
public Comp<?> comp() {
return new BrowserWelcomeComp((BrowserSessionModel) browserModel);
}
@Override
public boolean canImmediatelyClose() {
return true;
}
@Override
public void init() throws Exception {}
@Override
public void close() {}
@Override
public String getIcon() {
return null;
}
@Override
public DataColor getColor() {
return null;
}
@Override
public boolean isCloseable() {
return false;
}
}

View file

@ -39,7 +39,7 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
} }
private static BrowserSavedStateImpl load() { private static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> { return AppCache.getNonNull("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList()); return new BrowserSavedStateImpl(FXCollections.observableArrayList());
}); });
} }

View file

@ -43,8 +43,11 @@ public class BrowserTransferComp extends SimpleComp {
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true)) .apply(struc -> struc.get().setWrapText(true))
.visible(model.getEmpty()); .visible(model.getEmpty());
var backgroundStack = var backgroundStack = new StackComp(List.of(background))
new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); .grow(true, true)
.styleClass("color-box")
.styleClass("gray")
.styleClass("download-background");
var binding = new DerivedObservableList<>(model.getItems(), true) var binding = new DerivedObservableList<>(model.getItems(), true)
.mapped(item -> item.getBrowserEntry()) .mapped(item -> item.getBrowserEntry())

View file

@ -75,9 +75,8 @@ public interface LeafAction extends BrowserAction {
var name = getName(model, selected); var name = getName(model, selected);
var mi = new MenuItem(); var mi = new MenuItem();
mi.textProperty().bind(BindingsHelper.map(name, s -> { mi.textProperty().bind(BindingsHelper.map(name, s -> {
if (getProFeatureId() != null if (getProFeatureId() != null) {
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { return LicenseProvider.get().getFeature(getProFeatureId()).suffix(s);
return s + " (Pro)";
} }
return s; return s;
})); }));

View file

@ -130,7 +130,7 @@ public final class BrowserFileListComp extends SimpleComp {
table.setAccessibleText("Directory contents"); table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region()); table.setPlaceholder(new Region());
table.getStyleClass().add(Styles.STRIPED); table.getStyleClass().add(Styles.STRIPED);
table.getColumns().setAll(filenameCol, sizeCol, modeCol, ownerCol, mtimeCol); table.getColumns().setAll(filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);
table.getSortOrder().add(filenameCol); table.getSortOrder().add(filenameCol);
table.setFocusTraversable(true); table.setFocusTraversable(true);
table.setSortPolicy(param -> { table.setSortPolicy(param -> {
@ -313,8 +313,10 @@ public final class BrowserFileListComp extends SimpleComp {
.filter(browserAction -> browserAction.getShortcut().match(event)) .filter(browserAction -> browserAction.getShortcut().match(event))
.findAny(); .findAny();
action.ifPresent(browserAction -> { action.ifPresent(browserAction -> {
// Prevent concurrent modification by creating copy on platform thread
var selectionCopy = new ArrayList<>(selected);
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected); browserAction.execute(fileList.getFileSystemModel(), selectionCopy);
}); });
event.consume(); event.consume();
}); });

View file

@ -9,9 +9,10 @@ import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.file.BrowserFileTransferOperation; import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.FileSystemHelper; import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel; import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab; import io.xpipe.app.browser.session.BrowserStoreSessionTab;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
@ -41,7 +42,7 @@ import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
@Getter @Getter
public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore> { public final class OpenFileSystemModel extends BrowserStoreSessionTab<FileSystemStore> {
private final Property<String> filter = new SimpleStringProperty(); private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;

View file

@ -57,9 +57,10 @@ public class OpenFileSystemSavedState {
} }
static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) { static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) {
var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> { var state = AppCache.getNonNull(
return new OpenFileSystemSavedState(); "fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> {
}); return new OpenFileSystemSavedState();
});
state.setModel(model); state.setModel(model);
return state; return state;
} }

View file

@ -13,13 +13,13 @@ import javafx.collections.ObservableList;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> { public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList(); protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
protected final Property<T> selectedEntry = new SimpleObjectProperty<>(); protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
protected final BooleanProperty busy = new SimpleBooleanProperty(); protected final BooleanProperty busy = new SimpleBooleanProperty();
public void closeAsync(BrowserSessionTab<?> e) { public void closeAsync(BrowserSessionTab e) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
closeSync(e); closeSync(e);
}); });
@ -37,7 +37,7 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
} }
} }
public void closeSync(BrowserSessionTab<?> e) { public void closeSync(BrowserSessionTab e) {
e.close(); e.close();
synchronized (BrowserAbstractSessionModel.this) { synchronized (BrowserAbstractSessionModel.this) {
this.sessionEntries.remove(e); this.sessionEntries.remove(e);

View file

@ -6,10 +6,11 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemComp; import io.xpipe.app.browser.fs.OpenFileSystemComp;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.DialogComp; import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
@ -19,7 +20,6 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileReference; import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
@ -148,7 +148,7 @@ public class BrowserChooserComp extends DialogComp {
}); });
var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).styleClass("left"); var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).styleClass("left");
var splitPane = new SideSplitPaneComp(vertical, stack) var splitPane = new LeftSplitPaneComp(vertical, stack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth) .withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.styleClass("background") .styleClass("background")

View file

@ -4,23 +4,25 @@ import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.BrowserBookmarkHeaderComp; import io.xpipe.app.browser.BrowserBookmarkHeaderComp;
import io.xpipe.app.browser.BrowserTransferComp; import io.xpipe.app.browser.BrowserTransferComp;
import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.AnchorComp; import io.xpipe.app.fxcomps.impl.AnchorComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
@ -67,7 +69,11 @@ public class BrowserSessionComp extends SimpleComp {
var bookmarkTopBar = new BrowserBookmarkHeaderComp(); var bookmarkTopBar = new BrowserBookmarkHeaderComp();
var bookmarksList = new BrowserBookmarkComp( var bookmarksList = new BrowserBookmarkComp(
BindingsHelper.map(model.getSelectedEntry(), v -> v.getEntry().get()), BindingsHelper.map(
model.getSelectedEntry(),
v -> v instanceof BrowserStoreSessionTab<?> st
? st.getEntry().get()
: null),
applicable, applicable,
action, action,
bookmarkTopBar.getCategory(), bookmarkTopBar.getCategory(),
@ -99,8 +105,10 @@ public class BrowserSessionComp extends SimpleComp {
var vertical = var vertical =
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left"); new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
var split = new SimpleDoubleProperty(); var leftSplit = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> { var rightSplit = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, leftSplit, rightSplit);
tabs.apply(struc -> {
struc.get().setViewOrder(1); struc.get().setViewOrder(1);
struc.get().setPickOnBounds(false); struc.get().setPickOnBounds(false);
AnchorPane.setTopAnchor(struc.get(), 0.0); AnchorPane.setTopAnchor(struc.get(), 0.0);
@ -108,20 +116,54 @@ public class BrowserSessionComp extends SimpleComp {
AnchorPane.setLeftAnchor(struc.get(), 0.0); AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0); AnchorPane.setRightAnchor(struc.get(), 0.0);
}); });
vertical.apply(struc -> {
struc.get()
.paddingProperty()
.bind(Bindings.createObjectBinding(
() -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight()));
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy()) var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> { .apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 0.0); AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0); AnchorPane.setRightAnchor(struc.get(), 0.0);
}) })
.styleClass("tab-loading-indicator"); .styleClass("tab-loading-indicator");
var loadingStack = new AnchorComp(List.of(tabs, loadingIndicator));
var splitPane = new SideSplitPaneComp(vertical, loadingStack) var pinnedStack = new StackComp(List.of(new LabelComp("a")));
pinnedStack.apply(struc -> {
model.getEffectiveRightTab().subscribe( (newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
if (newValue != null) {
var r = newValue.comp().createRegion();
struc.get().getChildren().add(r);
} else {
struc.get().getChildren().clear();
}
});
});
rightSplit.addListener((observable, oldValue, newValue) -> {
struc.get().setMinWidth(newValue.doubleValue());
struc.get().setMaxWidth(newValue.doubleValue());
struc.get().setPrefWidth(newValue.doubleValue());
});
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
tabs.getHeaderHeight().subscribe(number -> {
AnchorPane.setTopAnchor(struc.get(), number.doubleValue());
});
});
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> { .withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d); AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
split.set(d); leftSplit.set(d);
}) });
.apply(struc -> { splitPane.apply(struc -> {
struc.getLeft().setMinWidth(200); struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500); struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false); struc.get().setPickOnBounds(false);
@ -140,9 +182,7 @@ public class BrowserSessionComp extends SimpleComp {
} }
}); });
}); });
splitPane.styleClass("browser");
var r = splitPane.createRegion(); return splitPane.createRegion();
r.getStyleClass().add("browser");
return r;
} }
} }

View file

@ -1,9 +1,11 @@
package io.xpipe.app.browser.session; package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserHomeModel;
import io.xpipe.app.browser.BrowserSavedState; import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl; import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferModel; import io.xpipe.app.browser.BrowserTransferModel;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
@ -11,22 +13,94 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction; import io.xpipe.core.util.FailableFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableMap;
import lombok.Getter; import lombok.Getter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Getter @Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab<?>> { public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {
public static final BrowserSessionModel DEFAULT = new BrowserSessionModel(); public static final BrowserSessionModel DEFAULT = new BrowserSessionModel();
static {
DEFAULT.getSessionEntries().add(new BrowserHomeModel(DEFAULT));
}
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this); private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty(); private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
private final Property<BrowserSessionTab> globalPinnedTab = new SimpleObjectProperty<>();
private final ObservableValue<BrowserSessionTab> effectiveRightTab = createEffectiveRightTab();
private final ObservableMap<BrowserSessionTab, BrowserSessionTab> splits = FXCollections.observableHashMap();
private ObservableValue<BrowserSessionTab> createEffectiveRightTab() {
return Bindings.createObjectBinding(() -> {
var current = selectedEntry.getValue();
if (!current.isCloseable()) {
return null;
}
var split = splits.get(current);
if (split != null) {
return split;
}
var global = globalPinnedTab.getValue();
if (global == null) {
return null;
}
if (global == selectedEntry.getValue()) {
return null;
}
return global;
}, globalPinnedTab, selectedEntry);
}
public BrowserSessionModel() {
sessionEntries.addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
var v = globalPinnedTab.getValue();
if (v != null && !c.getList().contains(v)) {
globalPinnedTab.setValue(null);
}
splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab));
});
}
public void splitTab(BrowserSessionTab tab, BrowserSessionTab split) {
splits.put(tab, split);
}
public void pinTab(BrowserSessionTab tab) {
if (tab.equals(globalPinnedTab.getValue())) {
return;
}
globalPinnedTab.setValue(tab);
var nextIndex = getSessionEntries().indexOf(tab) + 1;
if (nextIndex < getSessionEntries().size()) {
getSelectedEntry().setValue(getSessionEntries().get(nextIndex));
}
}
public void unpinTab(BrowserSessionTab tab) {
ThreadHelper.runFailableAsync(() -> {
globalPinnedTab.setValue(null);
});
}
public void restoreState(BrowserSavedState state) { public void restoreState(BrowserSavedState state) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
@ -74,14 +148,15 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
} }
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
openFileSystemSync(store, path, externalBusy); openFileSystemSync(store, path, externalBusy, true);
}); });
} }
public OpenFileSystemModel openFileSystemSync( public OpenFileSystemModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store, DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path, FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) BooleanProperty externalBusy,
boolean select)
throws Exception { throws Exception {
OpenFileSystemModel model; OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) { try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
@ -91,8 +166,10 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
// Prevent multiple calls from interfering with each other // Prevent multiple calls from interfering with each other
synchronized (BrowserSessionModel.this) { synchronized (BrowserSessionModel.this) {
sessionEntries.add(model); sessionEntries.add(model);
// The tab pane doesn't automatically select new tabs if (select) {
selectedEntry.setValue(model); // The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
} }
} }
} }

View file

@ -1,29 +1,30 @@
package io.xpipe.app.browser.session; package io.xpipe.app.browser.session;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableValue;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public abstract class BrowserSessionTab<T extends DataStore> { public abstract class BrowserSessionTab {
protected final DataStoreEntryRef<? extends T> entry;
protected final BooleanProperty busy = new SimpleBooleanProperty(); protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel; protected final BrowserAbstractSessionModel<?> browserModel;
protected final String name; protected final String name;
protected final String tooltip; protected final String tooltip;
protected final Property<BrowserSessionTab> splitTab = new SimpleObjectProperty<>();
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) { public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, String name, String tooltip) {
this.browserModel = browserModel; this.browserModel = browserModel;
this.entry = entry; this.name = name;
this.name = DataStorage.get().getStoreEntryDisplayName(entry.get()); this.tooltip = tooltip;
this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString();
} }
public abstract Comp<?> comp(); public abstract Comp<?> comp();
@ -33,4 +34,12 @@ public abstract class BrowserSessionTab<T extends DataStore> {
public abstract void init() throws Exception; public abstract void init() throws Exception;
public abstract void close(); public abstract void close();
public abstract String getIcon();
public abstract DataColor getColor();
public boolean isCloseable() {
return true;
}
} }

View file

@ -1,7 +1,5 @@
package io.xpipe.app.browser.session; package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserWelcomeComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
@ -10,28 +8,33 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper; import io.xpipe.app.util.ContextMenuHelper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableDoubleValue; import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.skin.TabPaneSkin;
import javafx.scene.input.*; import javafx.scene.input.*;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import atlantafx.base.controls.RingProgressIndicator; import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import lombok.Getter;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import static atlantafx.base.theme.Styles.DENSE; import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass; import static atlantafx.base.theme.Styles.toggleStyleClass;
@ -41,26 +44,30 @@ public class BrowserSessionTabsComp extends SimpleComp {
private final BrowserSessionModel model; private final BrowserSessionModel model;
private final ObservableDoubleValue leftPadding; private final ObservableDoubleValue leftPadding;
private final DoubleProperty rightPadding;
public BrowserSessionTabsComp(BrowserSessionModel model, ObservableDoubleValue leftPadding) { @Getter
private final DoubleProperty headerHeight;
public BrowserSessionTabsComp(BrowserSessionModel model, ObservableDoubleValue leftPadding, DoubleProperty rightPadding) {
this.model = model; this.model = model;
this.leftPadding = leftPadding; this.leftPadding = leftPadding;
this.rightPadding = rightPadding;
this.headerHeight = new SimpleDoubleProperty();
} }
public Region createSimple() { public Region createSimple() {
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>(); var tabs = createTabPane();
map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true)); var topBackground = Comp.hspacer().styleClass("top-spacer").createRegion();
map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries())); leftPadding.subscribe(number -> {
map.put( StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue()));
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), });
Bindings.createBooleanBinding( var stack = new StackPane(topBackground, tabs);
() -> { stack.setAlignment(Pos.TOP_CENTER);
return model.getSessionEntries().size() == 0; topBackground.prefHeightProperty().bind(headerHeight);
}, topBackground.maxHeightProperty().bind(topBackground.prefHeightProperty());
model.getSessionEntries())); topBackground.prefWidthProperty().bind(tabs.widthProperty());
var multi = new MultiContentComp(map); return stack;
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
return multi.createRegion();
} }
private TabPane createTabPane() { private TabPane createTabPane() {
@ -69,6 +76,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
tabs.setTabMinWidth(Region.USE_PREF_SIZE); tabs.setTabMinWidth(Region.USE_PREF_SIZE);
tabs.setTabMaxWidth(400); tabs.setTabMaxWidth(400);
tabs.setTabClosingPolicy(ALL_TABS); tabs.setTabClosingPolicy(ALL_TABS);
tabs.setSkin(new TabPaneSkin(tabs));
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE); toggleStyleClass(tabs, DENSE);
@ -80,22 +88,31 @@ public class BrowserSessionTabsComp extends SimpleComp {
tabs.lookupAll(".tab-header-area").forEach(node -> { tabs.lookupAll(".tab-header-area").forEach(node -> {
node.setClip(null); node.setClip(null);
node.setPickOnBounds(false); node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
}); });
tabs.lookupAll(".headers-region").forEach(node -> { tabs.lookupAll(".headers-region").forEach(node -> {
node.setClip(null); node.setClip(null);
node.setPickOnBounds(false); node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
}); });
Region headerArea = (Region) tabs.lookup(".tab-header-area"); Region headerArea = (Region) tabs.lookup(".tab-header-area");
headerArea headerArea
.paddingProperty() .paddingProperty()
.bind(Bindings.createObjectBinding( .bind(Bindings.createObjectBinding(
() -> new Insets(0, 0, 0, -leftPadding.get() + 2), leftPadding)); () -> new Insets(2, 0, 4, -leftPadding.get() + 2), leftPadding));
headerHeight.bind(headerArea.heightProperty());
}); });
} }
}); });
var map = new HashMap<BrowserSessionTab<?>, Tab>(); var map = new HashMap<BrowserSessionTab, Tab>();
// Restore state // Restore state
model.getSessionEntries().forEach(v -> { model.getSessionEntries().forEach(v -> {
@ -156,7 +173,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
}); });
}); });
model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab<?>>) c -> { model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
while (c.next()) { while (c.next()) {
for (var r : c.getRemoved()) { for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
@ -245,9 +262,28 @@ public class BrowserSessionTabsComp extends SimpleComp {
return tabs; return tabs;
} }
private ContextMenu createContextMenu(TabPane tabs, Tab tab) { private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {
var cm = ContextMenuHelper.create(); var cm = ContextMenuHelper.create();
if (tabModel.isCloseable()) {
var unsplit = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("unpinTab"));
unsplit.visibleProperty().bind(PlatformThread.sync(Bindings.createBooleanBinding(() -> {
return model.getGlobalPinnedTab().getValue() != null && model.getGlobalPinnedTab().getValue().equals(tabModel);
}, model.getGlobalPinnedTab())));
unsplit.setOnAction(event -> {
model.unpinTab(tabModel);
event.consume();
});
cm.getItems().add(unsplit);
var split = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("pinTab"));
split.setOnAction(event -> {
model.pinTab(tabModel);
event.consume();
});
cm.getItems().add(split);
}
var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab")); var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab"));
select.acceleratorProperty() select.acceleratorProperty()
.bind(Bindings.createObjectBinding( .bind(Bindings.createObjectBinding(
@ -272,7 +308,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab")); var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab"));
close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN)); close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
close.setOnAction(event -> { close.setOnAction(event -> {
tabs.getTabs().remove(tab); if (tab.isClosable()) {
tabs.getTabs().remove(tab);
}
event.consume(); event.consume();
}); });
cm.getItems().add(close); cm.getItems().add(close);
@ -280,7 +318,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs")); var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs"));
closeOthers.setOnAction(event -> { closeOthers.setOnAction(event -> {
tabs.getTabs() tabs.getTabs()
.removeAll(tabs.getTabs().stream().filter(t -> t != tab).toList()); .removeAll(tabs.getTabs().stream()
.filter(t -> t != tab && t.isClosable())
.toList());
event.consume(); event.consume();
}); });
cm.getItems().add(closeOthers); cm.getItems().add(closeOthers);
@ -290,7 +330,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var index = tabs.getTabs().indexOf(tab); var index = tabs.getTabs().indexOf(tab);
tabs.getTabs() tabs.getTabs()
.removeAll(tabs.getTabs().stream() .removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) < index) .filter(t -> tabs.getTabs().indexOf(t) < index && t.isClosable())
.toList()); .toList());
event.consume(); event.consume();
}); });
@ -301,7 +341,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var index = tabs.getTabs().indexOf(tab); var index = tabs.getTabs().indexOf(tab);
tabs.getTabs() tabs.getTabs()
.removeAll(tabs.getTabs().stream() .removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) > index) .filter(t -> tabs.getTabs().indexOf(t) > index && t.isClosable())
.toList()); .toList());
event.consume(); event.consume();
}); });
@ -311,7 +351,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
closeAll.setAccelerator( closeAll.setAccelerator(
new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
closeAll.setOnAction(event -> { closeAll.setOnAction(event -> {
tabs.getTabs().clear(); tabs.getTabs()
.removeAll(
tabs.getTabs().stream().filter(t -> t.isClosable()).toList());
event.consume(); event.consume();
}); });
cm.getItems().add(closeAll); cm.getItems().add(closeAll);
@ -319,36 +361,92 @@ public class BrowserSessionTabsComp extends SimpleComp {
return cm; return cm;
} }
private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) { private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) {
var tab = new Tab(); var tab = new Tab();
tab.setContextMenu(createContextMenu(tabs, tab)); tab.setContextMenu(createContextMenu(tabs, tab, tabModel));
var ring = new RingProgressIndicator(0, false); tab.setClosable(tabModel.isCloseable());
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get()
&& !AppPrefs.get().performanceMode().get()
? -1d
: 0,
PlatformThread.sync(model.getBusy()),
AppPrefs.get().performanceMode()));
var image = model.getEntry().get().getEffectiveIconFile(); if (tabModel.getIcon() != null) {
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion(); var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16);
ring.setPrefSize(16, 16);
ring.setMaxSize(16, 16);
ring.progressProperty()
.bind(Bindings.createDoubleBinding(
() -> tabModel.getBusy().get()
&& !AppPrefs.get().performanceMode().get()
? -1d
: 0,
PlatformThread.sync(tabModel.getBusy()),
AppPrefs.get().performanceMode()));
tab.graphicProperty() var image = tabModel.getIcon();
.bind(Bindings.createObjectBinding( var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
() -> {
return model.getBusy().get() ? ring : logo;
},
PlatformThread.sync(model.getBusy())));
tab.setText(model.getName());
Comp<?> comp = model.comp(); tab.graphicProperty()
tab.setContent(comp.createRegion()); .bind(Bindings.createObjectBinding(
() -> {
return tabModel.getBusy().get() ? ring : logo;
},
PlatformThread.sync(tabModel.getBusy())));
}
if (tabModel.getBrowserModel() instanceof BrowserSessionModel sessionModel) {
var global = PlatformThread.sync(sessionModel.getGlobalPinnedTab());
tab.textProperty().bind(Bindings.createStringBinding(() -> {
return tabModel.getName() + (global.getValue() == tabModel ? " (" + AppI18n.get("pinned") + ")" : "");
}, global, AppPrefs.get().language()));
} else {
tab.setText(tabModel.getName());
}
Comp<?> comp = tabModel.comp();
var compRegion = comp.createRegion();
var empty = new StackPane();
empty.widthProperty().addListener((observable, oldValue, newValue) -> {
if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) {
rightPadding.setValue(newValue.doubleValue());
}
});
var split = new SplitPane(compRegion);
if (tabModel.isCloseable()) {
split.getItems().add(empty);
}
model.getEffectiveRightTab().subscribe(browserSessionTab -> {
PlatformThread.runLaterIfNeeded(() -> {
if (browserSessionTab != null && split.getItems().size() > 1) {
split.getItems().set(1, empty);
} else if (browserSessionTab != null && split.getItems().size() == 1) {
split.getItems().add(empty);
} else if (browserSessionTab == null && split.getItems().size() > 1) {
split.getItems().remove(1);
}
});
});
tab.setContent(split);
// var lastSplitRegion = new AtomicReference<Region>();
// model.getGlobalPinnedTab().subscribe( (newValue) -> {
// PlatformThread.runLaterIfNeeded(() -> {
// if (newValue != null) {
// var r = newValue.comp().createRegion();
// split.getItems().add(r);
// lastSplitRegion.set(r);
// } else if (split.getItems().size() > 1) {
// split.getItems().removeLast();
// }
// });
// });
// model.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
// PlatformThread.runLaterIfNeeded(() -> {
// if (newValue != null && newValue.equals(model.getGlobalPinnedTab().getValue()) && split.getItems().size() > 1) {
// split.getItems().remove(lastSplitRegion.get());
// } else if (split.getItems().size() > 1 && !split.getItems().contains(lastSplitRegion.get())) {
// split.getItems().add(lastSplitRegion.get());
// }
// });
// });
var id = UUID.randomUUID().toString(); var id = UUID.randomUUID().toString();
tab.setId(id); tab.setId(id);
@ -360,18 +458,20 @@ public class BrowserSessionTabsComp extends SimpleComp {
var w = l.maxWidthProperty(); var w = l.maxWidthProperty();
l.minWidthProperty().bind(w); l.minWidthProperty().bind(w);
l.prefWidthProperty().bind(w); l.prefWidthProperty().bind(w);
if (!tabModel.isCloseable()) {
l.pseudoClassStateChanged(PseudoClass.getPseudoClass("static"), true);
}
var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button"); var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button");
close.setPrefWidth(30); close.setPrefWidth(30);
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box"); c.getStyleClass().add("color-box");
var color = var color = tabModel.getColor();
DataStorage.get().getEffectiveColor(model.getEntry().get());
if (color != null) { if (color != null) {
c.getStyleClass().add(color.getId()); c.getStyleClass().add(color.getId());
} }
new TooltipAugment<>(new SimpleStringProperty(model.getTooltip()), null).augment(c); new TooltipAugment<>(new SimpleStringProperty(tabModel.getTooltip()), null).augment(c);
c.addEventHandler( c.addEventHandler(
DragEvent.DRAG_ENTERED, DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater( mouseEvent -> Platform.runLater(

View file

@ -0,0 +1,41 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import lombok.Getter;
@Getter
public abstract class BrowserStoreSessionTab<T extends DataStore> extends BrowserSessionTab {
protected final DataStoreEntryRef<? extends T> entry;
public BrowserStoreSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
super(
browserModel,
DataStorage.get().getStoreEntryDisplayName(entry.get()),
DataStorage.get().getStorePath(entry.getEntry()).toString());
this.entry = entry;
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
@Override
public String getIcon() {
return entry.get().getEffectiveIconFile();
}
@Override
public DataColor getColor() {
return DataStorage.get().getEffectiveColor(entry.get());
}
}

View file

@ -68,7 +68,14 @@ public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structur
return new TextAreaStructure(c, textArea.getTextArea()); return new TextAreaStructure(c, textArea.getTextArea());
} }
}, },
paths -> value.setValue(Files.readString(paths.getFirst()))); paths -> {
var first = paths.getFirst();
if (Files.size(first) > 1_000_000) {
return;
}
value.setValue(Files.readString(first));
});
var struc = fileDrop.createStructure(); var struc = fileDrop.createStructure();
return new Structure(struc.get(), struc.getCompStructure().getTextArea()); return new Structure(struc.get(), struc.getCompStructure().getTextArea());
} }

View file

@ -11,14 +11,14 @@ import lombok.Value;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer; import java.util.function.Consumer;
public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> { public class LeftSplitPaneComp extends Comp<LeftSplitPaneComp.Structure> {
private final Comp<?> left; private final Comp<?> left;
private final Comp<?> center; private final Comp<?> center;
private Double initialWidth; private Double initialWidth;
private Consumer<Double> onDividerChange; private Consumer<Double> onDividerChange;
public SideSplitPaneComp(Comp<?> left, Comp<?> center) { public LeftSplitPaneComp(Comp<?> left, Comp<?> center) {
this.left = left; this.left = left;
this.center = center; this.center = center;
} }
@ -58,12 +58,12 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
return new Structure(sidebar, c, r, r.getDividers().getFirst()); return new Structure(sidebar, c, r, r.getDividers().getFirst());
} }
public SideSplitPaneComp withInitialWidth(double val) { public LeftSplitPaneComp withInitialWidth(double val) {
this.initialWidth = val; this.initialWidth = val;
return this; return this;
} }
public SideSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) { public LeftSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) {
this.onDividerChange = onDividerChange; this.onDividerChange = onDividerChange;
return this; return this;
} }

View file

@ -1,6 +1,5 @@
package io.xpipe.app.comp.base; package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -9,17 +8,13 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -27,9 +22,6 @@ import javafx.scene.control.Button;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List; import java.util.List;
public class SideMenuBarComp extends Comp<CompStructure<VBox>> { public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
@ -50,14 +42,14 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var selectedBorder = Bindings.createObjectBinding( var selectedBorder = Bindings.createObjectBinding(
() -> { () -> {
var c = Platform.getPreferences().getAccentColor().desaturate(); var c = Platform.getPreferences().getAccentColor().desaturate();
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2))); return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(12, 1, 12, 2)));
}, },
Platform.getPreferences().accentColorProperty()); Platform.getPreferences().accentColorProperty());
var hoverBorder = Bindings.createObjectBinding( var hoverBorder = Bindings.createObjectBinding(
() -> { () -> {
var c = Platform.getPreferences().getAccentColor().darker().desaturate(); var c = Platform.getPreferences().getAccentColor().darker().desaturate();
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2))); return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(12, 1, 12, 2)));
}, },
Platform.getPreferences().accentColorProperty()); Platform.getPreferences().accentColorProperty());
@ -141,29 +133,6 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
vbox.getChildren().add(b.createRegion()); vbox.getChildren().add(b.createRegion());
} }
{
var zone = ZoneId.of(ZoneId.SHORT_IDS.get("PST"));
var now = Instant.now();
var phStart = ZonedDateTime.of(2024, 10, 22, 0, 1, 0, 0, zone).toInstant();
var phEnd = ZonedDateTime.of(2024, 10, 23, 0, 1, 0, 0, zone).toInstant();
var clicked = AppCache.get("phClicked", Boolean.class, () -> false);
var phShow = now.isAfter(phStart) && now.isBefore(phEnd) && !clicked;
if (phShow) {
var hide = new SimpleBooleanProperty(false);
var b = new IconButtonComp(new LabelGraphic.ImageGraphic("app:producthunt-color.png", 24), () -> {
AppCache.update("phClicked", true);
Hyperlinks.open(Hyperlinks.PRODUCT_HUNT);
hide.set(true);
})
.tooltip(new SimpleStringProperty("Product Hunt"));
b.apply(struc -> {
AppFont.setSize(struc.get(), 1);
});
b.hide(hide);
vbox.getChildren().add(b.createRegion());
}
}
var filler = new Button(); var filler = new Button();
filler.setDisable(true); filler.setDisable(true);
filler.setMaxHeight(3000); filler.setMaxHeight(3000);

View file

@ -95,7 +95,16 @@ public class DenseStoreEntryComp extends StoreEntryComp {
nameCC.setMinWidth(100); nameCC.setMinWidth(100);
nameCC.setHgrow(Priority.ALWAYS); nameCC.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(nameCC); grid.getColumnConstraints().addAll(nameCC);
var active = new StoreActiveComp(getWrapper()).createRegion();
var nameBox = new HBox(name, notes); var nameBox = new HBox(name, notes);
getWrapper().getSessionActive().subscribe(aBoolean -> {
if (!aBoolean) {
nameBox.getChildren().remove(active);
} else {
nameBox.getChildren().add(1, active);
}
});
nameBox.setSpacing(6); nameBox.setSpacing(6);
nameBox.setAlignment(Pos.CENTER_LEFT); nameBox.setAlignment(Pos.CENTER_LEFT);
grid.addRow(0, nameBox); grid.addRow(0, nameBox);

View file

@ -41,11 +41,19 @@ public class StandardStoreEntryComp extends StoreEntryComp {
grid.add(storeIcon, 0, 0, 1, 2); grid.add(storeIcon, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(56)); grid.getColumnConstraints().add(new ColumnConstraints(56));
var nameAndNotes = new HBox(name, notes); var active = new StoreActiveComp(getWrapper()).createRegion();
nameAndNotes.setSpacing(6); var nameBox = new HBox(name, notes);
nameAndNotes.setAlignment(Pos.CENTER_LEFT); nameBox.setSpacing(6);
grid.add(nameAndNotes, 1, 0); nameBox.setAlignment(Pos.CENTER_LEFT);
GridPane.setVgrow(nameAndNotes, Priority.ALWAYS); grid.add(nameBox, 1, 0);
GridPane.setVgrow(nameBox, Priority.ALWAYS);
getWrapper().getSessionActive().subscribe(aBoolean -> {
if (!aBoolean) {
nameBox.getChildren().remove(active);
} else {
nameBox.getChildren().add(1, active);
}
});
var summaryBox = new HBox(createSummary()); var summaryBox = new HBox(createSummary());
summaryBox.setAlignment(Pos.TOP_LEFT); summaryBox.setAlignment(Pos.TOP_LEFT);

View file

@ -0,0 +1,38 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import javafx.geometry.Pos;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
public class StoreActiveComp extends SimpleComp {
private final StoreEntryWrapper wrapper;
public StoreActiveComp(StoreEntryWrapper wrapper) {
this.wrapper = wrapper;
}
@Override
protected Region createSimple() {
var c = new Circle(6);
c.getStyleClass().add("dot");
c.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
wrapper.stopSession();
event.consume();
}
});
var pane = new StackPane(c);
pane.setAlignment(Pos.CENTER);
pane.visibleProperty().bind(wrapper.getSessionActive());
pane.getStyleClass().add("store-active-comp");
new TooltipAugment<>("sessionActive", null).augment(pane);
return pane;
}
}

View file

@ -20,7 +20,7 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ValidationContext; import io.xpipe.core.store.ValidatableStore;
import io.xpipe.core.util.ValidationException; import io.xpipe.core.util.ValidationException;
import javafx.application.Platform; import javafx.application.Platform;
@ -157,6 +157,17 @@ public class StoreCreationComp extends DialogComp {
}, },
name, name,
store); store);
skippable.bind(Bindings.createBooleanBinding(
() -> {
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
return true;
} else {
return false;
}
},
store,
name));
} }
public static void showEdit(DataStoreEntry e) { public static void showEdit(DataStoreEntry e) {
@ -165,11 +176,8 @@ public class StoreCreationComp extends DialogComp {
e.getProvider(), e.getProvider(),
e.getStore(), e.getStore(),
v -> true, v -> true,
(newE, context, validated) -> { (newE, validated) -> {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
if (context != null) {
context.close();
}
if (!DataStorage.get().getStoreEntries().contains(e)) { if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE); DataStorage.get().addStoreEntryIfNotPresent(newE);
} else { } else {
@ -191,21 +199,22 @@ public class StoreCreationComp extends DialogComp {
} }
public static void showCreation(DataStore base, DataStoreCreationCategory category) { public static void showCreation(DataStore base, DataStoreCreationCategory category) {
var prov = base != null ? DataStoreProviders.byStore(base) : null;
show( show(
null, null,
base != null ? DataStoreProviders.byStore(base) : null, prov,
base, base,
dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()), dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
(e, context, validated) -> { || dataStoreProvider.equals(prov),
(e, validated) -> {
try { try {
DataStorage.get().addStoreEntryIfNotPresent(e); DataStorage.get().addStoreEntryIfNotPresent(e);
if (context != null if (validated
&& validated
&& e.getProvider().shouldShowScan() && e.getProvider().shouldShowScan()
&& AppPrefs.get() && AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation() .openConnectionSearchWindowOnConnectionCreation()
.get()) { .get()) {
ScanAlert.showAsync(e, context); ScanAlert.showAsync(e);
} }
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle(); ErrorEvent.fromThrowable(ex).handle();
@ -217,7 +226,7 @@ public class StoreCreationComp extends DialogComp {
public interface CreationConsumer { public interface CreationConsumer {
void consume(DataStoreEntry entry, ValidationContext<?> validationContext, boolean validated); void consume(DataStoreEntry entry, boolean validated);
} }
private static void show( private static void show(
@ -254,9 +263,9 @@ public class StoreCreationComp extends DialogComp {
@Override @Override
protected List<Comp<?>> customButtons() { protected List<Comp<?>> customButtons() {
return List.of( return List.of(
new ButtonComp(AppI18n.observable("skip"), null, () -> { new ButtonComp(AppI18n.observable("skipValidation"), null, () -> {
if (showInvalidConfirmAlert()) { if (showInvalidConfirmAlert()) {
commit(null, false); commit(false);
} else { } else {
finish(); finish();
} }
@ -299,7 +308,7 @@ public class StoreCreationComp extends DialogComp {
// We didn't change anything // We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) { if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
commit(null, false); commit(false);
return; return;
} }
@ -329,18 +338,14 @@ public class StoreCreationComp extends DialogComp {
try (var ignored = new BooleanScope(busy).start()) { try (var ignored = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue()); DataStorage.get().addStoreEntryInProgress(entry.getValue());
var context = entry.getValue().validateAndKeepOpenOrThrowAndClose(null); entry.getValue().validateOrThrow();
commit(context, true); commit(true);
} catch (Throwable ex) { } catch (Throwable ex) {
if (ex instanceof ValidationException) { if (ex instanceof ValidationException) {
ErrorEvent.expected(ex); ErrorEvent.expected(ex);
skippable.set(false);
} else if (ex instanceof StackOverflowError) { } else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected // Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex); ErrorEvent.expected(ex);
skippable.set(false);
} else {
skippable.set(true);
} }
var newMessage = ExceptionConverter.convertMessage(ex); var newMessage = ExceptionConverter.convertMessage(ex);
@ -415,14 +420,14 @@ public class StoreCreationComp extends DialogComp {
.createRegion(); .createRegion();
} }
private void commit(ValidationContext<?> validationContext, boolean validated) { private void commit(boolean validated) {
if (finished.get()) { if (finished.get()) {
return; return;
} }
finished.setValue(true); finished.setValue(true);
if (entry.getValue() != null) { if (entry.getValue() != null) {
consumer.consume(entry.getValue(), validationContext, validated); consumer.consume(entry.getValue(), validated);
} }
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
@ -433,7 +438,7 @@ public class StoreCreationComp extends DialogComp {
private Region createLayout() { private Region createLayout() {
var layout = new BorderPane(); var layout = new BorderPane();
layout.getStyleClass().add("store-creator"); layout.getStyleClass().add("store-creator");
var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay); var providerChoice = new StoreProviderChoiceComp(filter, provider);
var showProviders = (!staticDisplay var showProviders = (!staticDisplay
&& (providerChoice.getProviders().size() > 1 && (providerChoice.getProviders().size() > 1
|| providerChoice.getProviders().getFirst().showProviderChoice())) || providerChoice.getProviders().getFirst().showProviderChoice()))

View file

@ -22,7 +22,7 @@ public class StoreCreationMenu {
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline")); automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
automatically.textProperty().bind(AppI18n.observable("addAutomatically")); automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
automatically.setOnAction(event -> { automatically.setOnAction(event -> {
ScanAlert.showAsync(null, null); ScanAlert.showAsync(null);
event.consume(); event.consume();
}); });
menu.getItems().add(automatically); menu.getItems().add(automatically);
@ -32,13 +32,11 @@ public class StoreCreationMenu {
menu.getItems().add(category("addDesktop", "mdi2c-camera-plus", DataStoreCreationCategory.DESKTOP, null)); menu.getItems().add(category("addDesktop", "mdi2c-camera-plus", DataStoreCreationCategory.DESKTOP, null));
menu.getItems()
.add(category(
"addShell", "mdi2t-text-box-multiple", DataStoreCreationCategory.SHELL, "shellEnvironment"));
menu.getItems() menu.getItems()
.add(category("addScript", "mdi2s-script-text-outline", DataStoreCreationCategory.SCRIPT, "script")); .add(category("addScript", "mdi2s-script-text-outline", DataStoreCreationCategory.SCRIPT, "script"));
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null));
menu.getItems() menu.getItems()
.add(category( .add(category(
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, "customService")); "addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, "customService"));

View file

@ -439,7 +439,8 @@ public abstract class StoreEntryComp extends SimpleComp {
&& !LicenseProvider.get().getFeature(p.getProFeatureId()).isSupported(); && !LicenseProvider.get().getFeature(p.getProFeatureId()).isSupported();
if (proRequired) { if (proRequired) {
item.setDisable(true); item.setDisable(true);
item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name)); item.textProperty()
.bind(LicenseProvider.get().getFeature(p.getProFeatureId()).suffixObservable(name.getValue()));
} else { } else {
item.textProperty().bind(name); item.textProperty().bind(name);
} }

View file

@ -2,11 +2,13 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -34,18 +36,23 @@ public class StoreEntryListComp extends SimpleComp {
StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> { StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {
struc.get().setVvalue(0); struc.get().setVvalue(0);
}); });
});
content.apply(struc -> {
// Reset scroll // Reset scroll
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
struc.get().setVvalue(0); struc.get().setVvalue(0);
}); });
// Reset scroll
StoreViewState.get().getFilterString().addListener((observable, oldValue, newValue) -> {
struc.get().setVvalue(0);
});
}); });
return content.styleClass("store-list-comp"); return content.styleClass("store-list-comp");
} }
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var scriptsIntroShowing = new SimpleBooleanProperty(!AppCache.getBoolean("scriptsIntroCompleted", false));
var initialCount = 1; var initialCount = 1;
var showIntro = Bindings.createBooleanBinding( var showIntro = Bindings.createBooleanBinding(
() -> { () -> {
@ -63,6 +70,46 @@ public class StoreEntryListComp extends SimpleComp {
}, },
StoreViewState.get().getAllEntries().getList(), StoreViewState.get().getAllEntries().getList(),
StoreViewState.get().getActiveCategory()); StoreViewState.get().getActiveCategory());
var showScriptsIntro = Bindings.createBooleanBinding(
() -> {
if (StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllScriptsCategory())) {
return scriptsIntroShowing.get();
}
return false;
},
scriptsIntroShowing,
StoreViewState.get().getActiveCategory());
var showList = Bindings.createBooleanBinding(
() -> {
if (StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllScriptsCategory())) {
return !scriptsIntroShowing.get();
}
if (StoreViewState.get()
.getCurrentTopLevelSection()
.getShownChildren()
.getList()
.isEmpty()) {
return false;
}
return true;
},
StoreViewState.get().getActiveCategory(),
scriptsIntroShowing,
StoreViewState.get()
.getCurrentTopLevelSection()
.getShownChildren()
.getList());
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>(); var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put( map.put(
new StoreNotFoundComp(), new StoreNotFoundComp(),
@ -73,13 +120,9 @@ public class StoreEntryListComp extends SimpleComp {
.getCurrentTopLevelSection() .getCurrentTopLevelSection()
.getShownChildren() .getShownChildren()
.getList()))); .getList())));
map.put( map.put(createList(), showList);
createList(),
Bindings.not(Bindings.isEmpty(StoreViewState.get()
.getCurrentTopLevelSection()
.getShownChildren()
.getList())));
map.put(new StoreIntroComp(), showIntro); map.put(new StoreIntroComp(), showIntro);
map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);
return new MultiContentComp(map).createRegion(); return new MultiContentComp(map).createRegion();
} }

View file

@ -83,13 +83,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
return inRootCategory && showProvider; return inRootCategory && showProvider;
}, },
StoreViewState.get().getActiveCategory()); StoreViewState.get().getActiveCategory());
var shownList = all.filtered( var count = new CountComp<>(all.getList(), all.getList());
storeEntryWrapper -> {
return storeEntryWrapper.matchesFilter(
StoreViewState.get().getFilterString().getValue());
},
StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList.getList(), all.getList());
var c = count.createRegion(); var c = count.createRegion();
var topBar = new HBox( var topBar = new HBox(

View file

@ -9,6 +9,7 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.SingletonSessionStore;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -44,6 +45,7 @@ public class StoreEntryWrapper {
private final Property<StoreNotes> notes; private final Property<StoreNotes> notes;
private final Property<String> customIcon = new SimpleObjectProperty<>(); private final Property<String> customIcon = new SimpleObjectProperty<>();
private final Property<String> iconFile = new SimpleObjectProperty<>(); private final Property<String> iconFile = new SimpleObjectProperty<>();
private final BooleanProperty sessionActive = new SimpleBooleanProperty();
public StoreEntryWrapper(DataStoreEntry entry) { public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry; this.entry = entry;
@ -118,7 +120,15 @@ public class StoreEntryWrapper {
}); });
} }
public void update() { public void stopSession() {
ThreadHelper.runFailableAsync(() -> {
if (entry.getStore() instanceof SingletonSessionStore<?> singletonSessionStore) {
singletonSessionStore.stopSessionIfNeeded();
}
});
}
public synchronized void update() {
// We are probably in shutdown then // We are probably in shutdown then
if (StoreViewState.get() == null) { if (StoreViewState.get() == null) {
return; return;
@ -147,6 +157,7 @@ public class StoreEntryWrapper {
busy.setValue(entry.getBusyCounter().get() != 0); busy.setValue(entry.getBusyCounter().get() != 0);
deletable.setValue(entry.getConfiguration().isDeletable() deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue()); || AppPrefs.get().developerDisableGuiRestrictions().getValue());
sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore<?> ss && ss.isSessionRunning());
category.setValue(StoreViewState.get() category.setValue(StoreViewState.get()
.getCategoryWrapper(DataStorage.get() .getCategoryWrapper(DataStorage.get()
@ -220,7 +231,7 @@ public class StoreEntryWrapper {
} }
public void refreshChildren() { public void refreshChildren() {
var hasChildren = DataStorage.get().refreshChildren(entry, null); var hasChildren = DataStorage.get().refreshChildren(entry);
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
expanded.set(hasChildren); expanded.set(hasChildren);
}); });

View file

@ -39,7 +39,7 @@ public class StoreIntroComp extends SimpleComp {
var scanButton = new Button(null, new FontIcon("mdi2m-magnify")); var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
scanButton.textProperty().bind(AppI18n.observable("detectConnections")); scanButton.textProperty().bind(AppI18n.observable("detectConnections"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local(), null)); scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
scanButton.setDefaultButton(true); scanButton.setDefaultButton(true);
var scanPane = new StackPane(scanButton); var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER); scanPane.setAlignment(Pos.CENTER);

View file

@ -1,6 +1,6 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
@ -15,7 +15,7 @@ public class StoreLayoutComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()) var struc = new LeftSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp())
.withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth()) .withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth())
.withOnDividerChange(aDouble -> { .withOnDividerChange(aDouble -> {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble); AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);

View file

@ -27,7 +27,6 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
Predicate<DataStoreProvider> filter; Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider; Property<DataStoreProvider> provider;
boolean staticDisplay;
public List<DataStoreProvider> getProviders() { public List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream() return DataStoreProviders.getAll().stream()
@ -65,9 +64,7 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
return cellFactory.get(); return cellFactory.get();
}); });
cb.setButtonCell(cellFactory.get()); cb.setButtonCell(cellFactory.get());
var l = getProviders().stream() var l = getProviders();
.filter(p -> p.getCreationCategory() != null || staticDisplay)
.toList();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider)); l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) { if (provider.getValue() == null) {
provider.setValue(l.getFirst()); provider.setValue(l.getFirst());

View file

@ -0,0 +1,126 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.core.process.OsType;
import javafx.beans.property.BooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreScriptsIntroComp extends SimpleComp {
private final BooleanProperty show;
public StoreScriptsIntroComp(BooleanProperty show) {
this.show = show;
}
private Region createIntro() {
var title = new Label();
title.textProperty().bind(AppI18n.observable("scriptsIntroTitle"));
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable("scriptsIntroText"));
introDesc.setWrapText(true);
introDesc.setMaxWidth(470);
var img = new FontIcon("mdi2s-script-text");
img.setIconSize(80);
var text = new VBox(title, introDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(55);
hbox.setAlignment(Pos.CENTER);
var v = new VBox(hbox);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(10);
v.getStyleClass().add("intro");
return v;
}
private Region createBottom() {
var title = new Label();
title.textProperty().bind(AppI18n.observable("scriptsIntroBottomTitle"));
if (OsType.getLocal() != OsType.MACOS) {
title.getStyleClass().add(Styles.TEXT_BOLD);
}
AppFont.setSize(title, 7);
var importDesc = new Label();
importDesc.textProperty().bind(AppI18n.observable("scriptsIntroBottomText"));
importDesc.setWrapText(true);
importDesc.setMaxWidth(470);
var importButton = new Button(null, new FontIcon("mdi2p-play-circle"));
importButton.setDefaultButton(true);
importButton.textProperty().bind(AppI18n.observable("scriptsIntroStart"));
importButton.setOnAction(event -> {
AppCache.update("scriptsIntroCompleted", true);
show.set(false);
});
var importPane = new StackPane(importButton);
importPane.setAlignment(Pos.CENTER);
var fi = new FontIcon("mdi2t-tooltip-edit");
fi.setIconSize(80);
var img = new StackPane(fi);
img.setPrefWidth(100);
img.setPrefHeight(150);
var text = new VBox(title, importDesc);
text.setSpacing(5);
text.setAlignment(Pos.CENTER_LEFT);
var hbox = new HBox(img, text);
hbox.setSpacing(35);
hbox.setAlignment(Pos.CENTER);
var v = new VBox(hbox, importPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
v.setSpacing(20);
v.getStyleClass().add("intro");
return v;
}
@Override
public Region createSimple() {
var intro = createIntro();
var introImport = createBottom();
var v = new VBox(intro, introImport);
v.setSpacing(80);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE);
v.setMaxHeight(Region.USE_PREF_SIZE);
var sp = new StackPane(v);
sp.setPadding(new Insets(40, 0, 0, 0));
sp.setAlignment(Pos.CENTER);
sp.setPickOnBounds(false);
return sp;
}
}

View file

@ -31,6 +31,7 @@ public class StoreSidebarComp extends SimpleComp {
.styleClass("gray") .styleClass("gray")
.styleClass("bar") .styleClass("bar")
.styleClass("filler-bar") .styleClass("filler-bar")
.minHeight(10)
.vgrow())); .vgrow()));
sideBar.apply(struc -> struc.get().setFillWidth(true)); sideBar.apply(struc -> struc.get().setFillWidth(true));
sideBar.styleClass("sidebar"); sideBar.styleClass("sidebar");

View file

@ -126,7 +126,7 @@ public class StoreViewState {
activeCategory.addListener((observable, oldValue, newValue) -> { activeCategory.addListener((observable, oldValue, newValue) -> {
DataStorage.get().setSelectedCategory(newValue.getCategory()); DataStorage.get().setSelectedCategory(newValue.getCategory());
}); });
var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID); var selected = AppCache.getNonNull("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
activeCategory.setValue(categories.getList().stream() activeCategory.setValue(categories.getList().stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(selected)) storeCategoryWrapper.getCategory().getUuid().equals(selected))

View file

@ -4,23 +4,20 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.JsonConfigHelper; import io.xpipe.app.util.JsonConfigHelper;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
public class AppCache { public class AppCache {
public static <T> Optional<T> getIfPresent(String key, Class<T> type) { @Getter
return Optional.ofNullable(get(key, type, () -> null)); @Setter
} private static Path basePath;
private static Path getBasePath() {
return AppProperties.get().getDataDir().resolve("cache");
}
private static Path getPath(String key) { private static Path getPath(String key) {
var name = key + ".cache"; var name = key + ".cache";
@ -47,7 +44,33 @@ public class AppCache {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <T> T get(String key, Class<?> type, Supplier<T> notPresent) { public static <T> T getNonNull(String key, Class<?> type, Supplier<T> notPresent) {
var path = getPath(key);
if (Files.exists(path)) {
try {
var tree = JsonConfigHelper.readRaw(path);
if (tree.isMissingNode() || tree.isNull()) {
FileUtils.deleteQuietly(path.toFile());
return notPresent.get();
}
var r = (T) JacksonMapper.getDefault().treeToValue(tree, type);
if (r == null || !type.isAssignableFrom(r.getClass())) {
FileUtils.deleteQuietly(path.toFile());
return notPresent.get();
} else {
return r;
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
FileUtils.deleteQuietly(path.toFile());
}
}
return notPresent != null ? notPresent.get() : null;
}
@SuppressWarnings("unchecked")
public static <T> T getNullable(String key, Class<?> type, Supplier<T> notPresent) {
var path = getPath(key); var path = getPath(key);
if (Files.exists(path)) { if (Files.exists(path)) {
try { try {
@ -65,6 +88,25 @@ public class AppCache {
return notPresent != null ? notPresent.get() : null; return notPresent != null ? notPresent.get() : null;
} }
public static boolean getBoolean(String key, boolean notPresent) {
var path = getPath(key);
if (Files.exists(path)) {
try {
var tree = JsonConfigHelper.readRaw(path);
if (!tree.isBoolean()) {
FileUtils.deleteQuietly(path.toFile());
return notPresent;
}
return tree.asBoolean();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
FileUtils.deleteQuietly(path.toFile());
}
}
return notPresent;
}
public static <T> void update(String key, T val) { public static <T> void update(String key, T val) {
var path = getPath(key); var path = getPath(key);
@ -79,12 +121,4 @@ public class AppCache {
.handle(); .handle();
} }
} }
public <T> T getValue(String key, Class<?> type, Supplier<T> notPresent) {
return get(key, type, notPresent);
}
public <T> void updateValue(String key, T val) {
update(key, val);
}
} }

View file

@ -140,7 +140,7 @@ public class AppExtensionManager {
} }
private void loadAllExtensions() { private void loadAllExtensions() {
for (var ext : List.of("jdbc", "proc", "uacc")) { for (var ext : List.of("proc", "uacc")) {
var extension = findAndParseExtension(ext, baseLayer) var extension = findAndParseExtension(ext, baseLayer)
.orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext)); .orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext));
loadedExtensions.add(extension); loadedExtensions.add(extension);

View file

@ -52,7 +52,7 @@ public class AppGreetings {
} }
public static void showIfNeeded() { public static void showIfNeeded() {
boolean set = AppCache.get("legalAccepted", Boolean.class, () -> false); boolean set = AppCache.getBoolean("legalAccepted", false);
if (set || AppProperties.get().isDevelopmentEnvironment()) { if (set || AppProperties.get().isDevelopmentEnvironment()) {
return; return;
} }

View file

@ -48,7 +48,7 @@ public class AppLayoutModel {
} }
public static void init() { public static void init() {
var state = AppCache.get("layoutState", SavedState.class, () -> new SavedState(260, 300)); var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(260, 300));
INSTANCE = new AppLayoutModel(state); INSTANCE = new AppLayoutModel(state);
} }

View file

@ -44,6 +44,8 @@ public class AppProperties {
boolean locatorVersionCheck; boolean locatorVersionCheck;
boolean isTest; boolean isTest;
boolean autoAcceptEula; boolean autoAcceptEula;
UUID uuid;
boolean initialLaunch;
public AppProperties() { public AppProperties() {
var appDir = Path.of(System.getProperty("user.dir")).resolve("app"); var appDir = Path.of(System.getProperty("user.dir")).resolve("app");
@ -113,6 +115,15 @@ public class AppProperties {
autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula")) autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula"))
.map(Boolean::parseBoolean) .map(Boolean::parseBoolean)
.orElse(false); .orElse(false);
AppCache.setBasePath(dataDir.resolve("cache"));
UUID id = AppCache.getNonNull("uuid", UUID.class, null);
if (id == null) {
uuid = UUID.randomUUID();
AppCache.update("uuid", uuid);
} else {
uuid = id;
}
initialLaunch = AppCache.getNonNull("lastBuild", String.class, () -> null) == null;
} }
private static boolean isJUnitTest() { private static boolean isJUnitTest() {

View file

@ -1,48 +0,0 @@
package io.xpipe.app.core;
import lombok.Setter;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.util.UUID;
@Value
public class AppState {
private static AppState INSTANCE;
UUID userId;
boolean initialLaunch;
@NonFinal
@Setter
String userName;
@NonFinal
@Setter
String userEmail;
public AppState() {
UUID id = AppCache.get("userId", UUID.class, null);
if (id == null) {
initialLaunch = AppCache.getIfPresent("lastBuild", String.class).isEmpty();
userId = UUID.randomUUID();
AppCache.update("userId", userId);
} else {
userId = id;
initialLaunch = false;
}
}
public static void init() {
if (INSTANCE != null) {
return;
}
INSTANCE = new AppState();
}
public static AppState get() {
return INSTANCE;
}
}

View file

@ -18,6 +18,7 @@ import java.util.*;
public class AppStyle { public class AppStyle {
private static final Map<Path, String> STYLESHEET_CONTENTS = new LinkedHashMap<>(); private static final Map<Path, String> STYLESHEET_CONTENTS = new LinkedHashMap<>();
private static final Map<AppTheme.Theme, String> THEME_SPECIFIC_STYLESHEET_CONTENTS = new LinkedHashMap<>();
private static final List<Scene> scenes = new ArrayList<>(); private static final List<Scene> scenes = new ArrayList<>();
private static String FONT_CONTENTS = ""; private static String FONT_CONTENTS = "";
@ -33,6 +34,9 @@ public class AppStyle {
AppPrefs.get().useSystemFont().addListener((c, o, n) -> { AppPrefs.get().useSystemFont().addListener((c, o, n) -> {
changeFontUsage(n); changeFontUsage(n);
}); });
AppPrefs.get().theme.addListener((c, o, n) -> {
changeTheme(n);
});
} }
} }
@ -73,6 +77,19 @@ public class AppStyle {
}); });
}); });
} }
AppResources.with(AppResources.XPIPE_MODULE, "theme", path -> {
if (!Files.exists(path)) {
return;
}
for (AppTheme.Theme theme : AppTheme.Theme.ALL) {
var file = path.resolve(theme.getId() + ".css");
var bytes = Files.readAllBytes(file);
var s = "data:text/css;base64," + Base64.getEncoder().encodeToString(bytes);
THEME_SPECIFIC_STYLESHEET_CONTENTS.put(theme, s);
}
});
} }
private static void changeFontUsage(boolean use) { private static void changeFontUsage(boolean use) {
@ -87,8 +104,16 @@ public class AppStyle {
} }
} }
private static void changeTheme(AppTheme.Theme theme) {
scenes.forEach(scene -> {
scene.getStylesheets().removeAll(THEME_SPECIFIC_STYLESHEET_CONTENTS.values());
scene.getStylesheets().add(THEME_SPECIFIC_STYLESHEET_CONTENTS.get(theme));
});
}
public static void reloadStylesheets(Scene scene) { public static void reloadStylesheets(Scene scene) {
STYLESHEET_CONTENTS.clear(); STYLESHEET_CONTENTS.clear();
THEME_SPECIFIC_STYLESHEET_CONTENTS.clear();
FONT_CONTENTS = ""; FONT_CONTENTS = "";
init(); init();
@ -107,7 +132,7 @@ public class AppStyle {
if (AppPrefs.get() != null) { if (AppPrefs.get() != null) {
var t = AppPrefs.get().theme.get(); var t = AppPrefs.get().theme.get();
if (t != null) { if (t != null) {
scene.getStylesheets().addAll(t.getAdditionalStylesheets()); scene.getStylesheets().add(THEME_SPECIFIC_STYLESHEET_CONTENTS.get(t));
} }
} }
TrackEvent.debug("Added stylesheets for scene"); TrackEvent.debug("Added stylesheets for scene");

View file

@ -97,7 +97,10 @@ public class AppTheme {
} }
try { try {
if (AppPrefs.get().theme.getValue() == null) { var lastSystemDark = AppCache.getBoolean("lastTheme", false);
var nowDark = Platform.getPreferences().getColorScheme() == ColorScheme.DARK;
AppCache.update("lastTheme", nowDark);
if (AppPrefs.get().theme.getValue() == null || lastSystemDark != nowDark) {
setDefault(); setDefault();
} }
@ -237,7 +240,7 @@ public class AppTheme {
public static final Theme CUPERTINO_LIGHT = new Theme("cupertinoLight", "cupertino", new CupertinoLight()); public static final Theme CUPERTINO_LIGHT = new Theme("cupertinoLight", "cupertino", new CupertinoLight());
public static final Theme CUPERTINO_DARK = new Theme("cupertinoDark", "cupertino", new CupertinoDark()); public static final Theme CUPERTINO_DARK = new Theme("cupertinoDark", "cupertino", new CupertinoDark());
public static final Theme DRACULA = new Theme("dracula", "dracula", new Dracula()); public static final Theme DRACULA = new Theme("dracula", "dracula", new Dracula());
public static final Theme MOCHA = new DerivedTheme("mocha", "primer", "Mocha", new PrimerDark()); public static final Theme MOCHA = new DerivedTheme("mocha", "mocha", "Mocha", new PrimerDark());
// Adjust this to create your own theme // Adjust this to create your own theme
public static final Theme CUSTOM = new DerivedTheme("custom", "primer", "Custom", new PrimerDark()); public static final Theme CUSTOM = new DerivedTheme("custom", "primer", "Custom", new PrimerDark());

View file

@ -3,7 +3,6 @@ package io.xpipe.app.core.check;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppState;
import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.core.window.AppWindowHelper;
@ -35,7 +34,7 @@ public class AppAvCheck {
public static void check() throws Throwable { public static void check() throws Throwable {
// Only show this on first launch on windows // Only show this on first launch on windows
if (OsType.getLocal() != OsType.WINDOWS || !AppState.get().isInitialLaunch()) { if (OsType.getLocal() != OsType.WINDOWS || !AppProperties.get().isInitialLaunch()) {
return; return;
} }

View file

@ -6,7 +6,7 @@ import io.xpipe.app.issue.ErrorEvent;
public class AppJavaOptionsCheck { public class AppJavaOptionsCheck {
public static void check() { public static void check() {
if (AppCache.get("javaOptionsWarningShown", Boolean.class, () -> false)) { if (AppCache.getBoolean("javaOptionsWarningShown", false)) {
return; return;
} }

View file

@ -105,12 +105,17 @@ public abstract class OperationMode {
return; return;
} }
// Handle any startup uncaught errors
if (OperationMode.isInStartup() && thread.threadId() == 1) {
ex.printStackTrace();
OperationMode.halt(1);
}
ErrorEvent.fromThrowable(ex).unhandled(true).build().handle(); ErrorEvent.fromThrowable(ex).unhandled(true).build().handle();
}); });
TrackEvent.info("Initial setup"); TrackEvent.info("Initial setup");
AppProperties.init(); AppProperties.init();
AppState.init();
XPipeSession.init(AppProperties.get().getBuildUuid()); XPipeSession.init(AppProperties.get().getBuildUuid());
AppUserDirectoryCheck.check(); AppUserDirectoryCheck.check();
AppTempCheck.check(); AppTempCheck.check();

View file

@ -56,7 +56,7 @@ public abstract class PlatformMode extends OperationMode {
// If we downloaded an update, and decided to no longer automatically update, don't remind us! // If we downloaded an update, and decided to no longer automatically update, don't remind us!
// You can still update manually in the about tab // You can still update manually in the about tab
if (AppPrefs.get().automaticallyUpdate().get()) { if (AppPrefs.get().automaticallyUpdate().get() || AppPrefs.get().checkForSecurityUpdates().get()) {
UpdateAvailableAlert.showIfNeeded(); UpdateAvailableAlert.showIfNeeded();
} }

View file

@ -231,7 +231,7 @@ public class AppMainWindow {
return null; return null;
} }
WindowState state = AppCache.get("windowState", WindowState.class, () -> null); WindowState state = AppCache.getNonNull("windowState", WindowState.class, () -> null);
if (state == null) { if (state == null) {
return null; return null;
} }

View file

@ -4,7 +4,6 @@ import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.NetworkTunnelStore; import io.xpipe.core.store.NetworkTunnelStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.StatefulDataStore; import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.util.JacksonizedValue; import io.xpipe.core.util.JacksonizedValue;
@ -19,18 +18,22 @@ public class LocalStore extends JacksonizedValue
return ShellStoreState.class; return ShellStoreState.class;
} }
@Override public ShellControl control(ShellControl parent) {
public ShellControl parentControl() { return parent;
var pc = ProcessControlProvider.get().createLocalProcessControl(true);
pc.withSourceStore(this);
pc.withShellStateInit(this);
pc.withShellStateFail(this);
return pc;
} }
@Override @Override
public ShellControl control(ShellControl parent) { public ShellControlFunction shellFunction() {
return parent; return new ShellControlFunction() {
@Override
public ShellControl control() throws Exception {
var pc = ProcessControlProvider.get().createLocalProcessControl(true);
pc.withSourceStore(LocalStore.this);
pc.withShellStateInit(LocalStore.this);
pc.withShellStateFail(LocalStore.this);
return pc;
}
};
} }
@Override @Override

View file

@ -6,5 +6,5 @@ import javafx.beans.property.Property;
public interface PrefsHandler { public interface PrefsHandler {
<T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp); <T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp, boolean requiresRestart);
} }

View file

@ -2,7 +2,6 @@ package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.ModuleLayerLoader;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -21,31 +20,34 @@ public abstract class ScanProvider {
return ALL; return ALL;
} }
public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception { public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {
return null; return null;
} }
public abstract void scan(DataStoreEntry entry, ShellControl sc) throws Throwable;
@Value @Value
@AllArgsConstructor @AllArgsConstructor
public static class ScanOperation { public class ScanOpportunity {
String nameKey; String nameKey;
boolean disabled; boolean disabled;
boolean defaultSelected; boolean defaultSelected;
FailableRunnable<Throwable> scanner;
String licenseFeatureId; String licenseFeatureId;
public ScanOperation( public ScanOpportunity(String nameKey, boolean disabled, boolean defaultSelected) {
String nameKey, boolean disabled, boolean defaultSelected, FailableRunnable<Throwable> scanner) {
this.nameKey = nameKey; this.nameKey = nameKey;
this.disabled = disabled; this.disabled = disabled;
this.defaultSelected = defaultSelected; this.defaultSelected = defaultSelected;
this.scanner = scanner;
this.licenseFeatureId = null; this.licenseFeatureId = null;
} }
public String getLicensedFeatureId() { public String getLicensedFeatureId() {
return licenseFeatureId; return licenseFeatureId;
} }
public ScanProvider getProvider() {
return ScanProvider.this;
}
} }
public static class Loader implements ModuleLayerLoader { public static class Loader implements ModuleLayerLoader {

View file

@ -0,0 +1,8 @@
package io.xpipe.app.ext;
import io.xpipe.core.process.ShellControl;
public interface ShellControlFunction {
ShellControl control() throws Exception;
}

View file

@ -0,0 +1,14 @@
package io.xpipe.app.ext;
import io.xpipe.core.process.ShellControl;
public interface ShellControlParentStoreFunction extends ShellControlFunction {
default ShellControl control() throws Exception {
return control(getParentStore().standaloneControl());
}
ShellControl control(ShellControl parent) throws Exception;
ShellStore getParentStore();
}

View file

@ -0,0 +1,65 @@
package io.xpipe.app.ext;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.Session;
import io.xpipe.core.store.SessionListener;
import io.xpipe.core.util.FailableSupplier;
import lombok.Getter;
@Getter
public class ShellSession extends Session {
private final FailableSupplier<ShellControl> supplier;
private final ShellControl shellControl;
public ShellSession(SessionListener listener, FailableSupplier<ShellControl> supplier) throws Exception {
super(listener);
this.supplier = supplier;
this.shellControl = createControl();
}
public void start() throws Exception {
if (shellControl.isRunning()) {
return;
} else {
stop();
}
try {
shellControl.start();
} catch (Exception ex) {
stop();
throw ex;
}
}
private ShellControl createControl() throws Exception {
var pc = supplier.get();
pc.onStartupFail(shellControl -> {
listener.onStateChange(false);
});
pc.onInit(shellControl -> {
listener.onStateChange(true);
});
pc.onKill(() -> {
listener.onStateChange(false);
});
// Listen for parent exit as onExit is called before exit is completed
// In case it is stuck, we would not get the right status otherwise
pc.getParentControl().ifPresent(p -> {
p.onExit(shellControl -> {
listener.onStateChange(false);
});
});
return pc;
}
public boolean isRunning() {
return shellControl.isRunning();
}
public void stop() throws Exception {
shellControl.close();
}
}

View file

@ -0,0 +1,74 @@
package io.xpipe.app.ext;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.*;
public interface ShellStore extends DataStore, FileSystemStore, ValidatableStore, SingletonSessionStore<ShellSession> {
default ShellControl getOrStartSession() throws Exception {
var session = getSession();
if (session != null) {
session.getShellControl().refreshRunningState();
if (!session.isRunning()) {
stopSessionIfNeeded();
} else {
try {
session.getShellControl().command(" echo xpipetest").execute();
return session.getShellControl();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).expected().omit().handle();
stopSessionIfNeeded();
}
}
}
startSessionIfNeeded();
return new StubShellControl(getSession().getShellControl());
}
@Override
default ShellSession newSession() throws Exception {
var func = shellFunction();
var c = func.control();
if (!isInStorage()) {
c.withoutLicenseCheck();
}
return new ShellSession(this, () -> c);
}
@Override
default Class<?> getSessionClass() {
return ShellSession.class;
}
@Override
default FileSystem createFileSystem() throws Exception {
var func = shellFunction();
return new ConnectionFileSystem(func.control());
}
ShellControlFunction shellFunction();
@Override
default void validate() throws Exception {
try (var sc = tempControl().start()) {}
}
default ShellControl standaloneControl() throws Exception {
return shellFunction().control();
}
default ShellControl tempControl() throws Exception {
if (isSessionRunning()) {
return getOrStartSession();
}
var func = shellFunction();
if (!(func instanceof ShellControlParentStoreFunction p)) {
return func.control();
}
return p.control(p.getParentStore().getOrStartSession());
}
}

View file

@ -0,0 +1,13 @@
package io.xpipe.app.ext;
import io.xpipe.core.process.ShellControl;
public class StubShellControl extends WrapperShellControl {
public StubShellControl(ShellControl parent) {
super(parent);
}
@Override
public void close() throws Exception {}
}

View file

@ -0,0 +1,338 @@
package io.xpipe.app.ext;
import io.xpipe.core.process.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableConsumer;
import lombok.Getter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;
public class WrapperShellControl implements ShellControl {
@Getter
protected final ShellControl parent;
public WrapperShellControl(ShellControl parent) {
this.parent = parent;
}
@Override
public Optional<ShellControl> getParentControl() {
return parent.getParentControl();
}
@Override
public ShellTtyState getTtyState() {
return parent.getTtyState();
}
@Override
public void setNonInteractive() {
parent.setNonInteractive();
}
@Override
public boolean isInteractive() {
return parent.isInteractive();
}
@Override
public ElevationHandler getElevationHandler() {
return parent.getElevationHandler();
}
@Override
public void setElevationHandler(ElevationHandler ref) {
parent.setElevationHandler(ref);
}
@Override
public List<UUID> getExitUuids() {
return parent.getExitUuids();
}
@Override
public void setWorkingDirectory(WorkingDirectoryFunction workingDirectory) {
parent.setWorkingDirectory(workingDirectory);
}
@Override
public Optional<DataStore> getSourceStore() {
return parent.getSourceStore();
}
@Override
public ShellControl withSourceStore(DataStore store) {
return parent.withSourceStore(store);
}
@Override
public List<ShellInitCommand> getInitCommands() {
return parent.getInitCommands();
}
@Override
public ParentSystemAccess getParentSystemAccess() {
return parent.getParentSystemAccess();
}
@Override
public void setParentSystemAccess(ParentSystemAccess access) {
parent.setParentSystemAccess(access);
}
@Override
public ParentSystemAccess getLocalSystemAccess() {
return parent.getLocalSystemAccess();
}
@Override
public boolean isLocal() {
return parent.isLocal();
}
@Override
public ShellControl getMachineRootSession() {
return parent.getMachineRootSession();
}
@Override
public ShellControl withoutLicenseCheck() {
return parent.withoutLicenseCheck();
}
@Override
public String getOsName() {
return parent.getOsName();
}
@Override
public boolean isLicenseCheck() {
return parent.isLicenseCheck();
}
@Override
public ReentrantLock getLock() {
return parent.getLock();
}
@Override
public ShellDialect getOriginalShellDialect() {
return parent.getOriginalShellDialect();
}
@Override
public void setOriginalShellDialect(ShellDialect dialect) {
parent.setOriginalShellDialect(dialect);
}
@Override
public ShellControl onInit(FailableConsumer<ShellControl, Exception> pc) {
return parent.onInit(pc);
}
@Override
public ShellControl onExit(Consumer<ShellControl> pc) {
return parent.onExit(pc);
}
@Override
public ShellControl onKill(Runnable pc) {
return parent.onKill(pc);
}
@Override
public ShellControl onStartupFail(Consumer<Throwable> t) {
return parent.onStartupFail(t);
}
@Override
public UUID getUuid() {
return parent.getUuid();
}
@Override
public ShellControl withExceptionConverter(ExceptionConverter converter) {
return parent.withExceptionConverter(converter);
}
@Override
public void resetData() {
parent.resetData();
}
@Override
public String prepareTerminalOpen(TerminalInitScriptConfig config, WorkingDirectoryFunction workingDirectory)
throws Exception {
return parent.prepareTerminalOpen(config, workingDirectory);
}
@Override
public void refreshRunningState() {
parent.refreshRunningState();
}
@Override
public void closeStdin() throws IOException {
parent.closeStdin();
}
@Override
public boolean isStdinClosed() {
return parent.isStdinClosed();
}
@Override
public boolean isRunning() {
return parent.isRunning();
}
@Override
public ShellDialect getShellDialect() {
return parent.getShellDialect();
}
@Override
public void writeLine(String line) throws IOException {
parent.writeLine(line);
}
@Override
public void writeLine(String line, boolean log) throws IOException {
parent.writeLine(line, log);
}
@Override
public void write(byte[] b) throws IOException {
parent.write(b);
}
@Override
public void close() throws Exception {
parent.close();
}
@Override
public void shutdown() throws Exception {
parent.shutdown();
}
@Override
public void kill() {
parent.kill();
}
@Override
public ShellControl start() throws Exception {
return parent.start();
}
@Override
public InputStream getStdout() {
return parent.getStdout();
}
@Override
public OutputStream getStdin() {
return parent.getStdin();
}
@Override
public InputStream getStderr() {
return parent.getStderr();
}
@Override
public Charset getCharset() {
return parent.getCharset();
}
@Override
public ShellControl withErrorFormatter(Function<String, String> formatter) {
return parent.withErrorFormatter(formatter);
}
@Override
public String prepareIntermediateTerminalOpen(
TerminalInitFunction content, TerminalInitScriptConfig config, WorkingDirectoryFunction workingDirectory)
throws Exception {
return parent.prepareIntermediateTerminalOpen(content, config, workingDirectory);
}
@Override
public FilePath getSystemTemporaryDirectory() {
return parent.getSystemTemporaryDirectory();
}
@Override
public ShellControl withSecurityPolicy(ShellSecurityPolicy policy) {
return parent.withSecurityPolicy(policy);
}
@Override
public ShellSecurityPolicy getEffectiveSecurityPolicy() {
return parent.getEffectiveSecurityPolicy();
}
@Override
public String buildElevatedCommand(CommandConfiguration input, String prefix, UUID requestId, CountDown countDown)
throws Exception {
return parent.buildElevatedCommand(input, prefix, requestId, countDown);
}
@Override
public void restart() throws Exception {
parent.restart();
}
@Override
public OsType.Any getOsType() {
return parent.getOsType();
}
@Override
public ShellControl elevated(ElevationFunction elevationFunction) {
return parent.elevated(elevationFunction);
}
@Override
public ShellControl withInitSnippet(ShellInitCommand snippet) {
return parent.withInitSnippet(snippet);
}
@Override
public ShellControl subShell(ShellOpenFunction command, ShellOpenFunction terminalCommand) {
return parent.subShell(command, terminalCommand);
}
@Override
public ShellControl singularSubShell(ShellOpenFunction command) {
return parent.singularSubShell(command);
}
@Override
public void cd(String directory) throws Exception {
parent.cd(directory);
}
@Override
public CommandControl command(CommandBuilder builder) {
return parent.command(builder);
}
@Override
public void exitAndWait() throws IOException {
parent.exitAndWait();
}
}

View file

@ -97,8 +97,8 @@ public abstract class Comp<S extends CompStructure<?>> {
return apply(struc -> struc.get().setMinWidth(width)); return apply(struc -> struc.get().setMinWidth(width));
} }
public Comp<S> minHeight(double width) { public Comp<S> minHeight(double height) {
return apply(struc -> struc.get().setMinHeight(width)); return apply(struc -> struc.get().setMinHeight(height));
} }
public Comp<S> maxWidth(int width) { public Comp<S> maxWidth(int width) {

View file

@ -5,6 +5,7 @@ import io.xpipe.app.comp.store.*;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.LocalStore; import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
@ -12,7 +13,6 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.DataStoreCategoryChoiceComp; import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;

View file

@ -11,7 +11,6 @@ public class GuiErrorHandlerBase {
try { try {
PlatformState.initPlatformOrThrow(); PlatformState.initPlatformOrThrow();
AppProperties.init(); AppProperties.init();
AppState.init();
AppExtensionManager.init(false); AppExtensionManager.init(false);
AppI18n.init(); AppI18n.init();
AppStyle.init(); AppStyle.init();

View file

@ -2,7 +2,6 @@ package io.xpipe.app.issue;
import io.xpipe.app.core.AppLogs; import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppState;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
@ -145,6 +144,11 @@ public class SentryErrorHandler implements ErrorHandler {
AppPrefs.get() != null AppPrefs.get() != null
? AppPrefs.get().automaticallyUpdate().getValue().toString() ? AppPrefs.get().automaticallyUpdate().getValue().toString()
: "unknown"); : "unknown");
s.setTag(
"securityUpdatesEnabled",
AppPrefs.get() != null
? AppPrefs.get().checkForSecurityUpdates().getValue().toString()
: "unknown");
s.setTag("initError", String.valueOf(OperationMode.isInStartup())); s.setTag("initError", String.valueOf(OperationMode.isInStartup()));
s.setTag( s.setTag(
"developerMode", "developerMode",
@ -177,11 +181,7 @@ public class SentryErrorHandler implements ErrorHandler {
} }
var user = new User(); var user = new User();
user.setId(AppState.get().getUserId().toString()); user.setId(AppProperties.get().getUuid().toString());
if (ee.isShouldSendDiagnostics()) {
user.setEmail(AppState.get().getUserEmail());
user.setUsername(AppState.get().getUserName());
}
s.setUser(user); s.setUser(user);
} }
@ -189,7 +189,6 @@ public class SentryErrorHandler implements ErrorHandler {
// Assume that this object is wrapped by a synchronous error handler // Assume that this object is wrapped by a synchronous error handler
if (!init) { if (!init) {
AppProperties.init(); AppProperties.init();
AppState.init();
if (AppProperties.get().getSentryUrl() != null) { if (AppProperties.get().getSentryUrl() != null) {
Sentry.init(options -> { Sentry.init(options -> {
options.setDsn(AppProperties.get().getSentryUrl()); options.setDsn(AppProperties.get().getSentryUrl());

View file

@ -3,6 +3,7 @@ package io.xpipe.app.issue;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
@ -40,7 +41,6 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa
private void handleGui(ErrorEvent event) { private void handleGui(ErrorEvent event) {
try { try {
AppProperties.init(); AppProperties.init();
AppState.init();
AppExtensionManager.init(false); AppExtensionManager.init(false);
AppI18n.init(); AppI18n.init();
AppStyle.init(); AppStyle.init();
@ -74,7 +74,7 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa
} }
try { try {
var rel = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck(); var rel = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck(false, !AppPrefs.get().automaticallyUpdate().get());
if (rel != null && rel.isUpdate()) { if (rel != null && rel.isUpdate()) {
var update = AppWindowHelper.showBlockingAlert(alert -> { var update = AppWindowHelper.showBlockingAlert(alert -> {
alert.setAlertType(Alert.AlertType.INFORMATION); alert.setAlertType(Alert.AlertType.INFORMATION);

View file

@ -1,6 +1,7 @@
package io.xpipe.app.prefs; package io.xpipe.app.prefs;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.PrefsHandler; import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider; import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -22,8 +23,11 @@ import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableStringValue; import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
import lombok.experimental.NonFinal;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import java.nio.file.Path; import java.nio.file.Path;
@ -36,100 +40,104 @@ public class AppPrefs {
AppProperties.get() != null ? AppProperties.get().getDataDir().resolve("storage") : null; AppProperties.get() != null ? AppProperties.get().getDataDir().resolve("storage") : null;
private static final String DEVELOPER_MODE_PROP = "io.xpipe.app.developerMode"; private static final String DEVELOPER_MODE_PROP = "io.xpipe.app.developerMode";
private static AppPrefs INSTANCE; private static AppPrefs INSTANCE;
private final List<Mapping<?>> mapping = new ArrayList<>(); private final List<Mapping> mapping = new ArrayList<>();
@Getter
private final BooleanProperty requiresRestart = new SimpleBooleanProperty(false);
final BooleanProperty dontAllowTerminalRestart = final BooleanProperty dontAllowTerminalRestart =
mapVaultSpecific(new SimpleBooleanProperty(false), "dontAllowTerminalRestart", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "dontAllowTerminalRestart", Boolean.class, false);
final BooleanProperty enableHttpApi = final BooleanProperty enableHttpApi =
mapVaultSpecific(new SimpleBooleanProperty(false), "enableHttpApi", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "enableHttpApi", Boolean.class, false);
final BooleanProperty dontAutomaticallyStartVmSshServer = final BooleanProperty dontAutomaticallyStartVmSshServer =
mapVaultSpecific(new SimpleBooleanProperty(false), "dontAutomaticallyStartVmSshServer", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "dontAutomaticallyStartVmSshServer", Boolean.class, false);
final BooleanProperty dontAcceptNewHostKeys = final BooleanProperty dontAcceptNewHostKeys =
mapVaultSpecific(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class, false);
public final BooleanProperty performanceMode = map(new SimpleBooleanProperty(), "performanceMode", Boolean.class); public final BooleanProperty performanceMode = mapLocal(new SimpleBooleanProperty(), "performanceMode", Boolean.class, false);
public final BooleanProperty useBundledTools = public final BooleanProperty useBundledTools =
map(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class, true);
public final ObjectProperty<AppTheme.Theme> theme = public final ObjectProperty<AppTheme.Theme> theme =
map(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class); mapLocal(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class, false);
final BooleanProperty useSystemFont = map(new SimpleBooleanProperty(true), "useSystemFont", Boolean.class); final BooleanProperty useSystemFont = mapLocal(new SimpleBooleanProperty(true), "useSystemFont", Boolean.class, false);
final Property<Integer> uiScale = map(new SimpleObjectProperty<>(null), "uiScale", Integer.class); final Property<Integer> uiScale = mapLocal(new SimpleObjectProperty<>(null), "uiScale", Integer.class, true);
final BooleanProperty saveWindowLocation = final BooleanProperty saveWindowLocation =
map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class); mapLocal(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class, false);
final ObjectProperty<ExternalTerminalType> terminalType = final ObjectProperty<ExternalTerminalType> terminalType =
map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class); mapLocal(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class, false);
final ObjectProperty<ExternalRdpClientType> rdpClientType = final ObjectProperty<ExternalRdpClientType> rdpClientType =
map(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClientType.class); mapLocal(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClientType.class, false);
final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class); final DoubleProperty windowOpacity = mapLocal(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class, false);
final StringProperty customRdpClientCommand = final StringProperty customRdpClientCommand =
map(new SimpleStringProperty(null), "customRdpClientCommand", String.class); mapLocal(new SimpleStringProperty(null), "customRdpClientCommand", String.class, false);
final StringProperty customTerminalCommand = final StringProperty customTerminalCommand =
map(new SimpleStringProperty(null), "customTerminalCommand", String.class); mapLocal(new SimpleStringProperty(null), "customTerminalCommand", String.class, false);
final BooleanProperty clearTerminalOnInit = final BooleanProperty clearTerminalOnInit =
map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class); mapLocal(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class, false);
public final BooleanProperty disableCertutilUse = public final BooleanProperty disableCertutilUse =
map(new SimpleBooleanProperty(false), "disableCertutilUse", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "disableCertutilUse", Boolean.class, false);
public final BooleanProperty useLocalFallbackShell = public final BooleanProperty useLocalFallbackShell =
map(new SimpleBooleanProperty(false), "useLocalFallbackShell", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "useLocalFallbackShell", Boolean.class, true);
public final BooleanProperty disableTerminalRemotePasswordPreparation = mapVaultSpecific( public final BooleanProperty disableTerminalRemotePasswordPreparation = mapVaultShared(
new SimpleBooleanProperty(false), "disableTerminalRemotePasswordPreparation", Boolean.class); new SimpleBooleanProperty(false), "disableTerminalRemotePasswordPreparation", Boolean.class, false);
public final Property<Boolean> alwaysConfirmElevation = public final Property<Boolean> alwaysConfirmElevation =
mapVaultSpecific(new SimpleObjectProperty<>(false), "alwaysConfirmElevation", Boolean.class); mapVaultShared(new SimpleObjectProperty<>(false), "alwaysConfirmElevation", Boolean.class, false);
public final BooleanProperty dontCachePasswords = public final BooleanProperty dontCachePasswords =
mapVaultSpecific(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class, false);
public final BooleanProperty denyTempScriptCreation = public final BooleanProperty denyTempScriptCreation =
mapVaultSpecific(new SimpleBooleanProperty(false), "denyTempScriptCreation", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "denyTempScriptCreation", Boolean.class, false);
final Property<ExternalPasswordManager> passwordManager = final Property<ExternalPasswordManager> passwordManager =
mapVaultSpecific(new SimpleObjectProperty<>(), "passwordManager", ExternalPasswordManager.class); mapVaultShared(new SimpleObjectProperty<>(), "passwordManager", ExternalPasswordManager.class, false);
final StringProperty passwordManagerCommand = final StringProperty passwordManagerCommand =
map(new SimpleStringProperty(""), "passwordManagerCommand", String.class); mapLocal(new SimpleStringProperty(""), "passwordManagerCommand", String.class, false);
final ObjectProperty<StartupBehaviour> startupBehaviour = final ObjectProperty<StartupBehaviour> startupBehaviour =
map(new SimpleObjectProperty<>(StartupBehaviour.GUI), "startupBehaviour", StartupBehaviour.class); mapLocal(new SimpleObjectProperty<>(StartupBehaviour.GUI), "startupBehaviour", StartupBehaviour.class, true);
public final BooleanProperty enableGitStorage = public final BooleanProperty enableGitStorage =
map(new SimpleBooleanProperty(false), "enableGitStorage", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "enableGitStorage", Boolean.class, true);
final StringProperty storageGitRemote = map(new SimpleStringProperty(""), "storageGitRemote", String.class); final StringProperty storageGitRemote = mapLocal(new SimpleStringProperty(""), "storageGitRemote", String.class, true);
final ObjectProperty<CloseBehaviour> closeBehaviour = final ObjectProperty<CloseBehaviour> closeBehaviour =
map(new SimpleObjectProperty<>(CloseBehaviour.QUIT), "closeBehaviour", CloseBehaviour.class); mapLocal(new SimpleObjectProperty<>(CloseBehaviour.QUIT), "closeBehaviour", CloseBehaviour.class, false);
final ObjectProperty<ExternalEditorType> externalEditor = final ObjectProperty<ExternalEditorType> externalEditor =
map(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class); mapLocal(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class, false);
final StringProperty customEditorCommand = map(new SimpleStringProperty(""), "customEditorCommand", String.class); final StringProperty customEditorCommand = mapLocal(new SimpleStringProperty(""), "customEditorCommand", String.class, false);
final BooleanProperty preferEditorTabs = map(new SimpleBooleanProperty(true), "preferEditorTabs", Boolean.class);
final BooleanProperty automaticallyCheckForUpdates = final BooleanProperty automaticallyCheckForUpdates =
map(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class); mapLocal(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class, false);
final BooleanProperty encryptAllVaultData = final BooleanProperty encryptAllVaultData =
mapVaultSpecific(new SimpleBooleanProperty(false), "encryptAllVaultData", Boolean.class); mapVaultShared(new SimpleBooleanProperty(false), "encryptAllVaultData", Boolean.class, true);
final BooleanProperty enableTerminalLogging = final BooleanProperty enableTerminalLogging = map(Mapping.builder()
map(new SimpleBooleanProperty(false), "enableTerminalLogging", Boolean.class); .property(new SimpleBooleanProperty(false)).key("enableTerminalLogging").valueClass(Boolean.class).licenseFeatureId("logging").build());
final BooleanProperty enforceWindowModality = final BooleanProperty enforceWindowModality =
map(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class, false);
final BooleanProperty checkForSecurityUpdates =
mapLocal(new SimpleBooleanProperty(true), "checkForSecurityUpdates", Boolean.class, false);
final BooleanProperty condenseConnectionDisplay = final BooleanProperty condenseConnectionDisplay =
map(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class, false);
final BooleanProperty showChildCategoriesInParentCategory = final BooleanProperty showChildCategoriesInParentCategory =
map(new SimpleBooleanProperty(true), "showChildrenConnectionsInParentCategory", Boolean.class); mapLocal(new SimpleBooleanProperty(true), "showChildrenConnectionsInParentCategory", Boolean.class, false);
final BooleanProperty lockVaultOnHibernation = final BooleanProperty lockVaultOnHibernation =
map(new SimpleBooleanProperty(false), "lockVaultOnHibernation", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "lockVaultOnHibernation", Boolean.class, false);
final BooleanProperty openConnectionSearchWindowOnConnectionCreation = final BooleanProperty openConnectionSearchWindowOnConnectionCreation =
map(new SimpleBooleanProperty(true), "openConnectionSearchWindowOnConnectionCreation", Boolean.class); mapLocal(new SimpleBooleanProperty(true), "openConnectionSearchWindowOnConnectionCreation", Boolean.class, false);
final ObjectProperty<Path> storageDirectory = final ObjectProperty<Path> storageDirectory =
map(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), "storageDirectory", Path.class); mapLocal(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), "storageDirectory", Path.class, true);
final BooleanProperty confirmAllDeletions = final BooleanProperty confirmAllDeletions =
map(new SimpleBooleanProperty(false), "confirmAllDeletions", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "confirmAllDeletions", Boolean.class, false);
final BooleanProperty developerMode = map(new SimpleBooleanProperty(false), "developerMode", Boolean.class); final BooleanProperty developerMode = mapLocal(new SimpleBooleanProperty(false), "developerMode", Boolean.class, true);
final BooleanProperty developerDisableUpdateVersionCheck = final BooleanProperty developerDisableUpdateVersionCheck =
map(new SimpleBooleanProperty(false), "developerDisableUpdateVersionCheck", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "developerDisableUpdateVersionCheck", Boolean.class, false);
private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective = private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective =
bindDeveloperTrue(developerDisableUpdateVersionCheck); bindDeveloperTrue(developerDisableUpdateVersionCheck);
final BooleanProperty developerDisableGuiRestrictions = final BooleanProperty developerDisableGuiRestrictions =
map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class, false);
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = private final ObservableBooleanValue developerDisableGuiRestrictionsEffective =
bindDeveloperTrue(developerDisableGuiRestrictions); bindDeveloperTrue(developerDisableGuiRestrictions);
final BooleanProperty developerForceSshTty = final BooleanProperty developerForceSshTty =
map(new SimpleBooleanProperty(false), "developerForceSshTty", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "developerForceSshTty", Boolean.class, false);
final ObjectProperty<SupportedLocale> language = final ObjectProperty<SupportedLocale> language =
map(new SimpleObjectProperty<>(SupportedLocale.getEnglish()), "language", SupportedLocale.class); mapLocal(new SimpleObjectProperty<>(SupportedLocale.getEnglish()), "language", SupportedLocale.class, false);
final BooleanProperty requireDoubleClickForConnections = final BooleanProperty requireDoubleClickForConnections =
map(new SimpleBooleanProperty(false), "requireDoubleClickForConnections", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "requireDoubleClickForConnections", Boolean.class, false);
public ObservableBooleanValue requireDoubleClickForConnections() { public ObservableBooleanValue requireDoubleClickForConnections() {
return requireDoubleClickForConnections; return requireDoubleClickForConnections;
@ -140,12 +148,16 @@ public class AppPrefs {
@Getter @Getter
private final StringProperty lockCrypt = private final StringProperty lockCrypt =
mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class); mapVaultShared(new SimpleStringProperty(), "workspaceLock", String.class, true);
final StringProperty apiKey = final StringProperty apiKey =
mapVaultSpecific(new SimpleStringProperty(UUID.randomUUID().toString()), "apiKey", String.class); mapVaultShared(new SimpleStringProperty(UUID.randomUUID().toString()), "apiKey", String.class ,true);
final BooleanProperty disableApiAuthentication = final BooleanProperty disableApiAuthentication =
map(new SimpleBooleanProperty(false), "disableApiAuthentication", Boolean.class); mapLocal(new SimpleBooleanProperty(false), "disableApiAuthentication", Boolean.class, false);
public ObservableBooleanValue checkForSecurityUpdates() {
return checkForSecurityUpdates;
}
public ObservableBooleanValue enableTerminalLogging() { public ObservableBooleanValue enableTerminalLogging() {
return enableTerminalLogging; return enableTerminalLogging;
@ -168,16 +180,16 @@ public class AppPrefs {
} }
private final IntegerProperty editorReloadTimeout = private final IntegerProperty editorReloadTimeout =
map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class); mapLocal(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class, false);
private final BooleanProperty confirmDeletions = private final BooleanProperty confirmDeletions =
map(new SimpleBooleanProperty(true), "confirmDeletions", Boolean.class); mapLocal(new SimpleBooleanProperty(true), "confirmDeletions", Boolean.class, false);
@Getter @Getter
private final List<AppPrefsCategory> categories; private final List<AppPrefsCategory> categories;
private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler( private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler(
AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json")); AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json"));
private final Map<Mapping<?>, Comp<?>> customEntries = new LinkedHashMap<>(); private final Map<Mapping, Comp<?>> customEntries = new LinkedHashMap<>();
@Getter @Getter
private final Property<AppPrefsCategory> selectedCategory; private final Property<AppPrefsCategory> selectedCategory;
@ -207,7 +219,7 @@ public class AppPrefs {
new DeveloperCategory()) new DeveloperCategory())
.filter(appPrefsCategory -> appPrefsCategory.show()) .filter(appPrefsCategory -> appPrefsCategory.show())
.toList(); .toList();
var selected = AppCache.get("selectedPrefsCategory", Integer.class, () -> 0); var selected = AppCache.getNonNull("selectedPrefsCategory", Integer.class, () -> 0);
if (selected == null) { if (selected == null) {
selected = 0; selected = 0;
} }
@ -473,14 +485,20 @@ public class AppPrefs {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> T map(T o, String name, Class<?> clazz) { private <T> T map(Mapping m) {
mapping.add(new Mapping<>(name, (Property<T>) o, (Class<T>) clazz)); mapping.add(m);
return o; return (T) m.getProperty();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> T mapVaultSpecific(T o, String name, Class<?> clazz) { private <T> T mapLocal(Property<?> o, String name, Class<?> clazz, boolean requiresRestart) {
mapping.add(new Mapping<>(name, (Property<T>) o, (Class<T>) clazz, true)); mapping.add(new Mapping(name, o, clazz, false, requiresRestart));
return (T) o;
}
@SuppressWarnings("unchecked")
private <T> T mapVaultShared(T o, String name, Class<?> clazz, boolean requiresRestart) {
mapping.add(new Mapping(name, (Property<T>) o, (Class<T>) clazz, true, requiresRestart));
return o; return o;
} }
@ -498,7 +516,7 @@ public class AppPrefs {
if (rdpClientType.get() == null) { if (rdpClientType.get() == null) {
rdpClientType.setValue(ExternalRdpClientType.determineDefault()); rdpClientType.setValue(ExternalRdpClientType.determineDefault());
} }
if (AppState.get().isInitialLaunch()) { if (AppProperties.get().isInitialLaunch()) {
performanceMode.setValue(XPipeDistributionType.get() == XPipeDistributionType.WEBTOP); performanceMode.setValue(XPipeDistributionType.get() == XPipeDistributionType.WEBTOP);
} }
} }
@ -512,7 +530,7 @@ public class AppPrefs {
} }
private void loadLocal() { private void loadLocal() {
for (Mapping<?> value : mapping) { for (Mapping value : mapping) {
if (value.isVaultSpecific()) { if (value.isVaultSpecific()) {
continue; continue;
} }
@ -546,7 +564,7 @@ public class AppPrefs {
} }
private void loadSharedRemote() { private void loadSharedRemote() {
for (Mapping<?> value : mapping) { for (Mapping value : mapping) {
if (!value.isVaultSpecific()) { if (!value.isVaultSpecific()) {
continue; continue;
} }
@ -562,15 +580,19 @@ public class AppPrefs {
} }
} }
private <T> T loadValue(AppPrefsStorageHandler handler, Mapping<T> value) { @SuppressWarnings("unchecked")
private <T> T loadValue(AppPrefsStorageHandler handler, Mapping value) {
T def = (T) value.getProperty().getValue();
Property<T> property = (Property<T>) value.getProperty();
Class<T> clazz = (Class<T>) value.getValueClass();
var val = handler.loadObject( var val = handler.loadObject(
value.getKey(), value.getValueClass(), value.getProperty().getValue()); value.getKey(), clazz, def);
value.getProperty().setValue(val); property.setValue(val);
return val; return val;
} }
public void save() { public void save() {
for (Mapping<?> m : mapping) { for (Mapping m : mapping) {
AppPrefsStorageHandler handler = m.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler; AppPrefsStorageHandler handler = m.isVaultSpecific() ? vaultStorageHandler : globalStorageHandler;
// It might be possible that we save while the vault handler is not initialized yet / has no file or // It might be possible that we save while the vault handler is not initialized yet / has no file or
// directory // directory
@ -608,26 +630,36 @@ public class AppPrefs {
return ExternalApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key); return ExternalApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key);
} }
public Mapping getMapping(Object property) {
return mapping.stream().filter(m -> m.property == property).findFirst().orElseThrow();
}
@Value @Value
public static class Mapping<T> { @Builder
@AllArgsConstructor
public static class Mapping {
String key; String key;
Property<T> property; Property<?> property;
Class<T> valueClass; Class<?> valueClass;
boolean vaultSpecific; boolean vaultSpecific;
boolean requiresRestart;
String licenseFeatureId;
public Mapping(String key, Property<T> property, Class<T> valueClass) { public Mapping(String key, Property<?> property, Class<?> valueClass, boolean vaultSpecific, boolean requiresRestart) {
this.key = key;
this.property = property;
this.valueClass = valueClass;
this.vaultSpecific = false;
}
public Mapping(String key, Property<T> property, Class<T> valueClass, boolean vaultSpecific) {
this.key = key; this.key = key;
this.property = property; this.property = property;
this.valueClass = valueClass; this.valueClass = valueClass;
this.vaultSpecific = vaultSpecific; this.vaultSpecific = vaultSpecific;
this.requiresRestart = requiresRestart;
this.licenseFeatureId = null;
this.property.addListener((observable, oldValue, newValue) -> {
var running = OperationMode.get() == OperationMode.GUI;
if (running && requiresRestart) {
AppPrefs.get().requiresRestart.set(true);
}
});
} }
} }
@ -635,8 +667,8 @@ public class AppPrefs {
private class PrefsHandlerImpl implements PrefsHandler { private class PrefsHandlerImpl implements PrefsHandler {
@Override @Override
public <T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp) { public <T> void addSetting(String id, Class<T> c, Property<T> property, Comp<?> comp, boolean requiresRestart) {
var m = new Mapping<>(id, property, c); var m = new Mapping(id, property, c, false, requiresRestart);
customEntries.put(m, comp); customEntries.put(m, comp);
mapping.add(m); mapping.add(m);
} }

View file

@ -46,6 +46,7 @@ public class AppPrefsComp extends SimpleComp {
sidebar.setMaxWidth(280); sidebar.setMaxWidth(280);
var split = new HBox(sidebar, pfxLimit); var split = new HBox(sidebar, pfxLimit);
HBox.setMargin(sidebar, new Insets(6));
HBox.setHgrow(pfxLimit, Priority.ALWAYS); HBox.setHgrow(pfxLimit, Priority.ALWAYS);
split.setFillHeight(true); split.setFillHeight(true);
split.getStyleClass().add("prefs"); split.getStyleClass().add("prefs");

View file

@ -1,17 +1,24 @@
package io.xpipe.app.prefs; package io.xpipe.app.prefs;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.text.TextAlignment; import javafx.scene.text.TextAlignment;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class AppPrefsSidebarComp extends SimpleComp { public class AppPrefsSidebarComp extends SimpleComp {
@ -33,7 +40,17 @@ public class AppPrefsSidebarComp extends SimpleComp {
}) })
.grow(true, false); .grow(true, false);
}) })
.toList(); .collect(Collectors.toCollection(ArrayList::new));
var restartButton = new ButtonComp(AppI18n.observable("restart"), new FontIcon("mdi2r-restart"), () -> {
OperationMode.restart();
});
restartButton.grow(true, false);
restartButton.visible(AppPrefs.get().getRequiresRestart());
restartButton.padding(new Insets(6, 10, 6, 6));
buttons.add(Comp.vspacer());
buttons.add(restartButton);
var vbox = new VerticalComp(buttons).styleClass("sidebar"); var vbox = new VerticalComp(buttons).styleClass("sidebar");
vbox.apply(struc -> { vbox.apply(struc -> {
AppPrefs.get().getSelectedCategory().subscribe(val -> { AppPrefs.get().getSelectedCategory().subscribe(val -> {

View file

@ -20,7 +20,7 @@ public class CloseBehaviourAlert {
return true; return true;
} }
boolean set = AppCache.get("closeBehaviourSet", Boolean.class, () -> false); boolean set = AppCache.getBoolean("closeBehaviourSet", false);
if (set) { if (set) {
return true; return true;
} }

View file

@ -49,9 +49,7 @@ public class EditorCategory extends AppPrefsCategory {
.addComp(new TextFieldComp(prefs.customEditorCommand, true) .addComp(new TextFieldComp(prefs.customEditorCommand, true)
.apply(struc -> struc.get().setPromptText("myeditor $FILE")) .apply(struc -> struc.get().setPromptText("myeditor $FILE"))
.hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM))) .hide(prefs.externalEditor.isNotEqualTo(ExternalEditorType.CUSTOM)))
.addComp(terminalTest) .addComp(terminalTest))
.nameAndDescription("preferEditorTabs")
.addToggle(prefs.preferEditorTabs))
.buildComp(); .buildComp();
} }
} }

View file

@ -22,13 +22,10 @@ public class LoggingCategory extends AppPrefsCategory {
@Override @Override
protected Comp<?> create() { protected Comp<?> create() {
var prefs = AppPrefs.get(); var prefs = AppPrefs.get();
var feature = LicenseProvider.get().getFeature("logging");
var supported = feature.isSupported() || feature.isPreviewSupported();
var title = AppI18n.observable("sessionLogging").map(s -> s + (supported ? "" : " (Pro)"));
return new OptionsBuilder() return new OptionsBuilder()
.addTitle(title) .addTitle("sessionLogging")
.sub(new OptionsBuilder() .sub(new OptionsBuilder()
.nameAndDescription("enableTerminalLogging") .pref(prefs.enableTerminalLogging)
.addToggle(prefs.enableTerminalLogging) .addToggle(prefs.enableTerminalLogging)
.nameAndDescription("terminalLoggingDirectory") .nameAndDescription("terminalLoggingDirectory")
.addComp(new ButtonComp(AppI18n.observable("openSessionLogs"), () -> { .addComp(new ButtonComp(AppI18n.observable("openSessionLogs"), () -> {

View file

@ -15,6 +15,8 @@ public class SecurityCategory extends AppPrefsCategory {
var builder = new OptionsBuilder(); var builder = new OptionsBuilder();
builder.addTitle("securityPolicy") builder.addTitle("securityPolicy")
.sub(new OptionsBuilder() .sub(new OptionsBuilder()
.pref(prefs.checkForSecurityUpdates)
.addToggle(prefs.checkForSecurityUpdates)
.nameAndDescription("alwaysConfirmElevation") .nameAndDescription("alwaysConfirmElevation")
.addToggle(prefs.alwaysConfirmElevation) .addToggle(prefs.alwaysConfirmElevation)
.nameAndDescription("dontCachePasswords") .nameAndDescription("dontCachePasswords")

View file

@ -71,13 +71,7 @@ public class SyncCategory extends AppPrefsCategory {
testButton.apply(struc -> button.set(struc.get())); testButton.apply(struc -> button.set(struc.get()));
testButton.padding(new Insets(6, 10, 6, 6)); testButton.padding(new Insets(6, 10, 6, 6));
var restartButton = new ButtonComp(AppI18n.observable("restart"), new FontIcon("mdi2r-restart"), () -> { var testRow = new HorizontalComp(List.of(testButton))
OperationMode.restart();
});
restartButton.visible(canRestart);
restartButton.padding(new Insets(6, 10, 6, 6));
var testRow = new HorizontalComp(List.of(testButton, restartButton))
.spacing(10) .spacing(10)
.padding(new Insets(10, 0, 0, 0)) .padding(new Insets(10, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)); .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));
@ -92,10 +86,9 @@ public class SyncCategory extends AppPrefsCategory {
var builder = new OptionsBuilder(); var builder = new OptionsBuilder();
builder.addTitle("sync") builder.addTitle("sync")
.sub(new OptionsBuilder() .sub(new OptionsBuilder()
.name("enableGitStorage") .pref(prefs.enableGitStorage)
.description("enableGitStorageDescription")
.addToggle(prefs.enableGitStorage) .addToggle(prefs.enableGitStorage)
.nameAndDescription("storageGitRemote") .pref(prefs.storageGitRemote)
.addComp(remoteRow, prefs.storageGitRemote) .addComp(remoteRow, prefs.storageGitRemote)
.disable(prefs.enableGitStorage.not()) .disable(prefs.enableGitStorage.not())
.addComp(testRow) .addComp(testRow)

View file

@ -2,8 +2,8 @@ package io.xpipe.app.prefs;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.ChoiceComp; import io.xpipe.app.fxcomps.impl.ChoiceComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
@ -41,7 +41,10 @@ public class TerminalCategory extends AppPrefsCategory {
var term = AppPrefs.get().terminalType().getValue(); var term = AppPrefs.get().terminalType().getValue();
if (term != null) { if (term != null) {
TerminalLauncher.open( TerminalLauncher.open(
"Test", new LocalStore().control().command("echo Test")); "Test",
ProcessControlProvider.get()
.createLocalProcessControl(true)
.command("echo Test"));
} }
}); });
}))) })))

View file

@ -29,13 +29,13 @@ public class UpdateCheckComp extends SimpleComp {
} }
private void performUpdateAndRestart() { private void performUpdateAndRestart() {
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(); XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(false, false);
UpdateAvailableAlert.showIfNeeded(); UpdateAvailableAlert.showIfNeeded();
} }
private void refresh() { private void refresh() {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck(); XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck(false, false);
XPipeDistributionType.get().getUpdateHandler().prepareUpdate(); XPipeDistributionType.get().getUpdateHandler().prepareUpdate();
}); });
} }

View file

@ -27,15 +27,6 @@ public class VaultCategory extends AppPrefsCategory {
public Comp<?> create() { public Comp<?> create() {
var prefs = AppPrefs.get(); var prefs = AppPrefs.get();
var builder = new OptionsBuilder(); var builder = new OptionsBuilder();
if (!STORAGE_DIR_FIXED) {
var sub =
new OptionsBuilder().nameAndDescription("storageDirectory").addPath(prefs.storageDirectory);
sub.withValidator(val -> {
sub.check(Validator.absolutePath(val, prefs.storageDirectory));
sub.check(Validator.directory(val, prefs.storageDirectory));
});
builder.addTitle("storage").sub(sub);
}
var encryptVault = new SimpleBooleanProperty(prefs.encryptAllVaultData().get()); var encryptVault = new SimpleBooleanProperty(prefs.encryptAllVaultData().get());
encryptVault.addListener((observable, oldValue, newValue) -> { encryptVault.addListener((observable, oldValue, newValue) -> {

View file

@ -16,13 +16,7 @@ public class WorkspacesCategory extends AppPrefsCategory {
@Override @Override
protected Comp<?> create() { protected Comp<?> create() {
return new OptionsBuilder() return new OptionsBuilder()
.addTitle(AppI18n.observable("manageWorkspaces") .addTitle(LicenseProvider.get().getFeature("workspaces").suffixObservable("manageWorkspaces"))
.map(s -> s
+ (LicenseProvider.get()
.getFeature("workspaces")
.isSupported()
? ""
: " (Pro)")))
.sub(new OptionsBuilder() .sub(new OptionsBuilder()
.nameAndDescription("workspaceAdd") .nameAndDescription("workspaceAdd")
.addComp(new ButtonComp(AppI18n.observable("addWorkspace"), WorkspaceCreationAlert::showAsync))) .addComp(new ButtonComp(AppI18n.observable("addWorkspace"), WorkspaceCreationAlert::showAsync)))

View file

@ -52,7 +52,8 @@ public class DataStateProviderImpl extends DataStateProvider {
return; return;
} }
var entry = DataStorage.get().getStoreEntryIfPresent(store, true); var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()
.getStoreEntryInProgressIfPresent(store));
if (entry.isEmpty()) { if (entry.isEmpty()) {
return; return;
} }
@ -66,7 +67,8 @@ public class DataStateProviderImpl extends DataStateProvider {
return def.get(); return def.get();
} }
var entry = DataStorage.get().getStoreEntryIfPresent(store, true); var entry = DataStorage.get().getStoreEntryIfPresent(store, true).or(() -> DataStorage.get()
.getStoreEntryInProgressIfPresent(store));
if (entry.isEmpty()) { if (entry.isEmpty()) {
return def.get(); return def.get();
} }

View file

@ -10,7 +10,6 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedChildStore; import io.xpipe.core.store.FixedChildStore;
import io.xpipe.core.store.StorePath; import io.xpipe.core.store.StorePath;
import io.xpipe.core.store.ValidationContext;
import io.xpipe.core.util.UuidHelper; import io.xpipe.core.util.UuidHelper;
import javafx.util.Pair; import javafx.util.Pair;
@ -365,30 +364,24 @@ public abstract class DataStorage {
} }
@SneakyThrows @SneakyThrows
public boolean refreshChildren(DataStoreEntry e, ValidationContext<?> context) { public boolean refreshChildren(DataStoreEntry e) {
return refreshChildren(e, context, false); return refreshChildren(e, false);
} }
@SuppressWarnings("unchecked") public boolean refreshChildrenOrThrow(DataStoreEntry e) throws Exception {
public <T extends ValidationContext<?>> boolean refreshChildren(DataStoreEntry e, T context, boolean throwOnFail) return refreshChildren(e, true);
throws Exception { }
if (!(e.getStore() instanceof FixedHierarchyStore<?> h)) {
public boolean refreshChildren(DataStoreEntry e, boolean throwOnFail) throws Exception {
if (!(e.getStore() instanceof FixedHierarchyStore h)) {
return false; return false;
} }
e.incrementBusyCounter(); e.incrementBusyCounter();
List<? extends DataStoreEntryRef<? extends FixedChildStore>> newChildren; List<? extends DataStoreEntryRef<? extends FixedChildStore>> newChildren;
var hadContext = context != null;
try { try {
if (context == null) { newChildren = ((FixedHierarchyStore) h)
context = (T) h.createContext(); .listChildren().stream()
if (context == null) {
return false;
}
}
newChildren = ((FixedHierarchyStore<T>) h)
.listChildren(context).stream()
.filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null) .filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null)
.toList(); .toList();
} catch (Exception ex) { } catch (Exception ex) {
@ -399,9 +392,6 @@ public abstract class DataStorage {
return false; return false;
} }
} finally { } finally {
if (context != null && !hadContext) {
context.close();
}
e.decrementBusyCounter(); e.decrementBusyCounter();
} }

View file

@ -516,47 +516,18 @@ public class DataStoreEntry extends StorageElement {
} }
public void validateOrThrow() throws Throwable { public void validateOrThrow() throws Throwable {
validateOrThrowAndClose(null);
}
public boolean validateOrThrowAndClose(ValidationContext<?> existingContext) throws Throwable {
var subContext = validateAndKeepOpenOrThrowAndClose(existingContext);
if (subContext != null) {
subContext.close();
return true;
} else {
return false;
}
}
@SuppressWarnings("unchecked")
public <T> ValidationContext<?> validateAndKeepOpenOrThrowAndClose(ValidationContext<?> existingContext)
throws Throwable {
if (store == null) { if (store == null) {
return null; return;
} }
if (!(store instanceof ValidatableStore<?> l)) { if (!(store instanceof ValidatableStore l)) {
return null; return;
} }
try { try {
store.checkComplete(); store.checkComplete();
incrementBusyCounter(); incrementBusyCounter();
ValidationContext<T> context = existingContext != null l.validate();
? (ValidationContext<T>) existingContext
: (ValidationContext<T>) l.createContext();
if (context == null) {
return null;
}
try {
var r = ((ValidatableStore<ValidationContext<T>>) l).validate(context);
return r;
} catch (Throwable t) {
context.close();
throw t;
}
} finally { } finally {
decrementBusyCounter(); decrementBusyCounter();
} }

View file

@ -154,7 +154,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
private boolean showInfo() { private boolean showInfo() {
boolean set = AppCache.get("xshellSetup", Boolean.class, () -> false); boolean set = AppCache.getBoolean("xshellSetup", false);
if (set) { if (set) {
return true; return true;
} }
@ -368,7 +368,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
private boolean showInfo() throws IOException { private boolean showInfo() throws IOException {
boolean set = AppCache.get("termiusSetup", Boolean.class, () -> false); boolean set = AppCache.getBoolean("termiusSetup", false);
if (set) { if (set) {
return true; return true;
} }

View file

@ -127,7 +127,7 @@ public interface KittyTerminalType extends ExternalTerminalType {
var socket = getSocket(); var socket = getSocket();
try (var sc = LocalShell.getShell().start()) { try (var sc = LocalShell.getShell().start()) {
if (sc.executeSimpleBooleanCommand( if (sc.executeSimpleBooleanCommand(
"test -w " + sc.getShellDialect().fileArgument(socket))) { "/usr/bin/test -w " + sc.getShellDialect().fileArgument(socket))) {
return false; return false;
} }
@ -174,7 +174,7 @@ public interface KittyTerminalType extends ExternalTerminalType {
var socket = getSocket(); var socket = getSocket();
try (var sc = LocalShell.getShell().start()) { try (var sc = LocalShell.getShell().start()) {
if (sc.executeSimpleBooleanCommand( if (sc.executeSimpleBooleanCommand(
"test -w " + sc.getShellDialect().fileArgument(socket))) { "/usr/bin/test -w " + sc.getShellDialect().fileArgument(socket))) {
return false; return false;
} }

View file

@ -33,7 +33,8 @@ public interface WindowsTerminalType extends ExternalTerminalType {
// wt can't elevate a command consisting out of multiple parts if wt is configured to elevate by default // wt can't elevate a command consisting out of multiple parts if wt is configured to elevate by default
// So work around it by just passing a script file if possible // So work around it by just passing a script file if possible
if (ShellDialects.isPowershell(configuration.getScriptDialect())) { if (ShellDialects.isPowershell(configuration.getScriptDialect())) {
var usesPowershell = ShellDialects.isPowershell(ProcessControlProvider.get().getEffectiveLocalDialect()); var usesPowershell =
ShellDialects.isPowershell(ProcessControlProvider.get().getEffectiveLocalDialect());
if (usesPowershell) { if (usesPowershell) {
// We can't work around it in this case, so let's just hope that there's no elevation configured // We can't work around it in this case, so let's just hope that there's no elevation configured
cmd.add(configuration.getDialectLaunchCommand()); cmd.add(configuration.getDialectLaunchCommand());
@ -41,7 +42,9 @@ public interface WindowsTerminalType extends ExternalTerminalType {
// There might be a mismatch if we are for example using logging // There might be a mismatch if we are for example using logging
// In this case we can actually work around the problem // In this case we can actually work around the problem
cmd.addFile(shellControl -> { cmd.addFile(shellControl -> {
var script = ScriptHelper.createExecScript(shellControl, configuration.getDialectLaunchCommand().buildFull(shellControl)); var script = ScriptHelper.createExecScript(
shellControl,
configuration.getDialectLaunchCommand().buildFull(shellControl));
return script.toString(); return script.toString();
}); });
} }

View file

@ -1,10 +1,11 @@
package io.xpipe.app.test; package io.xpipe.app.test;
import io.xpipe.core.util.FailableSupplier;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Named;
import java.util.*; import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public abstract class TestModule<V> { public abstract class TestModule<V> {
@ -12,6 +13,7 @@ public abstract class TestModule<V> {
private static final Map<Class<?>, Map<String, ?>> values = new LinkedHashMap<>(); private static final Map<Class<?>, Map<String, ?>> values = new LinkedHashMap<>();
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
@SneakyThrows
public static <T> Map<String, T> get(Class<T> c, Module module, String... classes) { public static <T> Map<String, T> get(Class<T> c, Module module, String... classes) {
if (!values.containsKey(c)) { if (!values.containsKey(c)) {
List<Class<?>> loadedClasses = Arrays.stream(classes) List<Class<?>> loadedClasses = Arrays.stream(classes)
@ -31,8 +33,13 @@ public abstract class TestModule<V> {
}); });
} }
return (Map<String, T>) values.get(c).entrySet().stream() Map<String, Object> map = new HashMap<>();
.collect(Collectors.toMap(o -> o.getKey(), o -> ((Supplier<?>) o.getValue()).get())); for (Map.Entry<String, ?> o : values.get(c).entrySet()) {
if (map.put(o.getKey(), ((FailableSupplier<?>) o.getValue()).get()) != null) {
throw new IllegalStateException("Duplicate key");
}
}
return (Map<String, T>) map;
} }
public static <T> Stream<Named<T>> getArguments(Class<T> c, Module module, String... classes) { public static <T> Stream<Named<T>> getArguments(Class<T> c, Module module, String... classes) {
@ -43,7 +50,7 @@ public abstract class TestModule<V> {
return argumentBuilder.build(); return argumentBuilder.build();
} }
protected abstract void init(Map<String, Supplier<V>> list) throws Exception; protected abstract void init(Map<String, FailableSupplier<V>> list) throws Exception;
protected abstract Class<V> getValueClass(); protected abstract Class<V> getValueClass();
} }

View file

@ -1,9 +1,11 @@
package io.xpipe.app.update; package io.xpipe.app.update;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.HttpHelper; import io.xpipe.app.util.HttpHelper;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -15,6 +17,9 @@ import org.kohsuke.github.authorization.AuthorizationProvider;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -112,30 +117,39 @@ public class AppDownloads {
} }
} }
public static Optional<GHRelease> getTopReleaseIncludingPreRelease() throws IOException { private static String queryLatestVersion(boolean first, boolean securityOnly) throws Exception {
var repo = getRepository(); var req = JsonNodeFactory.instance.objectNode();
return Optional.ofNullable(repo.listReleases().iterator().next()); req.put("securityOnly", securityOnly);
req.put("ptb", AppProperties.get().isStaging());
req.put("os", OsType.getLocal().getId());
req.put("arch", AppProperties.get().getArch());
req.put("uuid", AppProperties.get().getUuid().toString());
req.put("version", AppProperties.get().getVersion());
req.put("first", first);
var url = URI.create("https://api.xpipe.io/version");
var builder = HttpRequest.newBuilder();
var httpRequest = builder.uri(url)
.POST(HttpRequest.BodyPublishers.ofString(req.toPrettyString()))
.build();
var client = HttpClient.newHttpClient();
var response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException(response.body());
}
var json = JacksonMapper.getDefault().readTree(response.body());
var ver = json.required("version").asText();
return ver;
} }
public static Optional<GHRelease> getMarkedLatestRelease() throws IOException { public static Optional<GHRelease> queryLatestRelease(boolean first, boolean securityOnly) throws Exception {
var repo = getRepository();
return Optional.ofNullable(repo.getLatestRelease());
}
public static Optional<GHRelease> getLatestSuitableRelease() throws IOException {
try { try {
var preIncluding = getTopReleaseIncludingPreRelease(); var ver = queryLatestVersion(first, securityOnly);
// If we are currently running a prerelease, always return this as the suitable release! var repo = getRepository();
if (preIncluding.isPresent() var rel = repo.getReleaseByTagName(ver);
&& preIncluding.get().isPrerelease() return Optional.ofNullable(rel);
&& AppProperties.get() } catch (Exception e) {
.getVersion()
.equals(preIncluding.get().getTagName())) {
return preIncluding;
}
return getMarkedLatestRelease();
} catch (IOException e) {
throw ErrorEvent.expected(e); throw ErrorEvent.expected(e);
} }
} }

View file

@ -3,7 +3,6 @@ package io.xpipe.app.update;
import io.xpipe.app.core.AppLogs; import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.LocalShell;
@ -79,7 +78,6 @@ public class AppInstaller {
@Override @Override
public void installLocal(Path file) throws Exception { public void installLocal(Path file) throws Exception {
var shellProcessControl = new LocalStore().control().start();
var exec = (AppProperties.get().isDevelopmentEnvironment() var exec = (AppProperties.get().isDevelopmentEnvironment()
? Path.of(XPipeInstallation.getLocalDefaultInstallationBasePath()) ? Path.of(XPipeInstallation.getLocalDefaultInstallationBasePath())
: XPipeInstallation.getCurrentInstallationBasePath()) : XPipeInstallation.getCurrentInstallationBasePath())
@ -98,7 +96,7 @@ public class AppInstaller {
+ ScriptHelper.createLocalExecScript(command) + "`\"\""; + ScriptHelper.createLocalExecScript(command) + "`\"\"";
runAndClose(() -> { runAndClose(() -> {
shellProcessControl.executeSimpleCommand(toRun); LocalShell.getShell().executeSimpleCommand(toRun);
}); });
} }

View file

@ -1,58 +0,0 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.fxcomps.impl.CodeSnippet;
import io.xpipe.app.fxcomps.impl.CodeSnippetComp;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.layout.Region;
import java.time.Instant;
public class ChocoUpdater extends UpdateHandler {
public ChocoUpdater() {
super(true);
}
@Override
public Region createInterface() {
var snippet = CodeSnippet.builder()
.keyword("choco")
.space()
.identifier("install")
.space()
.string("xpipe")
.space()
.keyword("--version=" + getPreparedUpdate().getValue().getVersion())
.build();
return new CodeSnippetComp(false, new SimpleObjectProperty<>(snippet)).createRegion();
}
public AvailableRelease refreshUpdateCheckImpl() throws Exception {
try (var sc = new LocalStore().control().start()) {
var latest = sc.executeSimpleStringCommand("choco outdated -r --nocolor")
.lines()
.filter(s -> s.startsWith("xpipe"))
.findAny()
.map(string -> string.split("\\|")[2]);
if (latest.isEmpty()) {
return null;
}
var isUpdate = isUpdate(latest.get());
var rel = new AvailableRelease(
AppProperties.get().getVersion(),
XPipeDistributionType.get().getId(),
latest.get(),
"https://community.chocolatey.org/packages/xpipe/" + latest,
null,
null,
Instant.now(),
isUpdate);
lastUpdateCheckResult.setValue(rel);
return lastUpdateCheckResult.getValue();
}
}
}

View file

@ -65,8 +65,8 @@ public class GitHubUpdater extends UpdateHandler {
} }
} }
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception { public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {
var rel = AppDownloads.getLatestSuitableRelease(); var rel = AppDownloads.queryLatestRelease(first, securityOnly);
event("Determined latest suitable release " event("Determined latest suitable release "
+ rel.map(GHRelease::getName).orElse(null)); + rel.map(GHRelease::getName).orElse(null));

View file

@ -29,8 +29,8 @@ public class PortableUpdater extends UpdateHandler {
.createRegion(); .createRegion();
} }
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception { public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {
var rel = AppDownloads.getLatestSuitableRelease(); var rel = AppDownloads.queryLatestRelease(first, securityOnly);
event("Determined latest suitable release " event("Determined latest suitable release "
+ rel.map(GHRelease::getName).orElse(null)); + rel.map(GHRelease::getName).orElse(null));

View file

@ -3,6 +3,7 @@ package io.xpipe.app.update;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
@ -22,7 +23,7 @@ public class UpdateAvailableAlert {
} }
// Check whether we still have the latest version prepared // Check whether we still have the latest version prepared
uh.refreshUpdateCheckSilent(); uh.refreshUpdateCheckSilent(false, !AppPrefs.get().automaticallyUpdate().get());
if (uh.getPreparedUpdate().getValue() == null) { if (uh.getPreparedUpdate().getValue() == null) {
return; return;
} }

View file

@ -36,7 +36,7 @@ public abstract class UpdateHandler {
protected final boolean updateSucceeded; protected final boolean updateSucceeded;
protected UpdateHandler(boolean startBackgroundThread) { protected UpdateHandler(boolean startBackgroundThread) {
performedUpdate = AppCache.get("performedUpdate", PerformedUpdate.class, () -> null); performedUpdate = AppCache.getNonNull("performedUpdate", PerformedUpdate.class, () -> null);
var hasUpdated = performedUpdate != null; var hasUpdated = performedUpdate != null;
event("Was updated is " + hasUpdated); event("Was updated is " + hasUpdated);
if (hasUpdated) { if (hasUpdated) {
@ -48,7 +48,7 @@ public abstract class UpdateHandler {
updateSucceeded = false; updateSucceeded = false;
} }
preparedUpdate.setValue(AppCache.get("preparedUpdate", PreparedUpdate.class, () -> null)); preparedUpdate.setValue(AppCache.getNonNull("preparedUpdate", PreparedUpdate.class, () -> null));
// Check if the original version this was downloaded from is still the same // Check if the original version this was downloaded from is still the same
if (preparedUpdate.getValue() != null if (preparedUpdate.getValue() != null
@ -99,12 +99,14 @@ public abstract class UpdateHandler {
private void startBackgroundUpdater() { private void startBackgroundUpdater() {
ThreadHelper.createPlatformThread("updater", true, () -> { ThreadHelper.createPlatformThread("updater", true, () -> {
var checked = false;
ThreadHelper.sleep(Duration.ofMinutes(5).toMillis()); ThreadHelper.sleep(Duration.ofMinutes(5).toMillis());
event("Starting background updater thread"); event("Starting background updater thread");
while (true) { while (true) {
if (AppPrefs.get().automaticallyUpdate().get()) { if (AppPrefs.get().automaticallyUpdate().get() || AppPrefs.get().checkForSecurityUpdates().get()) {
event("Performing background update"); event("Performing background update");
refreshUpdateCheckSilent(); refreshUpdateCheckSilent(!checked, !AppPrefs.get().automaticallyUpdate().get());
checked = true;
prepareUpdate(); prepareUpdate();
} }
@ -134,17 +136,9 @@ public abstract class UpdateHandler {
return false; return false;
} }
public final void prepareUpdateAsync() { public final AvailableRelease refreshUpdateCheckSilent(boolean first, boolean securityOnly) {
ThreadHelper.runAsync(() -> prepareUpdate());
}
public final void refreshUpdateCheckAsync() {
ThreadHelper.runAsync(() -> refreshUpdateCheckSilent());
}
public final AvailableRelease refreshUpdateCheckSilent() {
try { try {
return refreshUpdateCheck(); return refreshUpdateCheck(first, securityOnly);
} catch (Exception ex) { } catch (Exception ex) {
ErrorEvent.fromThrowable(ex).discard().handle(); ErrorEvent.fromThrowable(ex).discard().handle();
return null; return null;
@ -214,7 +208,7 @@ public abstract class UpdateHandler {
// Check if prepared update is still the latest. // Check if prepared update is still the latest.
// We only do that here to minimize the sent requests by only executing when it's really necessary // We only do that here to minimize the sent requests by only executing when it's really necessary
var available = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(); var available = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(false, !AppPrefs.get().automaticallyUpdate().get());
if (preparedUpdate.getValue() == null) { if (preparedUpdate.getValue() == null) {
return; return;
} }
@ -233,17 +227,17 @@ public abstract class UpdateHandler {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
public final AvailableRelease refreshUpdateCheck() throws Exception { public final AvailableRelease refreshUpdateCheck(boolean first, boolean securityOnly) throws Exception {
if (busy.getValue()) { if (busy.getValue()) {
return lastUpdateCheckResult.getValue(); return lastUpdateCheckResult.getValue();
} }
try (var ignored = new BooleanScope(busy).start()) { try (var ignored = new BooleanScope(busy).start()) {
return refreshUpdateCheckImpl(); return refreshUpdateCheckImpl(first, securityOnly);
} }
} }
public abstract AvailableRelease refreshUpdateCheckImpl() throws Exception; public abstract AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception;
@Value @Value
@Builder @Builder

View file

@ -24,7 +24,7 @@ public enum XPipeDistributionType {
NATIVE_INSTALLATION("install", true, () -> new GitHubUpdater(true)), NATIVE_INSTALLATION("install", true, () -> new GitHubUpdater(true)),
HOMEBREW("homebrew", true, () -> new HomebrewUpdater()), HOMEBREW("homebrew", true, () -> new HomebrewUpdater()),
WEBTOP("webtop", true, () -> new PortableUpdater(false)), WEBTOP("webtop", true, () -> new PortableUpdater(false)),
CHOCO("choco", true, () -> new ChocoUpdater()); CHOCO("choco", true, () -> new PortableUpdater(true));
private static XPipeDistributionType type; private static XPipeDistributionType type;
@ -54,7 +54,7 @@ public enum XPipeDistributionType {
} }
if (!XPipeSession.get().isNewBuildSession()) { if (!XPipeSession.get().isNewBuildSession()) {
var cached = AppCache.get("dist", String.class, () -> null); var cached = AppCache.getNonNull("dist", String.class, () -> null);
var cachedType = Arrays.stream(values()) var cachedType = Arrays.stream(values())
.filter(xPipeDistributionType -> .filter(xPipeDistributionType ->
xPipeDistributionType.getId().equals(cached)) xPipeDistributionType.getId().equals(cached))

View file

@ -1,13 +1,6 @@
package io.xpipe.app.util; package io.xpipe.app.util;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.process.ShellTtyState;
import javafx.beans.value.ObservableValue;
import java.util.Arrays; import java.util.Arrays;
@ -17,45 +10,6 @@ public class DataStoreFormatter {
return String.join(" ", Arrays.stream(elements).filter(s -> s != null).toList()); return String.join(" ", Arrays.stream(elements).filter(s -> s != null).toList());
} }
public static String formattedOsName(String osName) {
osName = osName.replaceAll("^Microsoft ", "");
var proRequired = !LicenseProvider.get().checkOsName(osName);
if (!proRequired) {
return osName;
}
return "[Pro] " + osName;
}
public static ObservableValue<String> shellInformation(StoreEntryWrapper w) {
return BindingsHelper.map(w.getPersistentState(), o -> {
if (o instanceof ShellStoreState s) {
if (s.getRunning() == null) {
return null;
}
if (s.getShellDialect() != null
&& !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
if (s.getOsName() != null) {
return formattedOsName(s.getOsName());
}
if (s.getShellDialect().equals(ShellDialects.NO_INTERACTION)) {
return null;
}
return s.getShellDialect().getDisplayName();
}
var prefix = s.getTtyState() != null && s.getTtyState() != ShellTtyState.NONE ? "[PTY] " : "";
return s.isRunning() ? prefix + formattedOsName(s.getOsName()) : "Connection failed";
}
return "?";
});
}
public static String capitalize(String name) { public static String capitalize(String name) {
if (name == null) { if (name == null) {
return null; return null;

View file

@ -3,22 +3,14 @@ package io.xpipe.app.util;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedChildStore; import io.xpipe.core.store.FixedChildStore;
import io.xpipe.core.store.ValidatableStore;
import io.xpipe.core.store.ValidationContext;
import java.util.List; import java.util.List;
public interface FixedHierarchyStore<T extends ValidationContext<?>> extends ValidatableStore<T>, DataStore { public interface FixedHierarchyStore extends DataStore {
default boolean removeLeftovers() { default boolean removeLeftovers() {
return true; return true;
} }
@Override List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren() throws Exception;
default T validate(T context) throws Exception {
listChildren(context);
return null;
}
List<? extends DataStoreEntryRef<? extends FixedChildStore>> listChildren(T context) throws Exception;
} }

View file

@ -14,9 +14,11 @@ public class HostHelper {
return p; return p;
} }
public static int findRandomOpenPortOnAllLocalInterfaces() throws IOException { public static int findRandomOpenPortOnAllLocalInterfaces() {
try (ServerSocket socket = new ServerSocket(0)) { try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort(); return socket.getLocalPort();
} catch (IOException e) {
return randomPort();
} }
} }

View file

@ -7,7 +7,7 @@ public class Hyperlinks {
public static final String DOUBLE_PROMPT = "https://docs.xpipe.io/two-step-connections"; public static final String DOUBLE_PROMPT = "https://docs.xpipe.io/two-step-connections";
public static final String AGENT_SETUP = "https://docs.xpipe.io/ssh-agent-socket"; public static final String AGENT_SETUP = "https://docs.xpipe.io/ssh-agent-socket";
public static final String GITHUB = "https://github.com/xpipe-io/xpipe"; public static final String GITHUB = "https://github.com/xpipe-io/xpipe";
public static final String GITHUB_PTB = "https://github.com/xpipe-io/xpipe"; public static final String GITHUB_PTB = "https://github.com/xpipe-io/xpipe-ptb";
public static final String PRIVACY = "https://docs.xpipe.io/privacy-policy"; public static final String PRIVACY = "https://docs.xpipe.io/privacy-policy";
public static final String EULA = "https://docs.xpipe.io/end-user-license-agreement"; public static final String EULA = "https://docs.xpipe.io/end-user-license-agreement";
public static final String SECURITY = "https://docs.xpipe.io/security"; public static final String SECURITY = "https://docs.xpipe.io/security";

View file

@ -24,7 +24,7 @@ public abstract class LicenseProvider {
public abstract LicensedFeature getFeature(String id); public abstract LicensedFeature getFeature(String id);
public abstract boolean checkOsName(String name); public abstract LicensedFeature checkOsName(String name);
public abstract void checkOsNameOrThrow(String s); public abstract void checkOsNameOrThrow(String s);

View file

@ -1,19 +1,28 @@
package io.xpipe.app.util; package io.xpipe.app.util;
import io.xpipe.app.core.AppI18n;
import javafx.beans.value.ObservableValue;
import java.util.Optional; import java.util.Optional;
public interface LicensedFeature { public interface LicensedFeature {
default Optional<String> getDescriptionSuffix() { Optional<String> getDescriptionSuffix();
if (isSupported()) {
return Optional.empty();
}
if (isPreviewSupported()) { public default ObservableValue<String> suffixObservable(ObservableValue<String> s) {
return Optional.of("Preview"); return s.map(s2 ->
} getDescriptionSuffix().map(suffix -> s2 + " (" + suffix + "+)").orElse(s2));
}
return Optional.of("Pro"); public default ObservableValue<String> suffixObservable(String key) {
return AppI18n.observable(key).map(s -> getDescriptionSuffix()
.map(suffix -> s + " (" + suffix + "+)")
.orElse(s));
}
public default String suffix(String s) {
return getDescriptionSuffix().map(suffix -> s + " (" + suffix + "+)").orElse(s);
} }
String getId(); String getId();

View file

@ -5,6 +5,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.InPlaceSecretValue;
import javafx.beans.property.*; import javafx.beans.property.*;
@ -147,6 +148,28 @@ public class OptionsBuilder {
return this; return this;
} }
public OptionsBuilder pref(Object property) {
var mapping = AppPrefs.get().getMapping(property);
var name = mapping.getKey();
name(name);
if (mapping.isRequiresRestart()) {
description(AppI18n.observable(name + "Description").map(s -> s + "\n\n" + AppI18n.get("requiresRestart")));
} else {
description(AppI18n.observable(name + "Description"));
}
if (mapping.getLicenseFeatureId() != null) {
licenseRequirement(mapping.getLicenseFeatureId());
}
return this;
}
public OptionsBuilder licenseRequirement(String featureId) {
var f = LicenseProvider.get().getFeature(featureId);
name = f.suffixObservable(name);
lastNameReference = name;
return this;
}
public OptionsBuilder check(Function<Validator, Check> c) { public OptionsBuilder check(Function<Validator, Check> c) {
lastCompHeadReference.apply(s -> c.apply(ownValidator).decorates(s.get())); lastCompHeadReference.apply(s -> c.apply(ownValidator).decorates(s.get()));
return this; return this;

View file

@ -2,14 +2,12 @@ package io.xpipe.app.util;
import io.xpipe.app.comp.base.DialogComp; import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.process.ShellTtyState; import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.ShellValidationContext;
import io.xpipe.core.store.ValidationContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -17,7 +15,7 @@ import java.util.function.BiFunction;
public class ScanAlert { public class ScanAlert {
public static void showAsync(DataStoreEntry entry, ValidationContext<?> context) { public static void showAsync(DataStoreEntry entry) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
var showForCon = entry == null var showForCon = entry == null
|| (entry.getStore() instanceof ShellStore || (entry.getStore() instanceof ShellStore
@ -25,53 +23,48 @@ public class ScanAlert {
|| shellStoreState.getTtyState() == null || shellStoreState.getTtyState() == null
|| shellStoreState.getTtyState() == ShellTtyState.NONE)); || shellStoreState.getTtyState() == ShellTtyState.NONE));
if (showForCon) { if (showForCon) {
showForShellStore(entry, (ShellValidationContext) context); showForShellStore(entry);
} }
}); });
} }
public static void showForShellStore(DataStoreEntry initial, ShellValidationContext context) { public static void showForShellStore(DataStoreEntry initial) {
show( show(initial, (DataStoreEntry entry, ShellControl sc) -> {
initial, if (!sc.canHaveSubshells()) {
(DataStoreEntry entry, ShellControl sc) -> { return null;
if (!sc.canHaveSubshells()) { }
return null;
}
if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
return null; return null;
} }
if (sc.getTtyState() != ShellTtyState.NONE) { if (sc.getTtyState() != ShellTtyState.NONE) {
return null; return null;
} }
var providers = ScanProvider.getAll(); var providers = ScanProvider.getAll();
var applicable = new ArrayList<ScanProvider.ScanOperation>(); var applicable = new ArrayList<ScanProvider.ScanOpportunity>();
for (ScanProvider scanProvider : providers) { for (ScanProvider scanProvider : providers) {
try { try {
// Previous scan operation could have exited the shell // Previous scan operation could have exited the shell
sc.start(); sc.start();
ScanProvider.ScanOperation operation = scanProvider.create(entry, sc); ScanProvider.ScanOpportunity operation = scanProvider.create(entry, sc);
if (operation != null) { if (operation != null) {
applicable.add(operation); applicable.add(operation);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
} }
return applicable; } catch (Exception ex) {
}, ErrorEvent.fromThrowable(ex).handle();
context); }
}
return applicable;
});
} }
private static void show( private static void show(
DataStoreEntry initialStore, DataStoreEntry initialStore,
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable, BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOpportunity>> applicable) {
ShellValidationContext shellValidationContext) {
DialogComp.showWindow( DialogComp.showWindow(
"scanAlertTitle", "scanAlertTitle",
stage -> new ScanDialog( stage -> new ScanDialog(stage, initialStore != null ? initialStore.ref() : null, applicable));
stage, initialStore != null ? initialStore.ref() : null, applicable, shellValidationContext));
} }
} }

View file

@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
@ -12,8 +13,6 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.ShellValidationContext;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.*; import javafx.beans.property.*;
@ -34,24 +33,21 @@ import static javafx.scene.layout.Priority.ALWAYS;
class ScanDialog extends DialogComp { class ScanDialog extends DialogComp {
private final DataStoreEntryRef<ShellStore> initialStore; private final DataStoreEntryRef<ShellStore> initialStore;
private final BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable; private final BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOpportunity>> applicable;
private final Stage window; private final Stage window;
private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry; private final ObjectProperty<DataStoreEntryRef<ShellStore>> entry;
private final ListProperty<ScanProvider.ScanOperation> selected = private final ListProperty<ScanProvider.ScanOpportunity> selected =
new SimpleListProperty<>(FXCollections.observableArrayList()); new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty busy = new SimpleBooleanProperty(); private final BooleanProperty busy = new SimpleBooleanProperty();
private ShellValidationContext shellValidationContext;
ScanDialog( ScanDialog(
Stage window, Stage window,
DataStoreEntryRef<ShellStore> entry, DataStoreEntryRef<ShellStore> entry,
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable, BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOpportunity>> applicable) {
ShellValidationContext shellValidationContext) {
this.window = window; this.window = window;
this.initialStore = entry; this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry); this.entry = new SimpleObjectProperty<>(entry);
this.applicable = applicable; this.applicable = applicable;
this.shellValidationContext = shellValidationContext;
} }
@Override @Override
@ -62,55 +58,41 @@ class ScanDialog extends DialogComp {
@Override @Override
protected void finish() { protected void finish() {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
try { if (entry.get() == null) {
if (entry.get() == null) { return;
return;
}
Platform.runLater(() -> {
window.close();
});
BooleanScope.executeExclusive(busy, () -> {
entry.get().get().setExpanded(true);
var copy = new ArrayList<>(selected);
for (var a : copy) {
// If the user decided to remove the selected entry
// while the scan is running, just return instantly
if (!DataStorage.get()
.getStoreEntriesSet()
.contains(entry.get().get())) {
return;
}
// Previous scan operation could have exited the shell
shellValidationContext.get().start();
try {
a.getScanner().run();
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
});
} finally {
if (shellValidationContext != null) {
shellValidationContext.close();
shellValidationContext = null;
}
} }
Platform.runLater(() -> {
window.close();
});
BooleanScope.executeExclusive(busy, () -> {
entry.get().get().setExpanded(true);
var copy = new ArrayList<>(selected);
for (var a : copy) {
// If the user decided to remove the selected entry
// while the scan is running, just return instantly
if (!DataStorage.get()
.getStoreEntriesSet()
.contains(entry.get().get())) {
return;
}
// Previous scan operation could have exited the shell
var sc = initialStore.getStore().getOrStartSession();
try {
a.getProvider().scan(entry.get().getEntry(), sc);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
});
}); });
} }
@Override @Override
protected void discard() { protected void discard() {}
ThreadHelper.runAsync(() -> {
if (shellValidationContext != null) {
shellValidationContext.close();
shellValidationContext = null;
}
});
}
@Override @Override
protected Comp<?> pane(Comp<?> content) { protected Comp<?> pane(Comp<?> content) {
@ -161,22 +143,8 @@ class ScanDialog extends DialogComp {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> { BooleanScope.executeExclusive(busy, () -> {
if (shellValidationContext != null) { var sc = initialStore.getStore().getOrStartSession().withoutLicenseCheck();
shellValidationContext.close(); var a = applicable.apply(entry.get().get(), sc);
shellValidationContext = null;
}
shellValidationContext = new ShellValidationContext(
newValue.getStore().control().withoutLicenseCheck().start());
// Handle window close while connection is established
if (!window.isShowing()) {
discard();
return;
}
var a = applicable.apply(entry.get().get(), shellValidationContext.get());
Platform.runLater(() -> { Platform.runLater(() -> {
if (a == null) { if (a == null) {
window.close(); window.close();
@ -186,7 +154,7 @@ class ScanDialog extends DialogComp {
selected.setAll(a.stream() selected.setAll(a.stream()
.filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled()) .filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled())
.toList()); .toList());
Function<ScanProvider.ScanOperation, String> nameFunc = (ScanProvider.ScanOperation s) -> { Function<ScanProvider.ScanOpportunity, String> nameFunc = (ScanProvider.ScanOpportunity s) -> {
var n = AppI18n.get(s.getNameKey()); var n = AppI18n.get(s.getNameKey());
if (s.getLicensedFeatureId() == null) { if (s.getLicensedFeatureId() == null) {
return n; return n;

Some files were not shown because too many files have changed in this diff Show more