mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 15:10:23 +00:00
Rework pinning
This commit is contained in:
parent
6dabe53011
commit
54fce7248f
212 changed files with 2800 additions and 1980 deletions
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
49
app/src/main/java/io/xpipe/app/browser/BrowserHomeModel.java
Normal file
49
app/src/main/java/io/xpipe/app/browser/BrowserHomeModel.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
}));
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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()))
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package io.xpipe.app.ext;
|
||||
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
|
||||
public interface ShellControlFunction {
|
||||
|
||||
ShellControl control() throws Exception;
|
||||
}
|
|
@ -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();
|
||||
}
|
65
app/src/main/java/io/xpipe/app/ext/ShellSession.java
Normal file
65
app/src/main/java/io/xpipe/app/ext/ShellSession.java
Normal 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();
|
||||
}
|
||||
}
|
74
app/src/main/java/io/xpipe/app/ext/ShellStore.java
Normal file
74
app/src/main/java/io/xpipe/app/ext/ShellStore.java
Normal 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());
|
||||
}
|
||||
}
|
13
app/src/main/java/io/xpipe/app/ext/StubShellControl.java
Normal file
13
app/src/main/java/io/xpipe/app/ext/StubShellControl.java
Normal 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 {}
|
||||
}
|
338
app/src/main/java/io/xpipe/app/ext/WrapperShellControl.java
Normal file
338
app/src/main/java/io/xpipe/app/ext/WrapperShellControl.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -11,7 +11,6 @@ public class GuiErrorHandlerBase {
|
|||
try {
|
||||
PlatformState.initPlatformOrThrow();
|
||||
AppProperties.init();
|
||||
AppState.init();
|
||||
AppExtensionManager.init(false);
|
||||
AppI18n.init();
|
||||
AppStyle.init();
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"), () -> {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
});
|
||||
})))
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) -> {
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue