diff --git a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java index da8b99cb1..6130ff284 100644 --- a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java +++ b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java @@ -36,6 +36,8 @@ public interface QuickConnectProvider extends ActionProvider { String getPlaceholder(); + String getTemplate(); + boolean skipDialogIfPossible(); default void open(DataStoreEntry e) { diff --git a/app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java b/app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java index 2668a7287..578ea9426 100644 --- a/app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java +++ b/app/src/main/java/io/xpipe/app/beacon/api/DaemonOpenExchange.java @@ -5,14 +5,18 @@ import io.xpipe.app.beacon.BeaconInterface; import io.xpipe.app.beacon.BeaconServerException; import io.xpipe.app.core.AppOpenArguments; +import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.mode.AppOperationMode; import io.xpipe.app.platform.PlatformInit; +import io.xpipe.app.util.FilePath; import io.xpipe.app.util.OsType; +import io.xpipe.app.util.ThreadHelper; import lombok.Builder; import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; +import java.nio.file.Path; import java.util.List; public class DaemonOpenExchange extends BeaconInterface { @@ -31,6 +35,14 @@ public class DaemonOpenExchange extends BeaconInterface { + ThreadHelper.sleep(1000); + AppOperationMode.close(); + }); + return Response.builder().restartRequired(true).build(); + } + if (msg.getArguments().isEmpty()) { try { // At this point we are already loading this on another thread @@ -64,10 +76,15 @@ public class DaemonOpenExchange extends BeaconInterface arguments; + + Path directory; } @Jacksonized @Builder @Value - public static class Response {} + public static class Response { + + boolean restartRequired; + } } diff --git a/app/src/main/java/io/xpipe/app/core/AppI18n.java b/app/src/main/java/io/xpipe/app/core/AppI18n.java index f1afb9e7a..114935c13 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -53,7 +53,7 @@ public class AppI18n { } public static ObservableValue observable(ObservableValue s, Object... vars) { - return BindingsHelper.flatMap(s, v -> INSTANCE.observableImpl(v, vars)); + return BindingsHelper.flatMap(s, v -> v != null ? INSTANCE.observableImpl(v, vars) : null); } public static String get(String s, Object... vars) { diff --git a/app/src/main/java/io/xpipe/app/core/AppInstance.java b/app/src/main/java/io/xpipe/app/core/AppInstance.java index 13363f21e..a4bae57ca 100644 --- a/app/src/main/java/io/xpipe/app/core/AppInstance.java +++ b/app/src/main/java/io/xpipe/app/core/AppInstance.java @@ -78,6 +78,7 @@ public class AppInstance { client.get() .performRequest(DaemonOpenExchange.Request.builder() .arguments(inputs) + .directory(AppProperties.get().getDataDir()) .build()); } } catch (Exception ex) { @@ -108,6 +109,7 @@ public class AppInstance { client.get() .performRequest(DaemonOpenExchange.Request.builder() .arguments(List.of(e.getURI().toString())) + .directory(AppProperties.get().getDataDir()) .build()); } catch (Exception ex) { ErrorEventFactory.fromThrowable(ex).expected().omit().handle(); diff --git a/app/src/main/java/io/xpipe/app/core/window/AppDialog.java b/app/src/main/java/io/xpipe/app/core/window/AppDialog.java index 7b1a9028b..784e33efa 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppDialog.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppDialog.java @@ -58,7 +58,7 @@ public class AppDialog { var firstElement = o.equals(modalOverlays.stream() .filter(modalOverlay -> modalOverlay != null) .findFirst().orElse(null)); - var lastElement = modalOverlays.getLast().equals(o); + var lastElement = modalOverlays.size() > 0 && modalOverlays.getLast().equals(o); // Prevent indices from being moved when closing a modal in the back if (firstElement && !lastElement) { modalOverlays.set(modalOverlays.indexOf(o), null); diff --git a/app/src/main/java/io/xpipe/app/hub/action/impl/CloneHubLeafProvider.java b/app/src/main/java/io/xpipe/app/hub/action/impl/CloneHubLeafProvider.java index 5014e7944..1bb641039 100644 --- a/app/src/main/java/io/xpipe/app/hub/action/impl/CloneHubLeafProvider.java +++ b/app/src/main/java/io/xpipe/app/hub/action/impl/CloneHubLeafProvider.java @@ -62,7 +62,7 @@ public class CloneHubLeafProvider implements HubLeafProvider { entry.setIcon(ref.get().getIcon(), true); entry.setColor(ref.get().getColor()); entry.setExpanded(ref.get().isExpanded()); - entry.setFreeze(ref.get().isFreeze()); + entry.setTemplate(ref.get().isTemplate()); entry.setCategoryUuid(ref.get().getCategoryUuid()); entry.setPinToTop(ref.get().isPinToTop()); entry.setOrderIndex(ref.get().getOrderIndex()); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/hub/comp/DenseStoreEntryComp.java index 4442dabc9..0ceca2e0f 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/DenseStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/DenseStoreEntryComp.java @@ -87,6 +87,7 @@ public class DenseStoreEntryComp extends StoreEntryComp { }, grid.widthProperty())); var notes = new StoreNotesComp(getWrapper()).build(); + var templateIcon = createTemplateIcon().build(); var userIcon = createUserIcon().build(); var pinIcon = createPinIcon().build(); var active = new StoreActiveComp(getWrapper()).build(); @@ -134,7 +135,7 @@ public class DenseStoreEntryComp extends StoreEntryComp { nameCC.setHgrow(Priority.ALWAYS); grid.getColumnConstraints().addAll(nameCC); - var nameBox = new HBox(name, tags, index, active, userIcon, pinIcon, notes); + var nameBox = new HBox(name, tags, index, active, templateIcon, userIcon, pinIcon, notes); nameBox.setSpacing(4); nameBox.setAlignment(Pos.CENTER_LEFT); grid.addRow(0, nameBox); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StandardStoreEntryComp.java index 2682bd008..1977ef219 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StandardStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StandardStoreEntryComp.java @@ -35,6 +35,7 @@ public class StandardStoreEntryComp extends StoreEntryComp { var tags = createTags().build(); var index = createOrderIndex().build(); var notes = new StoreNotesComp(getWrapper()).build(); + var templateIcon = createTemplateIcon().build(); var userIcon = createUserIcon().build(); var pinIcon = createPinIcon().build(); var active = new StoreActiveComp(getWrapper()).build(); @@ -60,7 +61,7 @@ public class StandardStoreEntryComp extends StoreEntryComp { grid.add(storeIcon.build(), 1, 0, 1, 2); grid.getColumnConstraints().add(new ColumnConstraints(52)); - var nameBox = new HBox(name, tags, index, active, userIcon, pinIcon, notes); + var nameBox = new HBox(name, tags, index, active, templateIcon, userIcon, pinIcon, notes); nameBox.setSpacing(4); nameBox.setAlignment(Pos.CENTER_LEFT); grid.add(nameBox, 2, 0); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationComp.java index abda11c69..8dd5b7d22 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationComp.java @@ -9,6 +9,7 @@ import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; import javafx.scene.control.*; @@ -37,16 +38,9 @@ public class StoreCreationComp extends ModalOverlayContentComp { var nameKey = model.storeTypeNameKey(); var built = new OptionsBuilder() .name(nameKey + "Name") - .description(nameKey + "NameDescription") + .description(model.isTemplate() ? nameKey + "TemplateNameDescription" : nameKey + "NameDescription") .addString(model.getName()) - .nonNull() - .check(val -> Validator.create(val, AppI18n.observable("readOnlyStoreError"), model.getName(), s -> { - var same = s != null - && model.getExistingEntry() != null - && DataStorage.get().getEffectiveReadOnlyState(model.getExistingEntry()) - && s.equals(model.getExistingEntry().getName()); - return !same; - })); + .nonNullIf(new ReadOnlyBooleanWrapper(!model.isTemplate())); return built; } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java index 9b3294a06..b69cd906f 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java @@ -180,7 +180,7 @@ public class StoreCreationDialog { private static ModalOverlay createModalOverlay(StoreCreationModel model) { var comp = new StoreCreationComp(model); comp.prefWidth(650); - var nameKey = model.isQuickConnect() ? "quickConnect" : model.getTitleKey(); + var nameKey = model.isTemplate() ? "quickConnect" : model.getTitleKey(); var modal = ModalOverlay.of(nameKey, comp); comp.apply(struc -> { struc.addEventHandler(KeyEvent.KEY_PRESSED, e -> { @@ -194,25 +194,23 @@ public class StoreCreationDialog { }); }); - if (!model.isQuickConnect()) { - var queueEntry = StoreCreationQueueEntry.of(model, modal); + var queueEntry = StoreCreationQueueEntry.of(model, modal); - modal.setHideAction(() -> { - AppLayoutModel.get().getQueueEntries().add(queueEntry); - showNotice(); - }); + modal.setHideAction(() -> { + AppLayoutModel.get().getQueueEntries().add(queueEntry); + showNotice(); + }); - AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { - if (model.getFinished().get() || !modal.isShowing()) { - return; - } + AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { + if (model.getFinished().get() || !modal.isShowing()) { + return; + } - modal.hide(); - AppLayoutModel.get().getQueueEntries().add(queueEntry); - showNotice(); - }); - modal.setRequireCloseButtonForClose(true); - } + modal.hide(); + AppLayoutModel.get().getQueueEntries().add(queueEntry); + showNotice(); + }); + modal.setRequireCloseButtonForClose(true); var loadingLabel = new LabelComp(Bindings.createStringBinding( () -> { @@ -232,17 +230,25 @@ public class StoreCreationDialog { .augment(button -> { button.visibleProperty().bind(Bindings.not(model.canShowDocs())); })); - modal.addButton(new ModalButton( - "connect", - () -> { - model.connect(); - }, - false, - false) - .augment(button -> { - button.visibleProperty().bind(Bindings.not(model.canConnect())); - })); - if (!model.isQuickConnect()) { + if (model.isTemplate() && !model.isQuickConnect()) { + modal.addButton(new ModalButton("saveAs", () -> { + model.commit(false); + modal.close(); + }, false, false).augment(button -> { + button.disableProperty().bind(Bindings.not(model.canSave())); + })); + } + if (!model.isTemplate()) { + modal.addButton(new ModalButton( + "connect", + () -> { + model.connect(); + }, + false, + false) + .augment(button -> { + button.visibleProperty().bind(Bindings.not(model.canConnect())); + })); modal.addButton(new ModalButton( "skip", () -> { @@ -258,7 +264,7 @@ public class StoreCreationDialog { } modal.addButton(new ModalButton( - model.isQuickConnect() ? "connect" : "finish", + model.isTemplate() ? "connect" : "finish", () -> { model.finish(); }, @@ -279,7 +285,7 @@ public class StoreCreationDialog { .bind(Bindings.createStringBinding( () -> { return !model.getBusy().get() - ? AppI18n.get(model.isQuickConnect() ? "connect" : "finish") + ? AppI18n.get(model.isTemplate() ? "connect" : "finish") : null; }, PlatformThread.sync(model.getBusy()), diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationModel.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationModel.java index 2c7d9a205..d005ce002 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationModel.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationModel.java @@ -50,6 +50,7 @@ public class StoreCreationModel { StoreCreationConsumer consumer; ObservableBooleanValue syncable; UUID uuid; + String defaultName; public StoreCreationModel( Property provider, @@ -62,8 +63,9 @@ public class StoreCreationModel { this.provider = provider; this.store = store; this.filter = filter; - this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null); this.existingEntry = existingEntry; + this.name = new SimpleStringProperty(!isTemplate() ? initialName != null && !initialName.isEmpty() ? initialName : null : null); + this.defaultName = isTemplate() && initialName != null && !initialName.isEmpty() ? initialName : null; this.existingDependencies = existingEntry != null ? DataStorage.get().getDependencies(existingEntry).stream() .map(ref -> ref.getStore()) @@ -71,7 +73,7 @@ public class StoreCreationModel { : null; this.staticDisplay = staticDisplay; this.consumer = consumer; - this.uuid = existingEntry != null ? existingEntry.getUuid() : UUID.randomUUID(); + this.uuid = existingEntry != null && !DataStorage.get().getEffectiveReadOnlyState(existingEntry) ? existingEntry.getUuid() : UUID.randomUUID(); this.provider.addListener((c, o, n) -> { store.unbind(); @@ -96,19 +98,18 @@ public class StoreCreationModel { this.entry = Bindings.createObjectBinding( () -> { - if (name.getValue() == null - || store.getValue() == null - || name.getValue().isBlank()) { + var effectiveName = name.getValue() != null ? name.getValue() : defaultName; + if (effectiveName == null || store.getValue() == null) { return null; } var initial = DataStoreEntry.createNew( - uuid, DataStorage.get().getSelectedCategory().getUuid(), name.getValue(), store.getValue()); + uuid, DataStorage.get().getSelectedCategory().getUuid(), effectiveName, store.getValue()); var entryRef = existingEntry != null ? existingEntry : DataStorage.get().getDefaultDisplayParent(initial).orElse(initial); var targetCategory = getTargetCategory(entryRef.getCategoryUuid()); - return DataStoreEntry.createNew(uuid, targetCategory.getUuid(), name.getValue(), store.getValue()); + return DataStoreEntry.createNew(uuid, targetCategory.getUuid(), effectiveName, store.getValue()); }, name, store); @@ -136,7 +137,7 @@ public class StoreCreationModel { && store.get().isComplete() && store.get() instanceof ValidatableStore) { if (existingEntry != null) { - return !existingEntry.isFreeze() + return !existingEntry.isTemplate() || !existingEntry.getName().equals(name.getValue()); } else { return true; @@ -194,8 +195,16 @@ public class StoreCreationModel { store)); } - boolean isQuickConnect() { - return existingEntry != null && existingEntry.getUuid().equals(StoreQuickConnect.STORE_ID); + ObservableBooleanValue canSave() { + return Bindings.createBooleanBinding( + () -> { + return name.getValue() != null && store.get() != null && store.get().isComplete(); + }, + name, store); + } + + boolean isTemplate() { + return existingEntry != null && (existingEntry.getUuid().equals(StoreQuickConnect.STORE_ID) || existingEntry.isTemplate()); } void connect() { @@ -204,6 +213,10 @@ public class StoreCreationModel { action.executeAsync(); } + boolean isQuickConnect() { + return existingEntry != null && existingEntry.getUuid().equals(StoreQuickConnect.STORE_ID); + } + boolean hasBeenModified() { if (initialStore.getValue() == null) { return true; @@ -253,7 +266,7 @@ public class StoreCreationModel { } // We didn't change anything - if (store.getValue().isComplete() && !wasChanged() && !isQuickConnect()) { + if (store.getValue().isComplete() && !wasChanged() && !isTemplate()) { commit(false); return; } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryComp.java index e9764e7d7..2368e0e76 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryComp.java @@ -280,6 +280,20 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder { return button; } + protected BaseRegionBuilder createTemplateIcon() { + var button = new IconButtonComp("mdal-content_copy"); + button.style("template-icon"); + button.describe(d -> d.nameKey("template")); + button.apply(struc -> { + AppFontSizes.base(struc); + struc.setDisable(true); + struc.setOpacity(1.0); + }); + button.hide(Bindings.not(getWrapper().getTemplate()).or(AppSizeBreakpoints.compactMode())); + button.apply(struc -> struc.setOpacity(0.85)); + return button; + } + protected BaseRegionBuilder createPinIcon() { var button = new IconButtonComp("mdi2p-pin-outline"); button.disable(new SimpleBooleanProperty(true)); @@ -455,30 +469,19 @@ public abstract class StoreEntryComp extends SimpleRegionBuilder { notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s == null)); items.add(items.size(), notes); - var freeze = new MenuItem(); - freeze.graphicProperty() - .bind(Bindings.createObjectBinding( - () -> { - var is = getWrapper().getReadOnly().get(); - return is - ? new FontIcon("mdi2l-lock-open-variant-outline") - : new FontIcon("mdi2l-lock-open-outline"); - }, - getWrapper().getReadOnly())); - freeze.textProperty() - .bind(Bindings.createStringBinding( - () -> { - var is = getWrapper().getReadOnly().get(); - return is - ? AppI18n.get("unfreezeConfiguration") - : AppI18n.get("freezeConfiguration"); - }, - AppI18n.activeLanguage(), - getWrapper().getReadOnly())); - freeze.setOnAction(event -> getWrapper() - .getEntry() - .setFreeze(!getWrapper().getReadOnly().get())); - items.add(freeze); + if (getWrapper().getEntry().getProvider() != null && getWrapper().getEntry().getProvider().getCreationCategory() != null) { + var template = new MenuItem(); + template.graphicProperty().bind(Bindings.createObjectBinding(() -> { + var is = getWrapper().getTemplate().get(); + return is ? new FontIcon("mdi2c-credit-card-off-outline") : new FontIcon("mdi2c-credit-card-multiple-outline"); + }, getWrapper().getTemplate())); + template.textProperty().bind(Bindings.createStringBinding(() -> { + var is = getWrapper().getTemplate().get(); + return is ? AppI18n.get("untemplateConfiguration") : AppI18n.get("templateConfiguration"); + }, AppI18n.activeLanguage(), getWrapper().getTemplate())); + template.setOnAction(event -> getWrapper().getEntry().setTemplate(!getWrapper().getTemplate().get())); + items.add(template); + } } if (cat == StoreActionCategory.DEVELOPER) { diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java index b201e1e38..a874b5191 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java @@ -69,7 +69,7 @@ public class StoreEntryWrapper { private final ObservableValue shownSummary; private final ObservableValue shownDescription; private final Property shownInformation; - private final BooleanProperty readOnly = new SimpleBooleanProperty(); + private final BooleanProperty template = new SimpleBooleanProperty(); private final BooleanProperty renaming = new SimpleBooleanProperty(); private final BooleanProperty pinToTop = new SimpleBooleanProperty(); private final IntegerProperty orderIndex = new SimpleIntegerProperty(); @@ -218,7 +218,7 @@ public class StoreEntryWrapper { orderIndex.setValue(entry.getOrderIndex()); color.setValue(entry.getColor()); notes.setValue(entry.getNotes()); - readOnly.setValue(entry.isFreeze()); + template.setValue(entry.isTemplate()); iconFile.setValue(entry.getEffectiveIconFile()); busy.setValue(entry.getBusyCounter().get() != 0); deletable.setValue( @@ -300,60 +300,60 @@ public class StoreEntryWrapper { } if (!isInStorage()) { + minorActionProviders.clear(); majorActionProviders.clear(); defaultActionProvider.setValue(null); } else { try { - var defaultProvider = ActionProvider.ALL.stream() - .filter(e -> entry.getStore() != null - && e instanceof HubLeafProvider def - && (entry.getValidity().isUsable() - || (!def.requiresValidStore() && entry.getProvider() != null)) - && def.getApplicableClass() - .isAssignableFrom(entry.getStore().getClass()) - && def.isApplicable(entry.ref()) - && def.isDefault()) - .findFirst() - .or(() -> { - if (entry.getStore() instanceof GroupStore) { - return Optional.empty(); - } else if (entry.getProvider() != null - && entry.getProvider().canConfigure()) { - return Optional.of(new EditHubLeafProvider()); - } else { - return Optional.empty(); - } - }) - .orElse(null); - this.defaultActionProvider.setValue(defaultProvider); + if (!template.get()) { + var defaultProvider = ActionProvider.ALL.stream().filter(e -> entry.getStore() != null && + e instanceof HubLeafProvider def && + (entry.getValidity().isUsable() || (!def.requiresValidStore() && entry.getProvider() != null)) && + def.getApplicableClass().isAssignableFrom(entry.getStore().getClass()) && + def.isApplicable(entry.ref()) && + def.isDefault()).findFirst().or(() -> { + if (entry.getStore() instanceof GroupStore) { + return Optional.empty(); + } else if (entry.getProvider() != null && entry.getProvider().canConfigure()) { + return Optional.of(new EditHubLeafProvider()); + } else { + return Optional.empty(); + } + }).orElse(null); + this.defaultActionProvider.setValue(defaultProvider); - var newMajorProviders = ActionProvider.ALL.stream() - .map(actionProvider -> actionProvider instanceof HubMenuItemProvider sa ? sa : null) - .filter(Objects::nonNull) - .filter(dataStoreActionProvider -> { - return showActionProvider(dataStoreActionProvider, true); - }) - .toList(); - if (!majorActionProviders.equals(newMajorProviders)) { - majorActionProviders.setAll(newMajorProviders); - } + var newMajorProviders = ActionProvider.ALL.stream() + .map(actionProvider -> actionProvider instanceof HubMenuItemProvider sa ? sa : null) + .filter(Objects::nonNull) + .filter(dataStoreActionProvider -> { + return showActionProvider(dataStoreActionProvider, true); + }) + .toList(); + if (!majorActionProviders.equals(newMajorProviders)) { + majorActionProviders.setAll(newMajorProviders); + } - var newMinorProviders = ActionProvider.ALL.stream() - .map(actionProvider -> actionProvider instanceof HubMenuItemProvider sa ? sa : null) - .filter(Objects::nonNull) - .filter(dataStoreActionProvider -> { - return showActionProvider(dataStoreActionProvider, false); - }) - .collect(Collectors.toCollection(ArrayList::new)); - newMinorProviders.removeIf(storeActionProvider -> { - return newMajorProviders.stream().anyMatch(mj -> { - return mj instanceof HubBranchProvider branch - && branch.getChildren(entry.ref()).stream() - .anyMatch(c -> c.getClass().equals(storeActionProvider.getClass())); + var newMinorProviders = ActionProvider.ALL.stream() + .map(actionProvider -> actionProvider instanceof HubMenuItemProvider sa ? sa : null) + .filter(Objects::nonNull) + .filter(dataStoreActionProvider -> { + return showActionProvider(dataStoreActionProvider, false); + }) + .collect(Collectors.toCollection(ArrayList::new)); + newMinorProviders.removeIf(storeActionProvider -> { + return newMajorProviders.stream().anyMatch(mj -> { + return mj instanceof HubBranchProvider branch + && branch.getChildren(entry.ref()).stream() + .anyMatch(c -> c.getClass().equals(storeActionProvider.getClass())); + }); }); - }); - if (!minorActionProviders.equals(newMinorProviders)) { - minorActionProviders.setAll(newMinorProviders); + if (!minorActionProviders.equals(newMinorProviders)) { + minorActionProviders.setAll(newMinorProviders); + } + } else { + minorActionProviders.clear(); + majorActionProviders.clear(); + this.defaultActionProvider.setValue(new EditHubLeafProvider()); } } catch (Exception ex) { ErrorEventFactory.fromThrowable(ex).omit().handle(); diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java index c2c7e41c9..5223ad6ad 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterFieldComp.java @@ -53,9 +53,6 @@ public class StoreFilterFieldComp extends SimpleRegionBuilder { field.focusedProperty().subscribe(focus -> { if (focus) { popover.hide(); - Platform.runLater(() -> { - field.selectAll(); - }); } else { state.onFocusLost(); } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java index 02168c9be..2cc24f7af 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreFilterStateComp.java @@ -56,13 +56,11 @@ public class StoreFilterStateComp extends SimpleRegionBuilder { var quickConnectionsList = new ListBoxViewComp<>(quickConnections, quickConnections, s -> createButton(s, s), false); - var quickConnectionsPlaceholders = FXCollections.observableArrayList(QuickConnectProvider.getAll().stream() - .map(p -> p.getPlaceholder()) - .toList()); + var quickConnectionsPlaceholders = FXCollections.observableArrayList(QuickConnectProvider.getAll()); var quickConnectionsEmptyList = new ListBoxViewComp<>( quickConnectionsPlaceholders, quickConnectionsPlaceholders, - s -> createButton(s, s.split(" ")[0] + " "), + p -> createButton(p.getPlaceholder(), p.getTemplate()), false); var options = new OptionsBuilder() diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java index ae7e467a4..6c790331f 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreQuickConnect.java @@ -83,16 +83,19 @@ public class StoreQuickConnect { StoreCreationDialog.showEdit(quickConnectEntry, newStore, false, true, finished -> { update(finished.getStore()); - ThreadHelper.runAsync(() -> { - try { - DataStorage.get().addStoreEntryInProgress(quickConnectEntry); - provider.get().open(quickConnectEntry); - } catch (Exception e) { - ErrorEventFactory.fromThrowable(e).handle(); - } finally { - DataStorage.get().removeStoreEntryInProgress(quickConnectEntry); - } - }); + var saved = !finished.getName().equals("quick-connect"); + if (!saved) { + ThreadHelper.runAsync(() -> { + try { + DataStorage.get().addStoreEntryInProgress(quickConnectEntry); + provider.get().open(quickConnectEntry); + } catch (Exception e) { + ErrorEventFactory.fromThrowable(e).handle(); + } finally { + DataStorage.get().removeStoreEntryInProgress(quickConnectEntry); + } + }); + } }); return true; diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreSectionBaseComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreSectionBaseComp.java index 6e8d2999e..dc0c2af35 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreSectionBaseComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreSectionBaseComp.java @@ -85,6 +85,10 @@ public abstract class StoreSectionBaseComp extends RegionBuilder { BindingsHelper.attach(vbox, section.getWrapper().getPerUser(), val -> { vbox.pseudoClassStateChanged(PseudoClass.getPseudoClass("per-user"), val); }); + + BindingsHelper.attach(vbox, section.getWrapper().getTemplate(), val -> { + vbox.pseudoClassStateChanged(PseudoClass.getPseudoClass("template"), val); + }); } } diff --git a/app/src/main/java/io/xpipe/app/platform/BindingsHelper.java b/app/src/main/java/io/xpipe/app/platform/BindingsHelper.java index 46977d492..1284b85cc 100644 --- a/app/src/main/java/io/xpipe/app/platform/BindingsHelper.java +++ b/app/src/main/java/io/xpipe/app/platform/BindingsHelper.java @@ -66,7 +66,13 @@ public class BindingsHelper { ObservableValue observableValue, Function> mapper) { var prop = new SimpleObjectProperty(); Runnable runnable = () -> { - prop.bind(mapper.apply(observableValue.getValue())); + var observable = mapper.apply(observableValue.getValue()); + if (observable != null) { + prop.bind(observable); + } else { + prop.unbind(); + prop.setValue(null); + } }; runnable.run(); observableValue.addListener((observable, oldValue, newValue) -> { diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java index 8eb69d1b8..495771496 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java @@ -103,10 +103,14 @@ public class AppPrefsComp extends SimpleRegionBuilder { private double computeCategoryOffset(Region box, ScrollPane scrollPane, AppPrefsCategory val) { var node = val != null ? box.lookup("." + val.getId()) : null; if (node != null && scrollPane.getHeight() > 0.0) { + var minY = node.getBoundsInParent().getMinY(); + if (minY <= 40.0) { + minY = 0.0; + } var s = Math.min( box.getHeight(), - node.getBoundsInParent().getMinY() > 0.0 - ? node.getBoundsInParent().getMinY() + 20 + minY > 0.0 + ? minY + 20 : 0.0) / box.getHeight(); var off = (scrollPane.getHeight() * s * 1.02) / box.getHeight(); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 8edfdb3d9..8a68390ce 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -1226,7 +1226,7 @@ public abstract class DataStorage { } var catConfig = getEffectiveCategoryConfig(cat.get()); - return catConfig.getFreezeConfigurations() != null ? catConfig.getFreezeConfigurations() : entry.isFreeze(); + return catConfig.getFreezeConfigurations() != null ? catConfig.getFreezeConfigurations() : entry.isTemplate(); } public DataStoreColor getEffectiveColor(DataStoreEntry entry) { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index 26ad32fc1..b55f36e94 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -73,7 +73,7 @@ public class DataStoreEntry extends StorageElement { @NonFinal @Getter - boolean freeze; + boolean template; @NonFinal @Getter @@ -105,7 +105,7 @@ public class DataStoreEntry extends StorageElement { DataStoreColor color, String notes, String icon, - boolean freeze, + boolean template, boolean pinToTop, int orderIndex, UUID breakOutCategory, @@ -120,7 +120,7 @@ public class DataStoreEntry extends StorageElement { this.validity = this.provider != null ? validity : Validity.LOAD_FAILED; this.storePersistentStateNode = storePersistentState; this.notes = notes; - this.freeze = freeze; + this.template = template; this.pinToTop = pinToTop; this.orderIndex = orderIndex; this.breakOutCategory = breakOutCategory; @@ -256,7 +256,7 @@ public class DataStoreEntry extends StorageElement { } }) .orElse(null); - var freeze = Optional.ofNullable(json.get("freeze")) + var template = Optional.ofNullable(json.get("template")) .map(jsonNode -> jsonNode.booleanValue()) .orElse(false); var pinToTop = Optional.ofNullable(json.get("pinToTop")) @@ -350,7 +350,7 @@ public class DataStoreEntry extends StorageElement { color, notes, icon, - freeze, + template, pinToTop, orderIndex, breakOutCategory, @@ -543,7 +543,7 @@ public class DataStoreEntry extends StorageElement { obj.put("breakOutCategoryUuid", breakOutCategory != null ? breakOutCategory.toString() : null); obj.set("color", mapper.valueToTree(color)); obj.set("icon", mapper.valueToTree(icon)); - obj.put("freeze", freeze); + obj.put("template", template); obj.put("pinToTop", pinToTop); obj.put("orderIndex", orderIndex); @@ -597,9 +597,9 @@ public class DataStoreEntry extends StorageElement { } } - public void setFreeze(boolean newValue) { - var changed = freeze != newValue; - this.freeze = newValue; + public void setTemplate(boolean newValue) { + var changed = template != newValue; + this.template = newValue; if (changed) { notifyUpdate(false, true); } diff --git a/app/src/main/java/io/xpipe/app/util/RemoteDesktopWindow.java b/app/src/main/java/io/xpipe/app/util/RemoteDesktopWindow.java index 59812e636..c38733558 100644 --- a/app/src/main/java/io/xpipe/app/util/RemoteDesktopWindow.java +++ b/app/src/main/java/io/xpipe/app/util/RemoteDesktopWindow.java @@ -287,7 +287,7 @@ public class RemoteDesktopWindow { () -> { var selected = this.selected.get(); if (selected == null) { - return "Remote Desktop Dock"; + return "XPipe - Remote Desktop Connection"; } var name = selected.getName(); diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css index 026c2c30a..45ed04c3b 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css @@ -71,6 +71,10 @@ -fx-icon-color: #5b2cffff; } +.store-entry-grid .template-icon .ikonli-font-icon { + -fx-icon-color: #E6A616CC; +} + .store-entry-grid .icon > .background { -fx-background-radius: 5px; -fx-background-color: -color-bg-overlay; @@ -207,6 +211,10 @@ -fx-border-color: #5b2cffcc; } +.root .store-entry-section-comp.color-box.vertical-comp:template { + -fx-border-color: #E6A616CC; +} + /* Light sub backgrounds */ .root:light .store-entry-section-comp:sub:expanded { diff --git a/dist/changelog/24.0.md b/dist/changelog/24.0.md new file mode 100644 index 000000000..cf1e223ff --- /dev/null +++ b/dist/changelog/24.0.md @@ -0,0 +1,35 @@ +## Wake-on-LAN + +This release introduces support for Wake-on-LAN: + +![WOL](https://xpipe.io/assets/images/BlogPage/wol.png) + +You can create a Wake-on-LAN entry for any network interface and broadcast address. For convenience, there is an automatic finder for available network interfaces and broadcast addresses on a system, so you won't have to fill in anything manually: + +![WOL config](https://xpipe.io/assets/images/BlogPage/wol-config.png) + +The Wake-on-LAN support works through shell sessions, meaning that it also supports remote networks and is not limited to your local one. + +## Connection templates + +You can now create connection templates to allow for a more dynamic connection usage without having to create new entries every time. Any connection can be designated as a template: + +![Template](https://xpipe.io/assets/images/BlogPage/template.png) + +You can then use this template to connect to an individual system directly without having to store this connection configuration. You can still do so if you want to however. + +## macOS container machines + +The latest release of the [apple container runtime](https://github.com/apple/container) introduces support for container machines, which is essentially just WSL for macOS. This releases adds support for those environments as well: + +![Container Machines](https://xpipe.io/assets/images/BlogPage/container-machines.png) + +## Other + +- There are now multiple new UI themes available for you to choose from +- The API endpoints for /connection/... have been renamed to /store/... + +## Fixes + + + diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 87d85fc15..ab7ec3524 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -124,6 +124,7 @@ hostFeatureUnsupported=$FEATURE$ is not installed on the host missingStore=$NAME$ does not exist connectionName=Connection name connectionNameDescription=Give this connection a custom name +connectionTemplateNameDescription=Save a copy of this connection template with a custom name openFileTitle=Open file openDirectoryTitle=Open directory saveFileTitle=Save file @@ -1398,10 +1399,11 @@ tailscalePassword=Password tailscalePasswordDescription=The user password that can be used for sudo scriptName=Script name scriptNameDescription=Give this script a custom name +scriptTemplateNameDescription=Save a copy of this script template with a custom name scriptGroupName=Script group name scriptGroupNameDescription=Give this script group a custom name identityName=Identity name -identityNameDescription=Give this identity a custom name +identityTemplateNameDescription=Save a copy of this connection template with a custom name #context: Tailscale VPN tailscaleTailnet.displayName=Tailnet #context: Tailscale VPN @@ -1555,7 +1557,6 @@ gitPasswordDescription=The password or personal access token to use to authentic gitPasswordValue=Password / personal access token setReadOnly=Set read-only unsetReadOnly=Unset read-only -readOnlyStoreError=This entry's configuration is frozen. Choose a different name to save your changes to a new copy. categoryFreeze=Freeze connection configurations categoryFreezeDescription=Marks connection configurations as read-only. This means that no existing connection entry configuration in this category can be modified. New connections can be added though. updateFail=Update installation did not succeed @@ -1635,8 +1636,9 @@ moveToLast=Move to last category=Category includeRoot=Include root excludeRoot=Exclude root -freezeConfiguration=Freeze configuration -unfreezeConfiguration=Unfreeze configuration +templateConfiguration=Make template +untemplateConfiguration=Unset template +template=Template waylandScalingTitle=Wayland scaling actionApiUrl=$URL$ (Copy json body) copyBody=Copy request body @@ -2010,6 +2012,8 @@ scriptSourceName=Script name scriptSourceNameDescription=The file name of the script in the source workspaceRestartTitle=Workspace ready workspaceRestartContent=The new workspace was created and a desktop shortcut was created for it. You can navigate to the shortcut or restart XPipe now to open the new workspace automatically. +#force +#context: verb, to browse browseShortcut=Browse shortcut syncModeInstant=Sync instantly syncModeSession=Sync on startup and exit @@ -2274,4 +2278,9 @@ webtopSettingsApplyContent=Do you want to transfer your local XPipe settings to deploy=Deploy #context: verb transferSettings=Transfer +webtopDeployStatusPull=Pulling image ... +webtopDeployStatusCreate=Creating container ... +webtopDeployStatusTest=Testing container ... +webtopDeployStatusTransferSettings=Applying state ... +saveAs=Save as