Rework browser intro and about pages

This commit is contained in:
crschnick 2023-06-15 11:54:14 +00:00
parent c2ff618698
commit 26c3c16872
27 changed files with 698 additions and 200 deletions

View file

@ -108,6 +108,7 @@ List<String> jvmRunArgs = [
"--add-opens", "com.dustinredmond.fxtrayicon/com.dustinredmond.fxtrayicon=io.xpipe.app",
"--add-opens", "net.synedra.validatorfx/net.synedra.validatorfx=io.xpipe.app",
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.view=io.xpipe.app',
"--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.model=io.xpipe.app',
"-Xmx8g",
"-Dio.xpipe.app.arch=$rootProject.arch",
"--enable-preview",

View file

@ -98,6 +98,12 @@ final class BrowserBookmarkList extends SimpleComp {
private final Node imageView = new PrettyImageComp(img, 20, 20).createRegion();
private final BooleanProperty busy = new SimpleBooleanProperty(false);
@Override
protected double computePrefWidth(double height) {
// This makes the cell always properly cut of any overflow of text
return 1;
}
private StoreCell(TreeView<?> t) {
disableProperty().bind(busy);
setAccessibleRole(AccessibleRole.BUTTON);

View file

@ -6,7 +6,9 @@ import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.icon.DirectoryType;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.browser.icon.FileType;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.GrowAugment;
@ -30,6 +32,7 @@ import javafx.scene.input.DragEvent;
import javafx.scene.layout.*;
import java.util.HashMap;
import java.util.Map;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
@ -105,13 +108,14 @@ public class BrowserComp extends SimpleComp {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(s.getRawFileEntry().getPath());
field.setEditable(false);
field.setPrefWidth(400);
return field;
})
.toList());
.map(s -> {
var field =
new TextField(s.getRawFileEntry().getPath());
field.setEditable(false);
field.setPrefWidth(400);
return field;
})
.toList());
});
});
var spacer = new Spacer(Orientation.HORIZONTAL);
@ -130,9 +134,21 @@ public class BrowserComp extends SimpleComp {
}
private Node createTabs() {
var stack = new StackPane();
var tabs = createTabPane();
stack.getChildren().add(tabs);
var multi = new MultiContentComp(Map.of(
Comp.of(() -> createTabPane()),
Bindings.isNotEmpty(model.getOpenFileSystems()),
new BrowserWelcomeComp(model),
Bindings.isEmpty(model.getOpenFileSystems())));
return multi.createRegion();
}
private TabPane createTabPane() {
var tabs = new TabPane();
tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE);
tabs.setTabClosingPolicy(ALL_TABS);
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
var map = new HashMap<OpenFileSystemModel, Tab>();
@ -217,16 +233,6 @@ public class BrowserComp extends SimpleComp {
}
}
});
return stack;
}
private TabPane createTabPane() {
var tabs = new TabPane();
tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE);
tabs.setTabClosingPolicy(ALL_TABS);
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
return tabs;
}
@ -241,8 +247,7 @@ public class BrowserComp extends SimpleComp {
.bind(Bindings.createDoubleBinding(
() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy())));
var image = DataStoreProviders.byStore(model.getStore())
.getDisplayIconFileName(model.getStore());
var image = DataStoreProviders.byStore(model.getStore()).getDisplayIconFileName(model.getStore());
var logo = new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion();
var label = new Label(model.getName());

View file

@ -0,0 +1,28 @@
package io.xpipe.app.browser;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import java.time.LocalDateTime;
public class BrowserGreetingComp extends SimpleComp {
@Override
protected Region createSimple() {
var ldt = LocalDateTime.now();
var hour = ldt.getHour();
String text;
if (hour > 18 || hour < 5) {
text = "Good evening";
} else if (hour < 12) {
text = "Good morning";
} else {
text = "Good afternoon";
}
var r = new Label(text);
AppFont.setSize(r, 12);
return r;
}
}

View file

