This commit is contained in:
crschnick
2025-03-27 21:54:50 +00:00
parent 24b71e4900
commit 09bc130c31
18 changed files with 170 additions and 109 deletions
@@ -30,12 +30,6 @@ public class ModalButton {
@NonFinal
Consumer<Button> augment;
public static ModalButton hide(ObservableValue<String> name, LabelGraphic icon, Runnable action) {
return new ModalButton("hide", () -> {
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(name, icon, action));
}, true, false);
}
public static ModalButton finish(Runnable action) {
return new ModalButton("finish", action, true, true);
}
@@ -1,9 +1,11 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import lombok.*;
import lombok.experimental.NonFinal;
@@ -24,7 +26,7 @@ public class ModalOverlay {
}
public static ModalOverlay of(String titleKey, Comp<?> content, LabelGraphic graphic) {
return new ModalOverlay(titleKey, content, graphic, new ArrayList<>(), false);
return new ModalOverlay(titleKey, content, graphic, new ArrayList<>(), false, null);
}
public ModalOverlay withDefaultButtons(Runnable action) {
@@ -47,11 +49,21 @@ public class ModalOverlay {
@NonFinal
boolean persistent;
@NonFinal
@Setter
Runnable hideAction;
public ModalButton addButton(ModalButton button) {
buttons.add(button);
return button;
}
public void hideable(ObservableValue<String> name, LabelGraphic icon, Runnable action) {
setHideAction(() -> {
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(name, icon, action));
});
}
public void addButtonBarComp(Comp<?> comp) {
buttons.add(comp);
}
@@ -1,5 +1,6 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFontSizes;
@@ -21,6 +22,7 @@ import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -41,6 +43,22 @@ public class ModalOverlayComp extends SimpleComp {
this.overlayContent = overlayContent;
}
private Animation showAnimation(Node node) {
if (OsType.getLocal() == OsType.LINUX) {
return null;
}
Timeline t = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(node.opacityProperty(), 0.01)),
new KeyFrame(Duration.millis(100), new KeyValue(node.opacityProperty(), 0.01)),
new KeyFrame(Duration.millis(200), new KeyValue(node.opacityProperty(), 1)));
t.statusProperty().addListener((obs, old, val) -> {
if (val == Animation.Status.STOPPED) {
node.setOpacity(1.0F);
}
});
return t;
}
@Override
protected Region createSimple() {
var bgRegion = background.createRegion();
@@ -150,6 +168,7 @@ public class ModalOverlayComp extends SimpleComp {
private void showModalBox(ModalPane modal, ModalOverlay overlay) {
var modalBox = toBox(modal, overlay);
modalBox.setOpacity(0.01);
modal.setPersistent(overlay.isPersistent());
modal.show(modalBox);
if (overlay.isPersistent() || overlay.getTitleKey() == null) {
@@ -158,6 +177,19 @@ public class ModalOverlayComp extends SimpleComp {
closeButton.setVisible(false);
}
}
// This is ugly, but works better than animations
// The content layout takes some time, resulting in shifting content
// We don't want to show that, so wait after that is done
Platform.runLater(() -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
Platform.runLater(() -> {
modalBox.setOpacity(1.0);
});
});
});
});
}
private Region toBox(ModalPane pane, ModalOverlay newValue) {
@@ -184,11 +216,12 @@ public class ModalOverlayComp extends SimpleComp {
}
if (newValue.getButtons().size() > 0) {
var buttonBar = new ButtonBar();
var buttonBar = new HBox();
buttonBar.setSpacing(10);
buttonBar.setAlignment(Pos.CENTER_RIGHT);
for (var o : newValue.getButtons()) {
var node = o instanceof ModalButton mb ? toButton(mb) : ((Comp<?>) o).createRegion();
buttonBar.getButtons().add(node);
ButtonBar.setButtonUniformSize(node, o instanceof ModalButton);
buttonBar.getChildren().add(node);
if (o instanceof ModalButton) {
node.prefHeightProperty().bind(buttonBar.heightProperty());
}
@@ -197,7 +230,7 @@ public class ModalOverlayComp extends SimpleComp {
AppFontSizes.xs(buttonBar);
}
var modalBox = new ModalBox(content) {
var modalBox = new ModalBox(pane, content) {
@Override
protected void setCloseButtonPosition() {
@@ -205,6 +238,12 @@ public class ModalOverlayComp extends SimpleComp {
setRightAnchor(closeButton, 19d);
}
};
if (newValue.getHideAction() != null) {
modalBox.setOnMinimize(event -> {
newValue.getHideAction().run();
event.consume();
});
}
modalBox.setOnClose(event -> {
overlayContent.setValue(null);
event.consume();
@@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProvider;
@@ -15,6 +16,7 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
@@ -134,18 +136,12 @@ public class StoreCreationDialog {
}
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getButtonTypes().add(new ButtonType(AppI18n.get("retry"), ButtonBar.ButtonData.CANCEL_CLOSE));
alert.getButtonTypes().add(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OK_DONE));
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
var skipped = new SimpleBooleanProperty();
var modal = ModalOverlay.of("confirmInvalidStoreTitle", AppDialog.dialogTextKey("confirmInvalidStoreContent"));
modal.addButton(new ModalButton("retry", null, true, false));
modal.addButton(new ModalButton("skip", () -> skipped.set(true), true, true));
modal.showAndWait();
return skipped.get();
}
private static ModalOverlay createModalOverlay(StoreCreationModel model) {
@@ -153,6 +149,13 @@ public class StoreCreationDialog {
comp.prefWidth(650);
var nameKey = model.storeTypeNameKey() + "Add";
var modal = ModalOverlay.of(nameKey, comp);
var provider = model.getProvider().getValue();
var graphic = provider != null && provider.getDisplayIconFileName(model.getStore().get()) != null?
new LabelGraphic.ImageGraphic(provider.getDisplayIconFileName(model.getStore().get()), 20) :
new LabelGraphic.IconGraphic("mdi2b-beaker-plus-outline");
modal.hideable(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
modal.show();
});
modal.persist();
modal.addButton(new ModalButton("docs", () -> {
model.showDocs();
@@ -160,27 +163,26 @@ public class StoreCreationDialog {
button.visibleProperty().bind(Bindings.not(model.canShowDocs()));
}));
modal.addButton(ModalButton.cancel());
var graphic = model.getProvider().getValue() != null ?
new LabelGraphic.ImageGraphic(model.getProvider().getValue().getDisplayIconFileName(null), 20) :
new LabelGraphic.IconGraphic("mdi2b-beaker-plus-outline");
modal.addButton(ModalButton.hide(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
modal.show();
}));
modal.addButton(new ModalButton("connect", () -> {
model.connect();
}, false, false).augment(button -> {
button.visibleProperty().bind(Bindings.not(model.canConnect()));
}));
modal.addButton(new ModalButton("skipValidation", () -> {
if (showInvalidConfirmAlert()) {
modal.addButton(new ModalButton("skip", () -> {
if (!showInvalidConfirmAlert()) {
model.commit();
} else {
model.finish();
}
}, true, false));
}, true, false)).augment(button -> {
button.visibleProperty().bind(model.getSkippable());
});
modal.addButton(new ModalButton("finish", () -> {
model.finish();
}, true, true));
}, false, true));
model.getFinished().addListener((obs, oldValue, newValue) -> {
modal.close();
});
return modal;
}
}
@@ -34,7 +34,7 @@ public class StoreIconChoiceDialog {
private ModalOverlay createOverlay() {
var filterText = new SimpleStringProperty();
var filter = new FilterComp(filterText).grow(true, false);
var filter = new FilterComp(filterText).hgrow();
filter.focusOnShow();
var github = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
overlay.close();
@@ -60,7 +60,10 @@ public class ErrorHandlerDialog {
}, false, false));
}
errorModal.addButton(new ModalButton("report", () -> {
UserReportComp.show(event);
if (UserReportComp.show(event)) {
comp.getTakenAction().setValue(ErrorAction.ignore());
errorModal.close();
}
}, false, false));
errorModal.addButton(ModalButton.ok());
modal.set(errorModal);
@@ -1,24 +1,17 @@
package io.xpipe.app.issue;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.*;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.resources.AppResources;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
@@ -28,31 +21,57 @@ import atlantafx.base.controls.Spacer;
import java.nio.file.Files;
import java.nio.file.Path;
public class UserReportComp extends SimpleComp {
public class UserReportComp extends ModalOverlayContentComp {
private final StringProperty email = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final ListProperty<Path> includedDiagnostics;
private final ErrorEvent event;
private final Stage stage;
private boolean sent;
public UserReportComp(ErrorEvent event, Stage stage) {
public UserReportComp(ErrorEvent event) {
this.event = event;
this.includedDiagnostics = new SimpleListProperty<>(FXCollections.observableArrayList());
this.stage = stage;
stage.setOnHidden(event1 -> {
if (!sent) {
ErrorAction.ignore().handle(event);
}
});
}
public static void show(ErrorEvent event) {
var window =
AppWindowHelper.sideWindow(AppI18n.get("errorHandler"), w -> new UserReportComp(event, w), true, null);
window.showAndWait();
public static boolean show(ErrorEvent event) {
var comp = new UserReportComp(event);
var modal = ModalOverlay.of("errorHandler", comp);
var sent = new SimpleBooleanProperty();
modal.addButtonBarComp(privacyPolicy());
modal.addButtonBarComp(Comp.hspacer());
modal.addButton(new ModalButton("sendReport", () -> {
comp.send();
sent.set(true);
}, true, true));
modal.showAndWait();
return sent.get();
}
private static Comp<?> privacyPolicy() {
return Comp.of(() -> {
var dataPolicyButton = new Hyperlink(AppI18n.get("dataHandlingPolicies"));
AppFontSizes.xs(dataPolicyButton);
dataPolicyButton.setOnAction(event1 -> {
AppResources.with(AppResources.XPIPE_MODULE, "misc/report_privacy_policy.md", file -> {
var markDown = new MarkdownComp(Files.readString(file), s -> s, true)
.apply(struc -> struc.get().setMaxWidth(500))
.apply(struc -> struc.get().setMaxHeight(400));
var popover = new Popover(markDown.createRegion());
popover.setCloseButtonEnabled(true);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFontSizes.xs(popover.getContentNode());
popover.show(dataPolicyButton);
});
event1.consume();
});
var agree = new Label("Note the issue reporter ");
var buttons = new HBox(agree, dataPolicyButton);
buttons.setAlignment(Pos.CENTER_LEFT);
buttons.setMinWidth(Region.USE_PREF_SIZE);
return buttons;
});
}
@Override
@@ -94,13 +113,10 @@ public class UserReportComp extends SimpleComp {
reportSection.setSpacing(5);
reportSection.getStyleClass().add("report");
var buttons = createBottomBarNavigation();
reportSection.getChildren().addAll(new Spacer(8, Orientation.VERTICAL), emailHeader, email);
var layout = new BorderPane();
layout.setCenter(reportSection);
layout.setBottom(buttons);
layout.getStyleClass().add("error-report");
layout.getStyleClass().add("background");
layout.setPrefWidth(600);
@@ -108,42 +124,11 @@ public class UserReportComp extends SimpleComp {
return layout;
}
private Region createBottomBarNavigation() {
var dataPolicyButton = new Hyperlink(AppI18n.get("dataHandlingPolicies"));
AppFontSizes.xs(dataPolicyButton);
dataPolicyButton.setOnAction(event1 -> {
AppResources.with(AppResources.XPIPE_MODULE, "misc/report_privacy_policy.md", file -> {
var markDown = new MarkdownComp(Files.readString(file), s -> s, true)
.apply(struc -> struc.get().setMaxWidth(500))
.apply(struc -> struc.get().setMaxHeight(400));
var popover = new Popover(markDown.createRegion());
popover.setCloseButtonEnabled(true);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFontSizes.xs(popover.getContentNode());
popover.show(dataPolicyButton);
});
event1.consume();
});
var sendButton = new ButtonComp(AppI18n.observable("sendReport"), this::send)
.apply(struc -> struc.get().setDefaultButton(true))
.createRegion();
var spacer = new Region();
var agree = new Label("Note the issue reporter ");
var buttons = new HBox(agree, dataPolicyButton, spacer, sendButton);
buttons.setAlignment(Pos.CENTER);
buttons.getStyleClass().add("buttons");
HBox.setHgrow(spacer, Priority.ALWAYS);
return buttons;
}
private void send() {
event.clearAttachments();
event.setShouldSendDiagnostics(true);
includedDiagnostics.forEach(event::addAttachment);
event.attachUserReport(email.get(), text.get());
SentryErrorHandler.getInstance().handle(event);
sent = true;
stage.close();
}
}
@@ -7,7 +7,6 @@ import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.ExternalApplicationHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.*;
@@ -16,7 +15,6 @@ import io.xpipe.core.process.ShellScript;
import io.xpipe.core.util.ValidationException;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import lombok.Builder;
@@ -106,7 +104,7 @@ public class PasswordManagerCommand implements PasswordManager {
@Override
public String retrievePassword(String key) {
var cmd = ExternalApplicationHelper.replaceFileArgument(script.getValue(), "KEY", key);
var cmd = ExternalApplicationHelper.replaceVariableArgument(script.getValue(), "KEY", key);
return retrieveWithCommand(cmd);
}
@@ -11,7 +11,7 @@ public abstract class PasswordManagerFixedCommand implements PasswordManager {
@Override
public synchronized String retrievePassword(String key) {
var cmd = ExternalApplicationHelper.replaceFileArgument(getScript().getValue(), "KEY", key);
var cmd = ExternalApplicationHelper.replaceVariableArgument(getScript().getValue(), "KEY", key);
return PasswordManagerCommand.retrieveWithCommand(cmd);
}
}
@@ -10,12 +10,12 @@ import java.util.stream.Collectors;
public class ExternalApplicationHelper {
public static String replaceFileArgument(String format, String variable, String file) {
public static String replaceVariableArgument(String format, String variable, String value) {
// Support for legacy variables that were not upper case
variable = variable.toUpperCase(Locale.ROOT);
format = format.replace("$" + variable.toLowerCase(Locale.ROOT), "$" + variable.toUpperCase(Locale.ROOT));
var fileString = file.contains(" ") ? "\"" + file + "\"" : file;
var fileString = value.contains(" ") ? "\"" + value + "\"" : value;
// Check if the variable is already quoted
return format.replace("\"$" + variable + "\"", fileString).replace("$" + variable, fileString);
}
@@ -173,7 +173,7 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
var command = CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", file.toString()));
var command = CommandBuilder.of().add(ExternalApplicationHelper.replaceVariableArgument(format, "FILE", file.toString()));
if (AppPrefs.get().customEditorCommandInTerminal().get()) {
TerminalLauncher.openDirect(file.toString(), sc -> command.buildFull(sc), AppPrefs.get().terminalType.get());
} else {
@@ -293,7 +293,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
var format =
customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.add(ExternalApplicationHelper.replaceFileArgument(
.add(ExternalApplicationHelper.replaceVariableArgument(
format,
"FILE",
writeRdpConfigFile(configuration.getTitle(), configuration.getConfig())
@@ -39,7 +39,7 @@ public class CustomTerminalType extends ExternalApplicationType implements Exter
var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD";
try (var pc = LocalShell.getShell()) {
var toExecute = ExternalApplicationHelper.replaceFileArgument(
var toExecute = ExternalApplicationHelper.replaceVariableArgument(
format, "CMD", configuration.getScriptFile().toString());
// We can't be sure whether the command is blocking or not, so always make it not blocking
if (pc.getOsType().equals(OsType.WINDOWS)) {
@@ -7,7 +7,6 @@
}
.error-report .report {
-fx-padding: 1.0em 1.5em 1em 1.5em;
-fx-spacing: 1.3em;
}
+28
View File
@@ -0,0 +1,28 @@
XPipe 15 comes with many new features, performance improvements, and many bug fixes. Note that the installation data layout has been changed and executables have been moved around. This will break some shortcuts and the old restart functionality after an update. So if you're updating from within XPipe, it won't automatically restart for this update.
## Tailscale SSH support
You can now connect to devices in your tailnet via Tailscale SSH and your locally installed tailscale command-line client. This integration supports multiple accounts as well to switch between different tailnets.
## Custom icons
You can now add custom icons to use for your connections. This implementation replaces the old model of shipping the icons from https://github.com/selfhst/icons along XPipe. Instead, you can now dynamically add sources of icons. This can either be a local directory or a remote git repository that can be cloned and pulled by xpipe. XPipe will pick up any .svg files in there, rasterize them to cached .pngs, and display them in XPipe. As default icon sources, it will still come with https://github.com/selfhst/icons, but now it can fetch these icons at runtime. If you are using the git vault, you can also add icons to a synced directory in your git vault to have access to them on all systems.
Your existing custom icons set for connections are not lost, it just requires you to first update the icons and then restart XPipe.
## Package manager repositories
There is now an apt repository available at https://apt.xpipe.io and an rpm repository available at https://rpm.xpipe.io. You can add them as sources to apt or your rpm-based package manager. This allows you to also install and upgrade xpipe via your native package manager instead of using the built-in self-updater.
## New docs
There is a new documentation site https://docs.xpipe.io. The goal is to expand this over time to provide proper documentation for many features. If you're looking for documentation for a certain feature, let me know.
## Other
- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization
## Fixes
- Fix custom service commands not launching properly with PowerShell as the local shell
- Fix update check being influenced by the local GitHub rate limiting
@@ -126,7 +126,7 @@ public interface ServiceProtocolType {
var format = commandTemplate.toLowerCase(Locale.ROOT).contains("$port")
? commandTemplate
: commandTemplate + " localhost:$PORT";
var toExecute = ExternalApplicationHelper.replaceFileArgument(format, "PORT", port);
var toExecute = ExternalApplicationHelper.replaceVariableArgument(format, "PORT", port);
// We can't be sure whether the command is blocking or not, so always make it not blocking
ExternalApplicationHelper.startAsync(toExecute);
}
Binary file not shown.
+4 -3
View File
@@ -50,9 +50,10 @@ dragAndDropFilesHere=Or just drag and drop a file here
confirmDsCreationAbortTitle=Confirm abort
confirmDsCreationAbortHeader=Do you want to abort the data source creation?
confirmDsCreationAbortContent=Any data source creation progress will be lost.
confirmInvalidStoreTitle=Failed connection
confirmInvalidStoreHeader=Do you want to skip connection validation?
confirmInvalidStoreContent=You can add this connection even if it could not be validated and fix the connection problems later on.
#force
confirmInvalidStoreTitle=Skip validation
#force
confirmInvalidStoreContent=Do you want to skip connection validation? You can add this connection even if it could not be validated and fix the connection problems later on.
expand=Expand
accessSubConnections=Access sub connections
#context: noun, not rare