This commit is contained in:
crschnick
2026-06-20 10:30:50 +00:00
parent 3763f18fe6
commit f03d16dbe2
25 changed files with 267 additions and 164 deletions

View File

@@ -36,6 +36,8 @@ public interface QuickConnectProvider extends ActionProvider {
String getPlaceholder();
String getTemplate();
boolean skipDialogIfPossible();
default void open(DataStoreEntry e) {

View File

@@ -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<DaemonOpenExchange.Request> {
@@ -31,6 +35,14 @@ public class DaemonOpenExchange extends BeaconInterface<DaemonOpenExchange.Reque
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {
if (msg.getDirectory() != null && !AppProperties.get().getDataDir().equals(msg.getDirectory())) {
ThreadHelper.runAsync(() -> {
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<DaemonOpenExchange.Reque
public static class Request {
@NonNull
List<String> arguments;
Path directory;
}
@Jacksonized
@Builder
@Value
public static class Response {}
public static class Response {
boolean restartRequired;
}
}

View File

@@ -53,7 +53,7 @@ public class AppI18n {
}
public static ObservableValue<String> observable(ObservableValue<String> 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) {

View File

@@ -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();

View File

@@ -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);

View File

@@ -62,7 +62,7 @@ public class CloneHubLeafProvider implements HubLeafProvider<DataStore> {
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());

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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()),

View File

@@ -50,6 +50,7 @@ public class StoreCreationModel {
StoreCreationConsumer consumer;
ObservableBooleanValue syncable;
UUID uuid;
String defaultName;
public StoreCreationModel(
Property<DataStoreProvider> 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()
.<DataStore>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;
}

View File

@@ -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) {

View File

@@ -69,7 +69,7 @@ public class StoreEntryWrapper {
private final ObservableValue<String> shownSummary;
private final ObservableValue<String> shownDescription;
private final Property<String> 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();

View File

@@ -53,9 +53,6 @@ public class StoreFilterFieldComp extends SimpleRegionBuilder {
field.focusedProperty().subscribe(focus -> {
if (focus) {
popover.hide();
Platform.runLater(() -> {
field.selectAll();
});
} else {
state.onFocusLost();
}

View File

@@ -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()

View File

@@ -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;

View File

@@ -85,6 +85,10 @@ public abstract class StoreSectionBaseComp extends RegionBuilder<VBox> {
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);
});
}
}

View File

@@ -66,7 +66,13 @@ public class BindingsHelper {
ObservableValue<T> observableValue, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
var prop = new SimpleObjectProperty<U>();
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) -> {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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 {

35
dist/changelog/24.0.md vendored Normal file
View File

@@ -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

View File

@@ -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