@ -1,6 +1,7 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileStore;
@ -14,9 +15,7 @@ import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.function.Consumer;
@Getter
@ -68,6 +67,16 @@ public class BrowserModel {
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel();
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
public void reset() {
var map = new LinkedHashMap<UUID, String>();
openFileSystems.forEach(model -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(model.getStore());
storageEntry.ifPresent(entry -> map.put(entry.getUuid(), model.getCurrentPath().get()));
});
var state = new BrowserSavedState(map);
state.save();
}
public void finishChooser() {
if (!getMode().isChooser()) {
throw new IllegalStateException();

View file

@ -0,0 +1,54 @@
package io.xpipe.app.browser;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.store.FileSystemStore;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.UUID;
@Getter
public class BrowserSavedState {
static BrowserSavedState load() {
var state = AppCache.get("browser-state", BrowserSavedState.class, () -> {
return new BrowserSavedState();
});
return state;
}
@Value
@Jacksonized
@Builder
public static class RecentEntry {
String directory;
Instant time;
}
@NonNull
private final LinkedHashMap<UUID, String> lastSystems;
public BrowserSavedState() {
lastSystems = new LinkedHashMap<>();
}
public BrowserSavedState(@NonNull LinkedHashMap<UUID, String> lastSystems) {
this.lastSystems = lastSystems;
}
public void save() {
AppCache.update("browser-state", this);
}
public void open(FileSystemStore store) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(store);
storageEntry.ifPresent(entry -> lastSystems.put(entry.getUuid(), null));
}
}

View file

@ -0,0 +1,72 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.storage.DataStorage;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
public class BrowserWelcomeComp extends SimpleComp {
private final BrowserModel model;
public BrowserWelcomeComp(BrowserModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
var state = BrowserSavedState.load();
var showList = state.getLastSystems().size() > 1
|| (state.getLastSystems().size() == 1
&& state.getLastSystems().values().stream().allMatch(s -> s != null));
var welcome = new BrowserGreetingComp().createSimple();
var vbox = new VBox(welcome);
vbox.setPadding(new Insets(40, 40, 40, 50));
vbox.setSpacing(18);
if (!showList) {
return vbox;
}
var header = new Label("Last time you were connected to the following systems:");
AppFont.header(header);
vbox.getChildren().add(header);
var storeList = new VBox();
storeList.setPadding(new Insets(0, 0, 0, 10));
storeList.setSpacing(8);
state.getLastSystems().forEach((uuid, s) -> {
var entry = DataStorage.get().getStoreEntry(uuid);
if (entry.isEmpty()) {
return;
}
var graphic =
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = new PrettyImageComp(new SimpleStringProperty(graphic), 24, 24);
var l = new Label(entry.get().getName() + (s != null ? ": " + s : ""), view.createRegion());
l.setGraphicTextGap(10);
storeList.getChildren().add(l);
});
vbox.getChildren().add(storeList);
vbox.getChildren().add(new Spacer(20));
var restoreLabel = new Label("Do you want to restore these sessions?");
AppFont.header(restoreLabel);
vbox.getChildren().add(restoreLabel);
var restoreButton = new Button("Restore sessions");
vbox.getChildren().add(restoreButton);
return vbox;
}
}

View file

@ -2,7 +2,6 @@ package io.xpipe.app.comp;
import io.xpipe.app.browser.BrowserComp;
import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.about.AboutTabComp;
import io.xpipe.app.comp.base.SideMenuBarComp;
import io.xpipe.app.comp.storage.store.StoreLayoutComp;
import io.xpipe.app.core.*;
@ -48,11 +47,10 @@ public class AppLayoutComp extends Comp<CompStructure<BorderPane>> {
new SideMenuBarComp.Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("data"), "mdsal-dvr", new SourceCollectionLayoutComp()),
new SideMenuBarComp.Entry(
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this)),
AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new PrefsComp(this))));
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
// StorageLayoutComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()),
new SideMenuBarComp.Entry(AppI18n.observable("about"), "mdi2p-package-variant", new AboutTabComp())));
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp())
if (AppProperties.get().isDeveloperMode()) {
l.add(new SideMenuBarComp.Entry(
AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));

View file

@ -1,77 +0,0 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.util.DynamicOptionsBuilder;
import io.xpipe.app.util.Hyperlinks;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class AboutTabComp extends Comp<CompStructure<?>> {
private Region createDepsList() {
var deps = new ThirdPartyDependencyListComp().createRegion();
return deps;
}
private Comp<?> hyperlink(String link) {
return Comp.of(() -> {
var hl = new Hyperlink(link);
hl.setOnAction(e -> {
Hyperlinks.open(link);
});
hl.setMaxWidth(450);
return hl;
});
}
private Comp<?> createLinks() {
return new DynamicOptionsBuilder(false)
.addTitle("links")
//.addComp(AppI18n.observable("documentation"), hyperlink(Hyperlinks.DOCUMENTATION), null)
.addComp(AppI18n.observable("securityPolicy"), hyperlink(Hyperlinks.SECURITY), null)
.addComp(AppI18n.observable("privacy"), hyperlink(Hyperlinks.PRIVACY), null)
.addComp(AppI18n.observable("discord"), hyperlink(Hyperlinks.DISCORD), null)
.addComp(AppI18n.observable("slack"), hyperlink(Hyperlinks.SLACK), null)
.addComp(AppI18n.observable("github"), hyperlink(Hyperlinks.GITHUB), null)
.buildComp();
}
private Region createThirdPartyDeps() {
var label = new Label(AppI18n.get("openSourceNotices"), new FontIcon("mdi2o-open-source-initiative"));
label.getStyleClass().add("open-source-header");
var list = createDepsList();
var box = new VBox(label, list);
box.getStyleClass().add("open-source-notices");
return box;
}
@Override
public CompStructure<?> createBase() {
var props = new PropertiesComp();
var update = new UpdateCheckComp();
var box = new VerticalComp(List.of(props, update, createLinks(), new BrowseDirectoryComp()))
.apply(s -> s.get().setFillWidth(true))
.styleClass("information");
return Comp.derive(box, boxS -> {
var bp = new SplitPane();
bp.getItems().add(boxS);
var deps = createThirdPartyDeps();
bp.getItems().add(createThirdPartyDeps());
deps.prefWidthProperty().bind(bp.widthProperty().divide(2));
boxS.prefWidthProperty().bind(bp.widthProperty().divide(2));
bp.getStyleClass().add("about-tab");
return bp;
})
.createStructure();
}
}

View file

@ -1,71 +0,0 @@
package io.xpipe.app.comp.about;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.*;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.util.XPipeInstallation;
import javafx.scene.layout.Region;
public class BrowseDirectoryComp extends SimpleComp {
@Override
protected Region createSimple() {
var b = new DynamicOptionsBuilder(false)
.addComp(
"issueReporter",
new ButtonComp(AppI18n.observable("reportIssue"), () -> {
var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory());
}
UserReportComp.show(event.build());
}),
null)
.addComp(
"logFile",
new ButtonComp(AppI18n.observable("openCurrentLogFile"), () -> {
FileOpener.openInTextEditor(AppLogs.get()
.getSessionLogsDirectory()
.resolve("xpipe.log")
.toString());
}),
null);
if (AppPrefs.get().developerMode().getValue()) {
b.addComp(
"launchDebugMode",
new ButtonComp(AppI18n.observable("launchDebugMode"), () -> {
OperationMode.executeAfterShutdown(() -> {
try (var sc = ShellStore.createLocal().control().start()) {
var script = FileNames.join(
XPipeInstallation.getCurrentInstallationBasePath()
.toString(),
XPipeInstallation.getDaemonDebugScriptPath(sc.getOsType()));
if (sc.getOsType().equals(OsType.WINDOWS)) {
sc.executeSimpleCommand(ScriptHelper.createDetachCommand(sc, "\"" + script + "\""));
} else {
TerminalHelper.open("XPipe Debug", "\"" + script + "\"");
}
}
});
DesktopHelper.browsePath(AppLogs.get().getSessionLogsDirectory());
}),
null);
}
return b.addComp(
"installationFiles",
new ButtonComp(AppI18n.observable("openInstallationDirectory"), () -> {
DesktopHelper.browsePath(XPipeInstallation.getCurrentInstallationBasePath());
}),
null)
.build();
}
}

