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')
compileOnly 'org.hamcrest:hamcrest:3.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.0'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.0'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.3'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.3'
api 'com.vladsch.flexmark:flexmark: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 'io.sentry:sentry:7.14.0'
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.datatype', name: 'jackson-datatype-jsr310', 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.18.1"
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-javafx', version: "12.2.0"

View file

@ -20,7 +20,7 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
throw new BeaconClientException("Not a file system connection");
}
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();
return Response.builder().build();
}

View file

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

View file

@ -1,10 +1,10 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange;
import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange;
@ -18,9 +18,8 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
if (!(e.getStore() instanceof ShellStore shellStore)) {
throw new BeaconClientException("Not a shell connection");
}
try (var sc = shellStore.control().start()) {
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
}
var sc = shellStore.getOrStartSession();
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
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.BeaconShellSession;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ShellStartExchange;
import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange;
import lombok.SneakyThrows;
@ -25,7 +25,9 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
var existing = AppBeaconServer.get().getCache().getShellSessions().stream()
.filter(beaconShellSession -> beaconShellSession.getEntry().equals(e))
.findFirst();
var control = (existing.isPresent() ? existing.get().getControl() : s.control());
var control = (existing.isPresent()
? existing.get().getControl()
: s.standaloneControl().start());
control.setNonInteractive();
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() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return AppCache.getNonNull("browser-state", BrowserSavedStateImpl.class, () -> {
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().setWrapText(true))
.visible(model.getEmpty());
var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var backgroundStack = new StackComp(List.of(background))
.grow(true, true)
.styleClass("color-box")
.styleClass("gray")
.styleClass("download-background");
var binding = new DerivedObservableList<>(model.getItems(), true)
.mapped(item -> item.getBrowserEntry())

View file

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

View file

@ -130,7 +130,7 @@ public final class BrowserFileListComp extends SimpleComp {
table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region());
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.setFocusTraversable(true);
table.setSortPolicy(param -> {
@ -313,8 +313,10 @@ public final class BrowserFileListComp extends SimpleComp {
.filter(browserAction -> browserAction.getShortcut().match(event))
.findAny();
action.ifPresent(browserAction -> {
// Prevent concurrent modification by creating copy on platform thread
var selectionCopy = new ArrayList<>(selected);
ThreadHelper.runFailableAsync(() -> {
browserAction.execute(fileList.getFileSystemModel(), selected);
browserAction.execute(fileList.getFileSystemModel(), selectionCopy);
});
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.FileSystemHelper;
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.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
@ -41,7 +42,7 @@ import java.util.Optional;
import java.util.stream.Stream;
@Getter
public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore> {
public final class OpenFileSystemModel extends BrowserStoreSessionTab<FileSystemStore> {
private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList;

View file

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

View file

@ -13,13 +13,13 @@ import javafx.collections.ObservableList;
import lombok.Getter;
@Getter
public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
protected final BooleanProperty busy = new SimpleBooleanProperty();
public void closeAsync(BrowserSessionTab<?> e) {
public void closeAsync(BrowserSessionTab e) {
ThreadHelper.runAsync(() -> {
closeSync(e);
});
@ -37,7 +37,7 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
}
}
public void closeSync(BrowserSessionTab<?> e) {
public void closeSync(BrowserSessionTab e) {
e.close();
synchronized (BrowserAbstractSessionModel.this) {
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.OpenFileSystemModel;
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.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.StackComp;
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.ThreadHelper;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
@ -148,7 +148,7 @@ public class BrowserChooserComp extends DialogComp {
});
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())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.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.BrowserTransferComp;
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.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
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.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.ShellStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
@ -67,7 +69,11 @@ public class BrowserSessionComp extends SimpleComp {
var bookmarkTopBar = new BrowserBookmarkHeaderComp();
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,
action,
bookmarkTopBar.getCategory(),
@ -99,8 +105,10 @@ public class BrowserSessionComp extends SimpleComp {
var vertical =
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
var split = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> {
var leftSplit = new SimpleDoubleProperty();
var rightSplit = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, leftSplit, rightSplit);
tabs.apply(struc -> {
struc.get().setViewOrder(1);
struc.get().setPickOnBounds(false);
AnchorPane.setTopAnchor(struc.get(), 0.0);
@ -108,20 +116,54 @@ public class BrowserSessionComp extends SimpleComp {
AnchorPane.setLeftAnchor(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())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.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())
.withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
split.set(d);
})
.apply(struc -> {
leftSplit.set(d);
});
splitPane.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
struc.get().setPickOnBounds(false);
@ -140,9 +182,7 @@ public class BrowserSessionComp extends SimpleComp {
}
});
});
var r = splitPane.createRegion();
r.getStyleClass().add("browser");
return r;
splitPane.styleClass("browser");
return splitPane.createRegion();
}
}

View file

@ -1,9 +1,11 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserHomeModel;
import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferModel;
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.DataStoreEntryRef;
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.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab<?>> {
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {
public static final BrowserSessionModel DEFAULT = new BrowserSessionModel();
static {
DEFAULT.getSessionEntries().add(new BrowserHomeModel(DEFAULT));
}
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
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) {
ThreadHelper.runAsync(() -> {
@ -74,14 +148,15 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
}
ThreadHelper.runFailableAsync(() -> {
openFileSystemSync(store, path, externalBusy);
openFileSystemSync(store, path, externalBusy, true);
});
}
public OpenFileSystemModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy)
BooleanProperty externalBusy,
boolean select)
throws Exception {
OpenFileSystemModel model;
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
synchronized (BrowserSessionModel.this) {
sessionEntries.add(model);
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
if (select) {
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
}
}
}

View file

@ -1,29 +1,30 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import io.xpipe.app.storage.DataColor;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableValue;
import lombok.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 BrowserAbstractSessionModel<?> browserModel;
protected final String name;
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.entry = entry;
this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString();
this.name = name;
this.tooltip = tooltip;
}
public abstract Comp<?> comp();
@ -33,4 +34,12 @@ public abstract class BrowserSessionTab<T extends DataStore> {
public abstract void init() throws Exception;
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;
import io.xpipe.app.browser.BrowserWelcomeComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
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.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.skin.TabPaneSkin;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles;
import lombok.Getter;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
@ -41,26 +44,30 @@ public class BrowserSessionTabsComp extends SimpleComp {
private final BrowserSessionModel model;
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.leftPadding = leftPadding;
this.rightPadding = rightPadding;
this.headerHeight = new SimpleDoubleProperty();
}
public Region createSimple() {
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true));
map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries()));
map.put(
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding(
() -> {
return model.getSessionEntries().size() == 0;
},
model.getSessionEntries()));
var multi = new MultiContentComp(map);
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
return multi.createRegion();
var tabs = createTabPane();
var topBackground = Comp.hspacer().styleClass("top-spacer").createRegion();
leftPadding.subscribe(number -> {
StackPane.setMargin(topBackground, new Insets(0, 0, 0, -number.doubleValue()));
});
var stack = new StackPane(topBackground, tabs);
stack.setAlignment(Pos.TOP_CENTER);
topBackground.prefHeightProperty().bind(headerHeight);
topBackground.maxHeightProperty().bind(topBackground.prefHeightProperty());
topBackground.prefWidthProperty().bind(tabs.widthProperty());
return stack;
}
private TabPane createTabPane() {
@ -69,6 +76,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
tabs.setTabMinWidth(Region.USE_PREF_SIZE);
tabs.setTabMaxWidth(400);
tabs.setTabClosingPolicy(ALL_TABS);
tabs.setSkin(new TabPaneSkin(tabs));
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
@ -80,22 +88,31 @@ public class BrowserSessionTabsComp extends SimpleComp {
tabs.lookupAll(".tab-header-area").forEach(node -> {
node.setClip(null);
node.setPickOnBounds(false);
var r = (Region) node;
r.prefHeightProperty().bind(r.maxHeightProperty());
r.setMinHeight(Region.USE_PREF_SIZE);
});
tabs.lookupAll(".headers-region").forEach(node -> {
node.setClip(null);
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");
headerArea
.paddingProperty()
.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
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()) {
for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> {
@ -245,9 +262,28 @@ public class BrowserSessionTabsComp extends SimpleComp {
return tabs;
}
private ContextMenu createContextMenu(TabPane tabs, Tab tab) {
private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {
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"));
select.acceleratorProperty()
.bind(Bindings.createObjectBinding(
@ -272,7 +308,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab"));
close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
if (tab.isClosable()) {
tabs.getTabs().remove(tab);
}
event.consume();
});
cm.getItems().add(close);
@ -280,7 +318,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs"));
closeOthers.setOnAction(event -> {
tabs.getTabs()
.removeAll(tabs.getTabs().stream().filter(t -> t != tab).toList());
.removeAll(tabs.getTabs().stream()
.filter(t -> t != tab && t.isClosable())
.toList());
event.consume();
});
cm.getItems().add(closeOthers);
@ -290,7 +330,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) < index)
.filter(t -> tabs.getTabs().indexOf(t) < index && t.isClosable())
.toList());
event.consume();
});
@ -301,7 +341,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) > index)
.filter(t -> tabs.getTabs().indexOf(t) > index && t.isClosable())
.toList());
event.consume();
});
@ -311,7 +351,9 @@ public class BrowserSessionTabsComp extends SimpleComp {
closeAll.setAccelerator(
new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
closeAll.setOnAction(event -> {
tabs.getTabs().clear();
tabs.getTabs()
.removeAll(
tabs.getTabs().stream().filter(t -> t.isClosable()).toList());
event.consume();
});
cm.getItems().add(closeAll);
@ -319,36 +361,92 @@ public class BrowserSessionTabsComp extends SimpleComp {
return cm;
}
private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) {
private Tab createTab(TabPane tabs, BrowserSessionTab tabModel) {
var tab = new Tab();
tab.setContextMenu(createContextMenu(tabs, tab));
tab.setContextMenu(createContextMenu(tabs, tab, tabModel));
var ring = new RingProgressIndicator(0, false);
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()));
tab.setClosable(tabModel.isCloseable());
var image = model.getEntry().get().getEffectiveIconFile();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
if (tabModel.getIcon() != null) {
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()
.bind(Bindings.createObjectBinding(
() -> {
return model.getBusy().get() ? ring : logo;
},
PlatformThread.sync(model.getBusy())));
tab.setText(model.getName());
var image = tabModel.getIcon();
var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion();
Comp<?> comp = model.comp();
tab.setContent(comp.createRegion());
tab.graphicProperty()
.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();
tab.setId(id);
@ -360,18 +458,20 @@ public class BrowserSessionTabsComp extends SimpleComp {
var w = l.maxWidthProperty();
l.minWidthProperty().bind(w);
l.prefWidthProperty().bind(w);
if (!tabModel.isCloseable()) {
l.pseudoClassStateChanged(PseudoClass.getPseudoClass("static"), true);
}
var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button");
close.setPrefWidth(30);
StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container");
c.getStyleClass().add("color-box");
var color =
DataStorage.get().getEffectiveColor(model.getEntry().get());
var color = tabModel.getColor();
if (color != null) {
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(
DragEvent.DRAG_ENTERED,
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());
}
},
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();
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.function.Consumer;
public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
public class LeftSplitPaneComp extends Comp<LeftSplitPaneComp.Structure> {
private final Comp<?> left;
private final Comp<?> center;
private Double initialWidth;
private Consumer<Double> onDividerChange;
public SideSplitPaneComp(Comp<?> left, Comp<?> center) {
public LeftSplitPaneComp(Comp<?> left, Comp<?> center) {
this.left = left;
this.center = center;
}
@ -58,12 +58,12 @@ public class SideSplitPaneComp extends Comp<SideSplitPaneComp.Structure> {
return new Structure(sidebar, c, r, r.getDividers().getFirst());
}
public SideSplitPaneComp withInitialWidth(double val) {
public LeftSplitPaneComp withInitialWidth(double val) {
this.initialWidth = val;
return this;
}
public SideSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) {
public LeftSplitPaneComp withOnDividerChange(Consumer<Double> onDividerChange) {
this.onDividerChange = onDividerChange;
return this;
}

View file

@ -1,6 +1,5 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
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.StackComp;
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.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -27,9 +22,6 @@ import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
@ -50,14 +42,14 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var selectedBorder = Bindings.createObjectBinding(
() -> {
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());
var hoverBorder = Bindings.createObjectBinding(
() -> {
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());
@ -141,29 +133,6 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
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();
filler.setDisable(true);
filler.setMaxHeight(3000);

View file

@ -95,7 +95,16 @@ public class DenseStoreEntryComp extends StoreEntryComp {
nameCC.setMinWidth(100);
nameCC.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(nameCC);
var active = new StoreActiveComp(getWrapper()).createRegion();
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.setAlignment(Pos.CENTER_LEFT);
grid.addRow(0, nameBox);

View file

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

View file

@ -22,7 +22,7 @@ public class StoreCreationMenu {
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
automatically.setOnAction(event -> {
ScanAlert.showAsync(null, null);
ScanAlert.showAsync(null);
event.consume();
});
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(
"addShell", "mdi2t-text-box-multiple", DataStoreCreationCategory.SHELL, "shellEnvironment"));
menu.getItems()
.add(category("addScript", "mdi2s-script-text-outline", DataStoreCreationCategory.SCRIPT, "script"));
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null));
menu.getItems()
.add(category(
"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();
if (proRequired) {
item.setDisable(true);
item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name));
item.textProperty()
.bind(LicenseProvider.get().getFeature(p.getProFeatureId()).suffixObservable(name.getValue()));
} else {
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.MultiContentComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
@ -34,18 +36,23 @@ public class StoreEntryListComp extends SimpleComp {
StoreViewState.get().getActiveCategory().addListener((observable, oldValue, newValue) -> {
struc.get().setVvalue(0);
});
});
content.apply(struc -> {
// Reset scroll
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
struc.get().setVvalue(0);
});
// Reset scroll
StoreViewState.get().getFilterString().addListener((observable, oldValue, newValue) -> {
struc.get().setVvalue(0);
});
});
return content.styleClass("store-list-comp");
}
@Override
protected Region createSimple() {
var scriptsIntroShowing = new SimpleBooleanProperty(!AppCache.getBoolean("scriptsIntroCompleted", false));
var initialCount = 1;
var showIntro = Bindings.createBooleanBinding(
() -> {
@ -63,6 +70,46 @@ public class StoreEntryListComp extends SimpleComp {
},
StoreViewState.get().getAllEntries().getList(),
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>>();
map.put(
new StoreNotFoundComp(),
@ -73,13 +120,9 @@ public class StoreEntryListComp extends SimpleComp {
.getCurrentTopLevelSection()
.getShownChildren()
.getList())));
map.put(
createList(),
Bindings.not(Bindings.isEmpty(StoreViewState.get()
.getCurrentTopLevelSection()
.getShownChildren()
.getList())));
map.put(createList(), showList);
map.put(new StoreIntroComp(), showIntro);
map.put(new StoreScriptsIntroComp(scriptsIntroShowing), showScriptsIntro);
return new MultiContentComp(map).createRegion();
}

View file

@ -83,13 +83,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
return inRootCategory && showProvider;
},
StoreViewState.get().getActiveCategory());
var shownList = all.filtered(
storeEntryWrapper -> {
return storeEntryWrapper.matchesFilter(
StoreViewState.get().getFilterString().getValue());
},
StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList.getList(), all.getList());
var count = new CountComp<>(all.getList(), all.getList());
var c = count.createRegion();
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.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.SingletonSessionStore;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
@ -44,6 +45,7 @@ public class StoreEntryWrapper {
private final Property<StoreNotes> notes;
private final Property<String> customIcon = new SimpleObjectProperty<>();
private final Property<String> iconFile = new SimpleObjectProperty<>();
private final BooleanProperty sessionActive = new SimpleBooleanProperty();
public StoreEntryWrapper(DataStoreEntry 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
if (StoreViewState.get() == null) {
return;
@ -147,6 +157,7 @@ public class StoreEntryWrapper {
busy.setValue(entry.getBusyCounter().get() != 0);
deletable.setValue(entry.getConfiguration().isDeletable()
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore<?> ss && ss.isSessionRunning());
category.setValue(StoreViewState.get()
.getCategoryWrapper(DataStorage.get()
@ -220,7 +231,7 @@ public class StoreEntryWrapper {
}
public void refreshChildren() {
var hasChildren = DataStorage.get().refreshChildren(entry, null);
var hasChildren = DataStorage.get().refreshChildren(entry);
PlatformThread.runLaterIfNeeded(() -> {
expanded.set(hasChildren);
});

View file

@ -39,7 +39,7 @@ public class StoreIntroComp extends SimpleComp {
var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
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);
var scanPane = new StackPane(scanButton);
scanPane.setAlignment(Pos.CENTER);

View file

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

View file

@ -27,7 +27,6 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider;
boolean staticDisplay;
public List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream()
@ -65,9 +64,7 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
return cellFactory.get();
});
cb.setButtonCell(cellFactory.get());
var l = getProviders().stream()
.filter(p -> p.getCreationCategory() != null || staticDisplay)
.toList();
var l = getProviders();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) {
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("bar")
.styleClass("filler-bar")
.minHeight(10)
.vgrow()));
sideBar.apply(struc -> struc.get().setFillWidth(true));
sideBar.styleClass("sidebar");

View file

@ -126,7 +126,7 @@ public class StoreViewState {
activeCategory.addListener((observable, oldValue, newValue) -> {
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()
.filter(storeCategoryWrapper ->
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.core.util.JacksonMapper;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
public class AppCache {
public static <T> Optional<T> getIfPresent(String key, Class<T> type) {
return Optional.ofNullable(get(key, type, () -> null));
}
private static Path getBasePath() {
return AppProperties.get().getDataDir().resolve("cache");
}
@Getter
@Setter
private static Path basePath;
private static Path getPath(String key) {
var name = key + ".cache";
@ -47,7 +44,33 @@ public class AppCache {
}
@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);
if (Files.exists(path)) {
try {
@ -65,6 +88,25 @@ public class AppCache {
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) {
var path = getPath(key);
@ -79,12 +121,4 @@ public class AppCache {
.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() {
for (var ext : List.of("jdbc", "proc", "uacc")) {
for (var ext : List.of("proc", "uacc")) {
var extension = findAndParseExtension(ext, baseLayer)
.orElseThrow(() -> ExtensionException.corrupt("Missing module " + ext));
loadedExtensions.add(extension);

View file

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

View file

@ -48,7 +48,7 @@ public class AppLayoutModel {
}
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);
}

View file

@ -44,6 +44,8 @@ public class AppProperties {
boolean locatorVersionCheck;
boolean isTest;
boolean autoAcceptEula;
UUID uuid;
boolean initialLaunch;
public AppProperties() {
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"))
.map(Boolean::parseBoolean)
.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() {

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 {
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 String FONT_CONTENTS = "";
@ -33,6 +34,9 @@ public class AppStyle {
AppPrefs.get().useSystemFont().addListener((c, o, 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) {
@ -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) {
STYLESHEET_CONTENTS.clear();
THEME_SPECIFIC_STYLESHEET_CONTENTS.clear();
FONT_CONTENTS = "";
init();
@ -107,7 +132,7 @@ public class AppStyle {
if (AppPrefs.get() != null) {
var t = AppPrefs.get().theme.get();
if (t != null) {
scene.getStylesheets().addAll(t.getAdditionalStylesheets());
scene.getStylesheets().add(THEME_SPECIFIC_STYLESHEET_CONTENTS.get(t));
}
}
TrackEvent.debug("Added stylesheets for scene");

View file

@ -97,7 +97,10 @@ public class AppTheme {
}
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();
}
@ -237,7 +240,7 @@ public class AppTheme {
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 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
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.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppState;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.window.AppWindowHelper;
@ -35,7 +34,7 @@ public class AppAvCheck {
public static void check() throws Throwable {
// 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;
}

View file

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

View file

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

View file

@ -231,7 +231,7 @@ public class AppMainWindow {
return null;
}
WindowState state = AppCache.get("windowState", WindowState.class, () -> null);
WindowState state = AppCache.getNonNull("windowState", WindowState.class, () -> null);
if (state == 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.store.DataStore;
import io.xpipe.core.store.NetworkTunnelStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.util.JacksonizedValue;
@ -19,18 +18,22 @@ public class LocalStore extends JacksonizedValue
return ShellStoreState.class;
}
@Override
public ShellControl parentControl() {
var pc = ProcessControlProvider.get().createLocalProcessControl(true);
pc.withSourceStore(this);
pc.withShellStateInit(this);
pc.withShellStateFail(this);
return pc;
public ShellControl control(ShellControl parent) {
return parent;
}
@Override
public ShellControl control(ShellControl parent) {
return parent;
public ShellControlFunction shellFunction() {
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

View file

@ -6,5 +6,5 @@ import javafx.beans.property.Property;
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.core.process.ShellControl;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.ModuleLayerLoader;
import lombok.AllArgsConstructor;
@ -21,31 +20,34 @@ public abstract class ScanProvider {
return ALL;
}
public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception {
public ScanOpportunity create(DataStoreEntry entry, ShellControl sc) throws Exception {
return null;
}
public abstract void scan(DataStoreEntry entry, ShellControl sc) throws Throwable;
@Value
@AllArgsConstructor
public static class ScanOperation {
public class ScanOpportunity {
String nameKey;
boolean disabled;
boolean defaultSelected;
FailableRunnable<Throwable> scanner;
String licenseFeatureId;
public ScanOperation(
String nameKey, boolean disabled, boolean defaultSelected, FailableRunnable<Throwable> scanner) {
public ScanOpportunity(String nameKey, boolean disabled, boolean defaultSelected) {
this.nameKey = nameKey;
this.disabled = disabled;
this.defaultSelected = defaultSelected;
this.scanner = scanner;
this.licenseFeatureId = null;
}
public String getLicensedFeatureId() {
return licenseFeatureId;
}
public ScanProvider getProvider() {
return ScanProvider.this;
}
}
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));
}
public Comp<S> minHeight(double width) {
return apply(struc -> struc.get().setMinHeight(width));
public Comp<S> minHeight(double height) {
return apply(struc -> struc.get().setMinHeight(height));
}
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.AppI18n;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
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.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;

View file

@ -11,7 +11,6 @@ public class GuiErrorHandlerBase {
try {
PlatformState.initPlatformOrThrow();
AppProperties.init();
AppState.init();
AppExtensionManager.init(false);
AppI18n.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.AppProperties;
import io.xpipe.app.core.AppState;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
@ -145,6 +144,11 @@ public class SentryErrorHandler implements ErrorHandler {
AppPrefs.get() != null
? AppPrefs.get().automaticallyUpdate().getValue().toString()
: "unknown");
s.setTag(
"securityUpdatesEnabled",
AppPrefs.get() != null
? AppPrefs.get().checkForSecurityUpdates().getValue().toString()
: "unknown");
s.setTag("initError", String.valueOf(OperationMode.isInStartup()));
s.setTag(
"developerMode",
@ -177,11 +181,7 @@ public class SentryErrorHandler implements ErrorHandler {
}
var user = new User();
user.setId(AppState.get().getUserId().toString());
if (ee.isShouldSendDiagnostics()) {
user.setEmail(AppState.get().getUserEmail());
user.setUsername(AppState.get().getUserName());
}
user.setId(AppProperties.get().getUuid().toString());
s.setUser(user);
}
@ -189,7 +189,6 @@ public class SentryErrorHandler implements ErrorHandler {
// Assume that this object is wrapped by a synchronous error handler
if (!init) {
AppProperties.init();
AppState.init();
if (AppProperties.get().getSentryUrl() != null) {
Sentry.init(options -> {
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.mode.OperationMode;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ThreadHelper;
@ -40,7 +41,6 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa
private void handleGui(ErrorEvent event) {
try {
AppProperties.init();
AppState.init();
AppExtensionManager.init(false);
AppI18n.init();
AppStyle.init();
@ -74,7 +74,7 @@ public class TerminalErrorHandler extends GuiErrorHandlerBase implements ErrorHa
}
try {
var rel = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck();
var rel = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck(false, !AppPrefs.get().automaticallyUpdate().get());
if (rel != null && rel.isUpdate()) {
var update = AppWindowHelper.showBlockingAlert(alert -> {
alert.setAlertType(Alert.AlertType.INFORMATION);

View file

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

View file

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

View file

@ -1,17 +1,24 @@
package io.xpipe.app.prefs;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.Region;
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 {
@ -33,7 +40,17 @@ public class AppPrefsSidebarComp extends SimpleComp {
})
.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");
vbox.apply(struc -> {
AppPrefs.get().getSelectedCategory().subscribe(val -> {

View file

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

View file

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

View file

@ -22,13 +22,10 @@ public class LoggingCategory extends AppPrefsCategory {
@Override
protected Comp<?> create() {
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()
.addTitle(title)
.addTitle("sessionLogging")
.sub(new OptionsBuilder()
.nameAndDescription("enableTerminalLogging")
.pref(prefs.enableTerminalLogging)
.addToggle(prefs.enableTerminalLogging)
.nameAndDescription("terminalLoggingDirectory")
.addComp(new ButtonComp(AppI18n.observable("openSessionLogs"), () -> {

View file

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

View file

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

View file

@ -2,8 +2,8 @@ package io.xpipe.app.prefs;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.ChoiceComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
@ -41,7 +41,10 @@ public class TerminalCategory extends AppPrefsCategory {
var term = AppPrefs.get().terminalType().getValue();
if (term != null) {
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() {
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent();
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(false, false);
UpdateAvailableAlert.showIfNeeded();
}
private void refresh() {
ThreadHelper.runFailableAsync(() -> {
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck();
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheck(false, false);
XPipeDistributionType.get().getUpdateHandler().prepareUpdate();
});
}

View file

@ -27,15 +27,6 @@ public class VaultCategory extends AppPrefsCategory {
public Comp<?> create() {
var prefs = AppPrefs.get();
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());
encryptVault.addListener((observable, oldValue, newValue) -> {

View file

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

View file

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

View file

@ -516,47 +516,18 @@ public class DataStoreEntry extends StorageElement {
}
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) {
return null;
return;
}
if (!(store instanceof ValidatableStore<?> l)) {
return null;
if (!(store instanceof ValidatableStore l)) {
return;
}
try {
store.checkComplete();
incrementBusyCounter();
ValidationContext<T> context = existingContext != null
? (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;
}
l.validate();
} finally {
decrementBusyCounter();
}

View file

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

View file

@ -127,7 +127,7 @@ public interface KittyTerminalType extends ExternalTerminalType {
var socket = getSocket();
try (var sc = LocalShell.getShell().start()) {
if (sc.executeSimpleBooleanCommand(
"test -w " + sc.getShellDialect().fileArgument(socket))) {
"/usr/bin/test -w " + sc.getShellDialect().fileArgument(socket))) {
return false;
}
@ -174,7 +174,7 @@ public interface KittyTerminalType extends ExternalTerminalType {
var socket = getSocket();
try (var sc = LocalShell.getShell().start()) {
if (sc.executeSimpleBooleanCommand(
"test -w " + sc.getShellDialect().fileArgument(socket))) {
"/usr/bin/test -w " + sc.getShellDialect().fileArgument(socket))) {
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
// So work around it by just passing a script file if possible
if (ShellDialects.isPowershell(configuration.getScriptDialect())) {
var usesPowershell = ShellDialects.isPowershell(ProcessControlProvider.get().getEffectiveLocalDialect());
var usesPowershell =
ShellDialects.isPowershell(ProcessControlProvider.get().getEffectiveLocalDialect());
if (usesPowershell) {
// We can't work around it in this case, so let's just hope that there's no elevation configured
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
// In this case we can actually work around the problem
cmd.addFile(shellControl -> {
var script = ScriptHelper.createExecScript(shellControl, configuration.getDialectLaunchCommand().buildFull(shellControl));
var script = ScriptHelper.createExecScript(
shellControl,
configuration.getDialectLaunchCommand().buildFull(shellControl));
return script.toString();
});
}

View file

@ -1,10 +1,11 @@
package io.xpipe.app.test;
import io.xpipe.core.util.FailableSupplier;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Named;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class TestModule<V> {
@ -12,6 +13,7 @@ public abstract class TestModule<V> {
private static final Map<Class<?>, Map<String, ?>> values = new LinkedHashMap<>();
@SuppressWarnings({"unchecked", "rawtypes"})
@SneakyThrows
public static <T> Map<String, T> get(Class<T> c, Module module, String... classes) {
if (!values.containsKey(c)) {
List<Class<?>> loadedClasses = Arrays.stream(classes)
@ -31,8 +33,13 @@ public abstract class TestModule<V> {
});
}
return (Map<String, T>) values.get(c).entrySet().stream()
.collect(Collectors.toMap(o -> o.getKey(), o -> ((Supplier<?>) o.getValue()).get()));
Map<String, Object> map = new HashMap<>();
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) {
@ -43,7 +50,7 @@ public abstract class TestModule<V> {
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();
}

View file

@ -1,9 +1,11 @@
package io.xpipe.app.update;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.HttpHelper;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.JacksonMapper;
import org.apache.commons.io.FileUtils;
@ -15,6 +17,9 @@ import org.kohsuke.github.authorization.AuthorizationProvider;
import java.io.IOException;
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.file.Files;
import java.nio.file.Path;
@ -112,30 +117,39 @@ public class AppDownloads {
}
}
public static Optional<GHRelease> getTopReleaseIncludingPreRelease() throws IOException {
var repo = getRepository();
return Optional.ofNullable(repo.listReleases().iterator().next());
private static String queryLatestVersion(boolean first, boolean securityOnly) throws Exception {
var req = JsonNodeFactory.instance.objectNode();
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 {
var repo = getRepository();
return Optional.ofNullable(repo.getLatestRelease());
}
public static Optional<GHRelease> getLatestSuitableRelease() throws IOException {
public static Optional<GHRelease> queryLatestRelease(boolean first, boolean securityOnly) throws Exception {
try {
var preIncluding = getTopReleaseIncludingPreRelease();
// If we are currently running a prerelease, always return this as the suitable release!
if (preIncluding.isPresent()
&& preIncluding.get().isPrerelease()
&& AppProperties.get()
.getVersion()
.equals(preIncluding.get().getTagName())) {
return preIncluding;
}
return getMarkedLatestRelease();
} catch (IOException e) {
var ver = queryLatestVersion(first, securityOnly);
var repo = getRepository();
var rel = repo.getReleaseByTagName(ver);
return Optional.ofNullable(rel);
} catch (Exception 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.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.LocalShell;
@ -79,7 +78,6 @@ public class AppInstaller {
@Override
public void installLocal(Path file) throws Exception {
var shellProcessControl = new LocalStore().control().start();
var exec = (AppProperties.get().isDevelopmentEnvironment()
? Path.of(XPipeInstallation.getLocalDefaultInstallationBasePath())
: XPipeInstallation.getCurrentInstallationBasePath())
@ -98,7 +96,7 @@ public class AppInstaller {
+ ScriptHelper.createLocalExecScript(command) + "`\"\"";
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 {
var rel = AppDownloads.getLatestSuitableRelease();
public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {
var rel = AppDownloads.queryLatestRelease(first, securityOnly);
event("Determined latest suitable release "
+ rel.map(GHRelease::getName).orElse(null));

View file

@ -29,8 +29,8 @@ public class PortableUpdater extends UpdateHandler {
.createRegion();
}
public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception {
var rel = AppDownloads.getLatestSuitableRelease();
public synchronized AvailableRelease refreshUpdateCheckImpl(boolean first, boolean securityOnly) throws Exception {
var rel = AppDownloads.queryLatestRelease(first, securityOnly);
event("Determined latest suitable release "
+ 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.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.Hyperlinks;
import javafx.event.ActionEvent;
@ -22,7 +23,7 @@ public class UpdateAvailableAlert {
}
// Check whether we still have the latest version prepared
uh.refreshUpdateCheckSilent();
uh.refreshUpdateCheckSilent(false, !AppPrefs.get().automaticallyUpdate().get());
if (uh.getPreparedUpdate().getValue() == null) {
return;
}

View file

@ -36,7 +36,7 @@ public abstract class UpdateHandler {
protected final boolean updateSucceeded;
protected UpdateHandler(boolean startBackgroundThread) {
performedUpdate = AppCache.get("performedUpdate", PerformedUpdate.class, () -> null);
performedUpdate = AppCache.getNonNull("performedUpdate", PerformedUpdate.class, () -> null);
var hasUpdated = performedUpdate != null;
event("Was updated is " + hasUpdated);
if (hasUpdated) {
@ -48,7 +48,7 @@ public abstract class UpdateHandler {
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
if (preparedUpdate.getValue() != null
@ -99,12 +99,14 @@ public abstract class UpdateHandler {
private void startBackgroundUpdater() {
ThreadHelper.createPlatformThread("updater", true, () -> {
var checked = false;
ThreadHelper.sleep(Duration.ofMinutes(5).toMillis());
event("Starting background updater thread");
while (true) {
if (AppPrefs.get().automaticallyUpdate().get()) {
if (AppPrefs.get().automaticallyUpdate().get() || AppPrefs.get().checkForSecurityUpdates().get()) {
event("Performing background update");
refreshUpdateCheckSilent();
refreshUpdateCheckSilent(!checked, !AppPrefs.get().automaticallyUpdate().get());
checked = true;
prepareUpdate();
}
@ -134,17 +136,9 @@ public abstract class UpdateHandler {
return false;
}
public final void prepareUpdateAsync() {
ThreadHelper.runAsync(() -> prepareUpdate());
}
public final void refreshUpdateCheckAsync() {
ThreadHelper.runAsync(() -> refreshUpdateCheckSilent());
}
public final AvailableRelease refreshUpdateCheckSilent() {
public final AvailableRelease refreshUpdateCheckSilent(boolean first, boolean securityOnly) {
try {
return refreshUpdateCheck();
return refreshUpdateCheck(first, securityOnly);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).discard().handle();
return null;
@ -214,7 +208,7 @@ public abstract class UpdateHandler {
// 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
var available = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent();
var available = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(false, !AppPrefs.get().automaticallyUpdate().get());
if (preparedUpdate.getValue() == null) {
return;
}
@ -233,17 +227,17 @@ public abstract class UpdateHandler {
throw new UnsupportedOperationException();
}
public final AvailableRelease refreshUpdateCheck() throws Exception {
public final AvailableRelease refreshUpdateCheck(boolean first, boolean securityOnly) throws Exception {
if (busy.getValue()) {
return lastUpdateCheckResult.getValue();
}
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
@Builder

View file

@ -24,7 +24,7 @@ public enum XPipeDistributionType {
NATIVE_INSTALLATION("install", true, () -> new GitHubUpdater(true)),
HOMEBREW("homebrew", true, () -> new HomebrewUpdater()),
WEBTOP("webtop", true, () -> new PortableUpdater(false)),
CHOCO("choco", true, () -> new ChocoUpdater());
CHOCO("choco", true, () -> new PortableUpdater(true));
private static XPipeDistributionType type;
@ -54,7 +54,7 @@ public enum XPipeDistributionType {
}
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())
.filter(xPipeDistributionType ->
xPipeDistributionType.getId().equals(cached))

View file

@ -1,13 +1,6 @@
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.core.process.ShellDialects;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.process.ShellTtyState;
import javafx.beans.value.ObservableValue;
import java.util.Arrays;
@ -17,45 +10,6 @@ public class DataStoreFormatter {
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) {
if (name == null) {
return null;

View file

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

View file

@ -14,9 +14,11 @@ public class HostHelper {
return p;
}
public static int findRandomOpenPortOnAllLocalInterfaces() throws IOException {
public static int findRandomOpenPortOnAllLocalInterfaces() {
try (ServerSocket socket = new ServerSocket(0)) {
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 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_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 EULA = "https://docs.xpipe.io/end-user-license-agreement";
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 boolean checkOsName(String name);
public abstract LicensedFeature checkOsName(String name);
public abstract void checkOsNameOrThrow(String s);

View file

@ -1,19 +1,28 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppI18n;
import javafx.beans.value.ObservableValue;
import java.util.Optional;
public interface LicensedFeature {
default Optional<String> getDescriptionSuffix() {
if (isSupported()) {
return Optional.empty();
}
Optional<String> getDescriptionSuffix();
if (isPreviewSupported()) {
return Optional.of("Preview");
}
public default ObservableValue<String> suffixObservable(ObservableValue<String> s) {
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();

View file

@ -5,6 +5,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.util.InPlaceSecretValue;
import javafx.beans.property.*;
@ -147,6 +148,28 @@ public class OptionsBuilder {
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) {
lastCompHeadReference.apply(s -> c.apply(ownValidator).decorates(s.get()));
return this;

View file

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

View file

@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp;
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.DataStoreEntryRef;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.ShellValidationContext;
import javafx.application.Platform;
import javafx.beans.property.*;
@ -34,24 +33,21 @@ import static javafx.scene.layout.Priority.ALWAYS;
class ScanDialog extends DialogComp {
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 ObjectProperty<DataStoreEntryRef<ShellStore>> entry;
private final ListProperty<ScanProvider.ScanOperation> selected =
private final ListProperty<ScanProvider.ScanOpportunity> selected =
new SimpleListProperty<>(FXCollections.observableArrayList());
private final BooleanProperty busy = new SimpleBooleanProperty();
private ShellValidationContext shellValidationContext;
ScanDialog(
Stage window,
DataStoreEntryRef<ShellStore> entry,
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOperation>> applicable,
ShellValidationContext shellValidationContext) {
BiFunction<DataStoreEntry, ShellControl, List<ScanProvider.ScanOpportunity>> applicable) {
this.window = window;
this.initialStore = entry;
this.entry = new SimpleObjectProperty<>(entry);
this.applicable = applicable;
this.shellValidationContext = shellValidationContext;
}
@Override
@ -62,55 +58,41 @@ class ScanDialog extends DialogComp {
@Override
protected void finish() {
ThreadHelper.runFailableAsync(() -> {
try {
if (entry.get() == null) {
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;
}
if (entry.get() == null) {
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
var sc = initialStore.getStore().getOrStartSession();
try {
a.getProvider().scan(entry.get().getEntry(), sc);
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
});
});
}
@Override
protected void discard() {
ThreadHelper.runAsync(() -> {
if (shellValidationContext != null) {
shellValidationContext.close();
shellValidationContext = null;
}
});
}
protected void discard() {}
@Override
protected Comp<?> pane(Comp<?> content) {
@ -161,22 +143,8 @@ class ScanDialog extends DialogComp {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (shellValidationContext != null) {
shellValidationContext.close();
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());
var sc = initialStore.getStore().getOrStartSession().withoutLicenseCheck();
var a = applicable.apply(entry.get().get(), sc);
Platform.runLater(() -> {
if (a == null) {
window.close();
@ -186,7 +154,7 @@ class ScanDialog extends DialogComp {
selected.setAll(a.stream()
.filter(scanOperation -> scanOperation.isDefaultSelected() && !scanOperation.isDisabled())
.toList());
Function<ScanProvider.ScanOperation, String> nameFunc = (ScanProvider.ScanOperation s) -> {
Function<ScanProvider.ScanOpportunity, String> nameFunc = (ScanProvider.ScanOpportunity s) -> {
var n = AppI18n.get(s.getNameKey());
if (s.getLicensedFeatureId() == null) {
return n;

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