View file

@ -0,0 +1,87 @@
package io.xpipe.app.comp.base;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
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 lombok.AllArgsConstructor;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.function.Consumer;
@AllArgsConstructor
@Getter
public class DescriptionButtonComp extends SimpleComp {
private final ObservableValue<String> name;
private final ObservableValue<String> description;
private final ObservableValue<String> icon;
private final Consumer<ActionEvent> action;
public DescriptionButtonComp(String nameKey, String descriptionKey, String icon, Consumer<ActionEvent> action) {
this.name = AppI18n.observable(nameKey);
this.description = AppI18n.observable(descriptionKey);
this.icon = new SimpleStringProperty(icon);
this.action = action;
}
@Override
protected Region createSimple() {
var bt = new Button();
bt.setGraphic(createNamedEntry());
Styles.toggleStyleClass(bt, Styles.FLAT);
bt.setOnAction(e -> {
action.accept(e);
});
return bt;
}
private Region createNamedEntry() {
var header = new Label();
header.textProperty().bind(PlatformThread.sync(name));
var desc = new Label();
desc.textProperty().bind(PlatformThread.sync(description));
AppFont.small(desc);
desc.setOpacity(0.65);
var text = new VBox(header, desc);
text.setSpacing(2);
var fi = new FontIcon();
SimpleChangeListener.apply(PlatformThread.sync(icon), val -> {
fi.setIconLiteral(val);
});
var pane = new StackPane(fi);
var hbox = new HBox(pane, text);
hbox.setSpacing(8);
pane.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> (header.getHeight() + desc.getHeight()) * 0.6,
header.heightProperty(),
desc.heightProperty()));
pane.prefHeightProperty()
.bind(Bindings.createDoubleBinding(
() -> header.getHeight() + desc.getHeight() + 2,
header.heightProperty(),
desc.heightProperty()));
pane.prefHeightProperty().addListener((c, o, n) -> {
var size = Math.min(n.intValue(), 100);
fi.setIconSize((int) (size * 0.55));
});
return hbox;
}
}

View file

@ -1,5 +1,6 @@
package io.xpipe.app.core.mode;
import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.storage.collection.SourceCollectionViewState;
import io.xpipe.app.comp.storage.store.StoreViewState;
import io.xpipe.app.core.*;
@ -52,6 +53,7 @@ public class BaseMode extends OperationMode {
@Override
public void finalTeardown() {
TrackEvent.info("mode", "Background mode shutdown started");
BrowserModel.DEFAULT.reset();
AppSocketServer.reset();
SourceCollectionViewState.reset();
StoreViewState.reset();

View file

@ -0,0 +1,177 @@
package io.xpipe.app.prefs;
import io.xpipe.app.comp.base.DescriptionButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.util.*;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.util.XPipeInstallation;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.List;
public class AboutComp extends Comp<CompStructure<?>> {
private Region createDepsList() {
var deps = new ThirdPartyDependencyListComp().createRegion();
return deps;
}
private Comp<?> createActions() {
return new OptionsBuilder()
.addTitle("usefulActions")
.addComp(
new DescriptionButtonComp("reportIssue", "reportIssueDescription", "mdal-bug_report", e -> {
var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory());
}
UserReportComp.show(event.build());
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp(
"openCurrentLogFile",
"openCurrentLogFileDescription",
"mdmz-text_snippet",
e -> {
FileOpener.openInTextEditor(AppLogs.get()
.getSessionLogsDirectory()
.resolve("xpipe.log")
.toString());
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp(
"launchDebugMode", "launchDebugModeDescription", "mdmz-refresh", e -> {
OperationMode.executeAfterShutdown(() -> {
try (var sc = ShellStore.createLocal()
.control()
.start()) {
var script = FileNames.join(
XPipeInstallation.getCurrentInstallationBasePath()
.toString(),
XPipeInstallation.getDaemonDebugScriptPath(sc.getOsType()));
if (sc.getOsType().equals(OsType.WINDOWS)) {
sc.executeSimpleCommand(ScriptHelper.createDetachCommand(
sc, "\"" + script + "\""));
} else {
TerminalHelper.open("XPipe Debug", "\"" + script + "\"");
}
}
});
DesktopHelper.browsePath(
AppLogs.get().getSessionLogsDirectory());
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp(
"openInstallationDirectory",
"openInstallationDirectoryDescription",
"mdomz-snippet_folder",
e -> {
DesktopHelper.browsePath(
XPipeInstallation.getCurrentInstallationBasePath());
e.consume();
})
.grow(true, false),
null)
.buildComp();
}
private Comp<?> createLinks() {
return new OptionsBuilder()
.addComp(
new DescriptionButtonComp(
"securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> {
Hyperlinks.open(Hyperlinks.SECURITY);
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
Hyperlinks.open(Hyperlinks.PRIVACY);
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp("thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> {
AppWindowHelper.sideWindow(AppI18n.get("openSourceNotices"), stage -> Comp.of(() -> createThirdPartyDeps()), true, null).show();
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp("discord", "discordDescription", "mdi2d-discord", e -> {
Hyperlinks.open(Hyperlinks.DISCORD);
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp("slack", "slackDescription", "mdi2s-slack", e -> {
Hyperlinks.open(Hyperlinks.SLACK);
e.consume();
})
.grow(true, false),
null)
.addComp(
new DescriptionButtonComp("github", "githubDescription", "mdi2g-github", e -> {
Hyperlinks.open(Hyperlinks.GITHUB);
e.consume();
})
.grow(true, false),
null)
.buildComp();
}
private Region createThirdPartyDeps() {
var list = new ThirdPartyDependencyListComp().createRegion();
list.getStyleClass().add("open-source-notices");
var sp = new ScrollPane(list);
sp.setFitToWidth(true);
sp.setPrefWidth(600);
sp.setPrefHeight(500);
return sp;
}
@Override
public CompStructure<?> createBase() {
var props = new PropertiesComp().padding(new Insets(0, 0, 0, 15));
var update = Comp.derive(new UpdateCheckComp(), u -> {
var sp = new StackPane(u);
sp.setAlignment(Pos.CENTER);
sp.setPadding(new Insets(10, 0, 10, 15));
return sp;
})
.grow(true, false);
var box = new VerticalComp(List.of(props, update, createLinks(), createActions()))
.apply(s -> s.get().setFillWidth(true))
.apply(struc -> struc.get().setSpacing(15))
.styleClass("information")
.styleClass("about-tab")
.apply(struc -> struc.get().setPrefWidth(800));
return box.createStructure();
}
}

View file

@ -28,6 +28,7 @@ import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.SneakyThrows;
import java.nio.file.Path;
import java.util.*;
@ -490,8 +491,15 @@ public class AppPrefs {
return null;
}
@SneakyThrows
private AppPreferencesFx createPreferences() {
var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class);
ctr.setAccessible(true);
var s = ctr.newInstance(null, new LazyNodeElement<>(() -> new AboutComp().createRegion()), null);
var categories = new ArrayList<>(List.of(
Category.of(
"application", Group.of(s)),
Category.of(
"system",
Group.of(

View file

@ -3,7 +3,6 @@ package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.structure.Element;
import com.dlsc.formsfx.model.structure.Field;
import com.dlsc.formsfx.model.structure.Form;
import com.dlsc.formsfx.model.structure.NodeElement;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl;
import com.dlsc.preferencesfx.formsfx.view.renderer.PreferencesFxFormRenderer;
import com.dlsc.preferencesfx.formsfx.view.renderer.PreferencesFxGroup;
@ -35,6 +34,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
@Override
public void initializeParts() {
super.initializeParts();
grid.getStyleClass().add("grid");
}
@Override
@ -73,7 +73,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
SimpleControl c = (SimpleControl) ((Field) element).getRenderer();
c.setField((Field) element);
AppFont.header(c.getFieldLabel());
AppFont.normal(c.getFieldLabel());
c.getFieldLabel().setPrefHeight(AppFont.getPixelSize(1));
c.getFieldLabel().setMaxHeight(AppFont.getPixelSize(1));
grid.add(c.getFieldLabel(), 0, i + rowAmount, 2, 1);
@ -81,7 +81,9 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
var canFocus = BindingsHelper.persist(c.getNode().disabledProperty().not());
var descriptionLabel = new Label();
AppFont.medium(descriptionLabel);
descriptionLabel.setWrapText(true);
descriptionLabel.setMaxWidth(700);
descriptionLabel
.disableProperty()
.bind(c.getFieldLabel().disabledProperty());
@ -89,14 +91,13 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
.opacityProperty()
.bind(c.getFieldLabel()
.opacityProperty()
.multiply(0.8));
.multiply(0.65));
descriptionLabel
.managedProperty()
.bind(c.getFieldLabel().managedProperty());
descriptionLabel
.visibleProperty()
.bind(c.getFieldLabel().visibleProperty());
descriptionLabel.setMaxHeight(USE_PREF_SIZE);
if (AppI18n.getInstance().containsKey(descriptionKey)) {
rowAmount++;
descriptionLabel.textProperty().bind(AppI18n.observable(descriptionKey));
@ -107,6 +108,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
rowAmount++;
var node = c.getNode();
AppFont.medium(c.getNode());
c.getFieldLabel().focusTraversableProperty().bind(canFocus);
grid.add(node, 0, i + rowAmount, 1, 1);
@ -130,8 +132,9 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
node.getStyleClass().add(styleClass.toString() + "-node");
}
if (element instanceof NodeElement nodeElement) {
grid.add(nodeElement.getNode(), 0, i + rowAmount, GridPane.REMAINING, 1);
if (element instanceof LazyNodeElement<?> nodeElement) {
var node = nodeElement.getNode();
grid.add(node, 0, i + rowAmount, 2, 1);
}
}
}

View file

@ -0,0 +1,26 @@
package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.structure.Element;
import javafx.scene.Node;
import java.util.function.Supplier;
public class LazyNodeElement<N extends Node> extends Element<LazyNodeElement<N>> {
protected Supplier<N> node;
public static <T extends Node> LazyNodeElement<T> of(Supplier<T> node) {
return new LazyNodeElement<>(node);
}
protected LazyNodeElement(Supplier<N> node) {
if (node == null) {
throw new NullPointerException("Node argument must not be null");
}
this.node = node;
}
public N getNode() {
return node.get();
}
}

View file

@ -1,4 +1,4 @@
package io.xpipe.app.comp.about;
package io.xpipe.app.prefs;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppFont;
@ -19,6 +19,7 @@ public class PropertiesComp extends SimpleComp {
var title = Comp.of(() -> {
var image = new ImageView(App.getApp().getIcon());
image.setPreserveRatio(true);
image.setSmooth(true);
image.setFitHeight(40);
var label = new Label(AppI18n.get("xPipeClient"), image);
label.getStyleClass().add("header");

View file

@ -1,4 +1,4 @@
package io.xpipe.app.comp.about;
package io.xpipe.app.prefs;
import io.xpipe.app.core.AppExtensionManager;
import io.xpipe.app.core.AppResources;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.comp.about;
package io.xpipe.app.prefs;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.comp.about;
package io.xpipe.app.prefs;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;

View file

@ -31,10 +31,9 @@ public class JfxHelper {
public static Region createNamedEntry(String nameString, String descString, FontIcon graphic) {
var header = new Label(nameString);
AppFont.header(header);
var desc = new Label(descString);
desc.setWrapText(true);
AppFont.small(desc);
desc.setOpacity(0.65);
var text = new VBox(header, desc);
text.setSpacing(2);
@ -43,7 +42,7 @@ public class JfxHelper {
hbox.setSpacing(8);
pane.prefWidthProperty()
.bind(Bindings.createDoubleBinding(
() -> header.getHeight() + desc.getHeight() + 2,
() -> (header.getHeight() + desc.getHeight()) * 0.6,
header.heightProperty(),
desc.heightProperty()));
pane.prefHeightProperty()
@ -53,9 +52,8 @@ public class JfxHelper {
desc.heightProperty()));
pane.prefHeightProperty().addListener((c, o, n) -> {
var size = Math.min(n.intValue(), 100);
graphic.setIconSize(size);
graphic.setIconSize((int) (size * 0.55));
});
return hbox;
}

View file

@ -0,0 +1,108 @@
package io.xpipe.app.util;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.util.Duration;
import java.util.function.Function;
public class SmoothScroll {
private static ScrollBar getScrollbarComponent(Node no, Orientation orientation) {
Node n = no.lookup(".scroll-bar");
if (n instanceof ScrollBar) {
final ScrollBar bar = (ScrollBar) n;
if (bar.getOrientation().equals(orientation)) {
return bar;
}
}
return null;
}
public static void smoothScrollingListView(Node n, double speed) {
smoothScrollingListView(n, speed, Orientation.VERTICAL, bounds -> bounds.getHeight());
}
public static void smoothHScrollingListView(ListView<?> listView, double speed) {
smoothScrollingListView(listView, speed, Orientation.HORIZONTAL, bounds -> bounds.getHeight());
}
private static void smoothScrollingListView(
Node n, double speed, Orientation orientation, Function<Bounds, Double> sizeFunc) {
((TableView) n).skinProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
ScrollBar scrollBar = getScrollbarComponent(n, orientation);
if (scrollBar == null) {
return;
}
scrollBar.setUnitIncrement(1);
final double[] frictions = {
0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003,
0.00001
};
final double[] pushes = {speed};
final double[] derivatives = new double[frictions.length];
final double[] lastVPos = {0};
Timeline timeline = new Timeline();
final EventHandler<MouseEvent> dragHandler = event -> timeline.stop();
final EventHandler<ScrollEvent> scrollHandler = event -> {
scrollBar.valueProperty().set(lastVPos[0]);
if (event.getEventType() == ScrollEvent.SCROLL) {
double direction = event.getDeltaY() > 0 ? -1 : 1;
for (int i = 0; i < pushes.length; i++) {
derivatives[i] += direction * pushes[i];
}
if (timeline.getStatus() == Animation.Status.STOPPED) {
timeline.play();
}
}
event.consume();
};
if (scrollBar.getParent() != null) {
scrollBar.getParent().addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
scrollBar.getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
}
scrollBar.parentProperty().addListener((o, oldVal, newVal) -> {
if (oldVal != null) {
oldVal.removeEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
oldVal.removeEventHandler(ScrollEvent.ANY, scrollHandler);
}
if (newVal != null) {
newVal.addEventHandler(MouseEvent.DRAG_DETECTED, dragHandler);
newVal.addEventHandler(ScrollEvent.ANY, scrollHandler);
}
});
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(3), (event) -> {
for (int i = 0; i < derivatives.length; i++) {
derivatives[i] *= frictions[i];
}
for (int i = 1; i < derivatives.length; i++) {
derivatives[i] += derivatives[i - 1];
}
double dy = derivatives[derivatives.length - 1];
double size = sizeFunc.apply(scrollBar.getLayoutBounds());
scrollBar.valueProperty().set(Math.min(Math.max(scrollBar.getValue() + dy / size, 0), 1));
lastVPos[0] = scrollBar.getValue();
if (Math.abs(dy) < 1) {
if (Math.abs(dy) < 0.001) {
timeline.stop();
}
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
}
});
}
}

View file

@ -24,6 +24,8 @@ noMatchingStoreFound=No suitable saved store was found
addStore=Add Store
anyStreamDescription=Or choose specific type
reportIssue=Report Issue
reportIssueDescription=Open the integrated issue reporter
usefulActions=Useful actions
stored=Saved
other=Other
remote=Remote File

View file

@ -7,6 +7,8 @@ editorProgramDescription=The default text editor to use when editing any kind of
useSystemFont=Use system font
updates=Updates
advanced=Advanced
thirdParty=Open source notices
thirdPartyDescription=View the open source licenses of third-party libraries
workspaceLock=Workspace lock
workspaceLockDescription=Sets a custom password to encrypt your stored information in XPipe. This results in increased security as it provides an additional layer of encryption for your stored sensitive information. You will then be prompted to enter the password when XPipe starts.
useSystemFontDescription=Controls whether to use your system font or the default font used by XPipe (Roboto).
@ -18,6 +20,7 @@ saveWindowLocation=Save window location
saveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts.
startupShutdown=Startup / Shutdown
system=System
application=Application
updateToPrereleases=Include prereleases
updateToPrereleasesDescription=When enabled, the update check will also look for available prereleases in addition to full releases.
storage=Storage

View file

@ -167,12 +167,17 @@ links=Useful links
website=Website
documentation=Documentation
discord=Discord
discordDescription=Join the Discord server
security=Security
securityPolicy=Security Policy
securityPolicy=Security information
securityPolicyDescription=Read the detailed security policy
privacy=Privacy Policy
privacyDescription=Read the privacy policy for the XPipe application
slack=Slack
slackDescription=Join the Slack workspace
support=Support
github=GitHub
githubDescription=Check out the GitHub repository
openSourceNotices=Open Source Notices
xPipeClient=XPipe Desktop
checkForUpdates=Check for updates
@ -202,11 +207,14 @@ logFile=Log File
logFiles=Log Files
logFilesAttachment=Log Files
issueReporter=Issue Reporter
openCurrentLogFile=Open current log file
openCurrentLogFile=Log files
openCurrentLogFileDescription=Open the log file of the current session
openLogsDirectory=Open logs directory
installationFiles=Installation Files
openInstallationDirectory=Open installation directory
launchDebugMode=Launch debug mode
openInstallationDirectory=Installation files
openInstallationDirectoryDescription=Open XPipe installation directory
launchDebugMode=Debug mode
launchDebugModeDescription=Restart XPipe in debug mode
extensionInstallTitle=Download
extensionInstallDescription=This action requires additional third party libraries that are not distributed by XPipe. You can automatically install them here. The components are then downloaded from the vendor website:
extensionInstallLicenseNote=By performing the download and automatic installation you agree to the terms of the third party licenses:

View file

@ -37,20 +37,30 @@ visibility: hidden;
}
.prefs .tree-view {
-fx-border-width: 0 1px 0 0;
}
.prefs .grid {
-fx-padding: 1em 2.3em 0 2.3em;
}
.prefs {
-fx-border-width: 0;
-fx-padding: 1em 1em 4em 1em;
-fx-padding: 0;
-fx-border-color: transparent;
-fx-background-insets: 0;
}
.prefs > * > * {
-fx-background-insets: 0;
.prefs .custom-text-field {
-fx-border-width: 5px;
-fx-padding: 0;
-fx-border-color: transparent;
-fx-border-radius: 0;
-fx-background-radius: 0;
}
.prefs .split-pane-divider {
-fx-padding: 0 ;
dividerPositions: 0.5;
}
}

View file

@ -0,0 +1,40 @@
package io.xpipe.core.process;
public class CommandBuilder {
public static CommandBuilder of() {
return new CommandBuilder(false);
}
public static CommandBuilder ofNoQuotes() {
return new CommandBuilder(true);
}
private CommandBuilder(boolean noQuoting) {
this.noQuoting = noQuoting;
}
private final boolean noQuoting;
private final StringBuilder builder = new StringBuilder();
public CommandBuilder add(String s) {
if (!builder.isEmpty()) {
builder.append(' ');
}
if (s.contains(" ") || s.contains("\t")) {
if (noQuoting) {
throw new IllegalArgumentException("No quoting rule conflicts with spaces an argument");
}
s = "\"" + s + "\"";
}
builder.append(s);
return this;
}
public String build() {
return builder.toString();
}
}