SSH config fixes

This commit is contained in:
crschnick 2024-06-07 08:38:07 +00:00
parent bc9b962be9
commit 880b17c7c1
61 changed files with 650 additions and 788 deletions

View file

@ -108,7 +108,6 @@ run {
} }
workingDir = rootDir workingDir = rootDir
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
} }
task runAttachedDebugger(type: JavaExec) { task runAttachedDebugger(type: JavaExec) {
@ -122,7 +121,7 @@ task runAttachedDebugger(type: JavaExec) {
"-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost".toString(), "-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost".toString(),
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0" "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0"
) )
jvmArgs += ['-XX:+EnableDynamicAgentLoading'] jvmArgs += '-XX:+EnableDynamicAgentLoading'
systemProperties run.systemProperties systemProperties run.systemProperties
} }

View file

@ -6,16 +6,18 @@ import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import java.util.List; import java.util.List;
@ -68,8 +70,9 @@ public class BrowserOverviewComp extends SimpleComp {
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true).mapped( var recent = ListBindingsHelper.mappedContentBinding(
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())).getList(); model.getSavedState().getRecentDirectories(),
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
var recentOverview = new BrowserFileOverviewComp(model, recent, true); var recentOverview = new BrowserFileOverviewComp(model, recent, true);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview); var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview);

View file

@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -47,7 +47,7 @@ public class BrowserTransferComp extends SimpleComp {
var backgroundStack = var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = new DerivedObservableList<>(syncItems, true).mapped(item -> item.getBrowserEntry()).getList(); var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry());
var list = new BrowserSelectionListComp( var list = new BrowserSelectionListComp(
binding, binding,
entry -> Bindings.createStringBinding( entry -> Bindings.createStringBinding(

View file

@ -1,7 +1,5 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.ListBoxViewComp;
@ -15,9 +13,10 @@ import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.PrettySvgComp; import io.xpipe.app.fxcomps.impl.PrettySvgComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -31,6 +30,9 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.util.List; import java.util.List;
public class BrowserWelcomeComp extends SimpleComp { public class BrowserWelcomeComp extends SimpleComp {
@ -65,7 +67,7 @@ public class BrowserWelcomeComp extends SimpleComp {
return new VBox(hbox); return new VBox(hbox);
} }
var list = new DerivedObservableList<>(state.getEntries(), true).filtered(e -> { var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) { if (entry.isEmpty()) {
return false; return false;
@ -76,7 +78,7 @@ public class BrowserWelcomeComp extends SimpleComp {
} }
return true; return true;
}).getList(); });
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var headerBinding = BindingsHelper.flatMap(empty, b -> { var headerBinding = BindingsHelper.flatMap(empty, b -> {

View file

@ -24,10 +24,6 @@ public class BrowserEntry {
} }
private static BrowserIconFileType fileType(FileSystem.FileEntry rawFileEntry) { private static BrowserIconFileType fileType(FileSystem.FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
if (rawFileEntry.getKind() == FileKind.DIRECTORY) { if (rawFileEntry.getKind() == FileKind.DIRECTORY) {
return null; return null;
} }
@ -42,10 +38,6 @@ public class BrowserEntry {
} }
private static BrowserIconDirectoryType directoryType(FileSystem.FileEntry rawFileEntry) { private static BrowserIconDirectoryType directoryType(FileSystem.FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
if (rawFileEntry.getKind() != FileKind.DIRECTORY) { if (rawFileEntry.getKind() != FileKind.DIRECTORY) {
return null; return null;
} }

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser.session;
import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileReference; import io.xpipe.app.util.FileReference;
@ -10,10 +10,12 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction; import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -38,8 +40,7 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
return; return;
} }
var l = new DerivedObservableList<>(fileSelection, true); ListBindingsHelper.bindContent(fileSelection, newValue.getFileList().getSelection());
l.bindContent(newValue.getFileList().getSelection());
}); });
} }

View file

@ -4,9 +4,8 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.css.Size; import javafx.css.Size;
import javafx.css.SizeUnits; import javafx.css.SizeUnits;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -39,13 +38,10 @@ public class DropdownComp extends Comp<CompStructure<Button>> {
})) }))
.createRegion(); .createRegion();
List<? extends ObservableValue<Boolean>> l = cm.getItems().stream()
.map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList();
button.visibleProperty() button.visibleProperty()
.bind(Bindings.createBooleanBinding(() -> { .bind(ListBindingsHelper.anyMatch(cm.getItems().stream()
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); .map(menuItem -> menuItem.getGraphic().visibleProperty())
}, l.toArray(ObservableValue[]::new))); .toList()));
var graphic = new FontIcon("mdi2c-chevron-double-down"); var graphic = new FontIcon("mdi2c-chevron-double-down");
button.fontProperty().subscribe(c -> { button.fontProperty().subscribe(c -> {

View file

@ -3,8 +3,9 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -88,8 +89,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
} }
if (!listView.getChildren().equals(newShown)) { if (!listView.getChildren().equals(newShown)) {
var d = new DerivedObservableList<>(listView.getChildren(), true); ListBindingsHelper.setContent(listView.getChildren(), newShown);
d.setContent(newShown);
} }
}; };

View file

@ -52,6 +52,7 @@ public class StoreToggleComp extends SimpleComp {
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
v -> { v -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
section.getWrapper().refreshChildren();
}); });
} }

View file

@ -60,7 +60,7 @@ public class StoreCategoryWrapper {
} }
public StoreCategoryWrapper getParent() { public StoreCategoryWrapper getParent() {
return StoreViewState.get().getCategories().getList().stream() return StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory())) storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory()))
.findAny() .findAny()
@ -122,7 +122,7 @@ public class StoreCategoryWrapper {
sortMode.setValue(category.getSortMode()); sortMode.setValue(category.getSortMode());
share.setValue(category.isShare()); share.setValue(category.isShare());
containedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream() containedEntries.setAll(StoreViewState.get().getAllEntries().stream()
.filter(entry -> { .filter(entry -> {
return entry.getEntry().getCategoryUuid().equals(category.getUuid()) return entry.getEntry().getCategoryUuid().equals(category.getUuid())
|| (AppPrefs.get() || (AppPrefs.get()
@ -132,7 +132,7 @@ public class StoreCategoryWrapper {
.anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry))); .anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)));
}) })
.toList()); .toList());
children.setAll(StoreViewState.get().getCategories().getList().stream() children.setAll(StoreViewState.get().getCategories().stream()
.filter(storeCategoryWrapper -> getCategory() .filter(storeCategoryWrapper -> getCategory()
.getUuid() .getUuid()
.equals(storeCategoryWrapper.getCategory().getParentCategory())) .equals(storeCategoryWrapper.getCategory().getParentCategory()))

View file

@ -286,6 +286,10 @@ public class StoreCreationComp extends DialogComp {
if (ex instanceof ValidationException) { if (ex instanceof ValidationException) {
ErrorEvent.expected(ex); ErrorEvent.expected(ex);
skippable.set(false); skippable.set(false);
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
skippable.set(false);
} else { } else {
skippable.set(true); skippable.set(true);
} }

View file

@ -372,14 +372,6 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().add(new SeparatorMenuItem());
} }
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
notes.setOnAction(event -> {
wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
event.consume();
});
notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null));
contextMenu.getItems().add(notes);
if (AppPrefs.get().developerMode().getValue()) { if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline")); var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
browse.setOnAction( browse.setOnAction(
@ -387,6 +379,26 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(browse); contextMenu.getItems().add(browse);
} }
if (wrapper.getEntry().getProvider() != null) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals(wrapper.getCategory().getValue())) {
m.setDisable(true);
}
move.getItems().add(m);
});
contextMenu.getItems().add(move);
}
if (DataStorage.get().isRootEntry(wrapper.getEntry())) { if (DataStorage.get().isRootEntry(wrapper.getEntry())) {
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill")); var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
var none = new MenuItem("None"); var none = new MenuItem("None");
@ -406,72 +418,13 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(color); contextMenu.getItems().add(color);
} }
if (wrapper.getEntry().getProvider() != null) { var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); notes.setOnAction(event -> {
StoreViewState.get() wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.getList()
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem();
m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume();
});
if (storeCategoryWrapper.getParent() == null) {
m.setDisable(true);
}
move.getItems().add(m);
});
contextMenu.getItems().add(move);
}
var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks"));
var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal"));
noOrder.setOnAction(event -> {
DataStorage.get().orderBefore(wrapper.getEntry(), null);
event.consume(); event.consume();
}); });
if (wrapper.getEntry().getOrderBefore() == null) { notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null));
noOrder.setDisable(true); contextMenu.getItems().add(notes);
}
order.getItems().add(noOrder);
order.getItems().add(new SeparatorMenuItem());
var stick = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending"));
stick.setOnAction(event -> {
DataStorage.get().orderBefore(wrapper.getEntry(), wrapper.getEntry());
event.consume();
});
if (wrapper.getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())) {
stick.setDisable(true);
}
order.getItems().add(stick);
order.getItems().add(new SeparatorMenuItem());
var desc = new MenuItem(AppI18n.get("orderAheadOf"), new FontIcon("mdi2o-order-bool-descending-variant"));
desc.setDisable(true);
order.getItems().add(desc);
var section = StoreViewState.get().getParentSectionForWrapper(wrapper);
if (section.isPresent()) {
section.get().getAllChildren().getList().forEach(other -> {
var ow = other.getWrapper();
var op = ow.getEntry().getProvider();
MenuItem m = new MenuItem(ow.getName().getValue(),
op != null ? PrettyImageHelper.ofFixedSizeSquare(op.getDisplayIconFileName(ow.getEntry().getStore()),
16).createRegion() : null);
if (other.getWrapper().equals(wrapper) || ow.getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())) {
m.setDisable(true);
}
m.setOnAction(event -> {
wrapper.orderBefore(ow);
event.consume();
});
order.getItems().add(m);
});
}
contextMenu.getItems().add(order);
contextMenu.getItems().add(new SeparatorMenuItem());
var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline"));
del.disableProperty() del.disableProperty()

View file

@ -18,8 +18,8 @@ public class StoreEntryListComp extends SimpleComp {
private Comp<?> createList() { private Comp<?> createList() {
var content = new ListBoxViewComp<>( var content = new ListBoxViewComp<>(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList(), StoreViewState.get().getCurrentTopLevelSection().getShownChildren(),
StoreViewState.get().getCurrentTopLevelSection().getAllChildren().getList(), StoreViewState.get().getCurrentTopLevelSection().getAllChildren(),
(StoreSection e) -> { (StoreSection e) -> {
var custom = StoreSection.customSection(e, true).hgrow(); var custom = StoreSection.customSection(e, true).hgrow();
return new HorizontalComp(List.of(Comp.hspacer(8), custom, Comp.hspacer(10))) return new HorizontalComp(List.of(Comp.hspacer(8), custom, Comp.hspacer(10)))
@ -35,7 +35,7 @@ public class StoreEntryListComp extends SimpleComp {
var showIntro = Bindings.createBooleanBinding( var showIntro = Bindings.createBooleanBinding(
() -> { () -> {
var all = StoreViewState.get().getAllConnectionsCategory(); var all = StoreViewState.get().getAllConnectionsCategory();
var connections = StoreViewState.get().getAllEntries().getList().stream() var connections = StoreViewState.get().getAllEntries().stream()
.filter(wrapper -> all.contains(wrapper)) .filter(wrapper -> all.contains(wrapper))
.toList(); .toList();
return initialCount == connections.size() return initialCount == connections.size()
@ -45,21 +45,21 @@ public class StoreEntryListComp extends SimpleComp {
.getRoot() .getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory()); .equals(StoreViewState.get().getAllConnectionsCategory());
}, },
StoreViewState.get().getAllEntries().getList(), StoreViewState.get().getAllEntries(),
StoreViewState.get().getActiveCategory()); StoreViewState.get().getActiveCategory());
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>(); var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put( map.put(
createList(), createList(),
Bindings.not(Bindings.isEmpty( Bindings.not(Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList()))); StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
map.put(new StoreIntroComp(), showIntro); map.put(new StoreIntroComp(), showIntro);
map.put( map.put(
new StoreNotFoundComp(), new StoreNotFoundComp(),
Bindings.and( Bindings.and(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries().getList())), Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
Bindings.isEmpty( Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList()))); StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
return new MultiContentComp(map).createRegion(); return new MultiContentComp(map).createRegion();
} }
} }

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -55,24 +56,25 @@ public class StoreEntryListStatusComp extends SimpleComp {
label.textProperty().bind(name); label.textProperty().bind(name);
label.getStyleClass().add("name"); label.getStyleClass().add("name");
var all = StoreViewState.get().getAllEntries().filtered( var all = ListBindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
storeEntryWrapper -> { storeEntryWrapper -> {
var rootCategory = storeEntryWrapper.getCategory().getValue().getRoot(); var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory); return StoreViewState.get()
// Sadly the all binding does not update when the individual visibility of entries changes .getActiveCategory()
// But it is good enough. .getValue()
var showProvider = storeEntryWrapper.getEntry().getProvider() == null || .getRoot()
storeEntryWrapper.getEntry().getProvider().shouldShow(storeEntryWrapper); .equals(storeRoot);
return inRootCategory && showProvider;
}, },
StoreViewState.get().getActiveCategory()); StoreViewState.get().getActiveCategory());
var shownList = all.filtered( var shownList = ListBindingsHelper.filteredContentBinding(
all,
storeEntryWrapper -> { storeEntryWrapper -> {
return storeEntryWrapper.shouldShow( return storeEntryWrapper.shouldShow(
StoreViewState.get().getFilterString().getValue()); StoreViewState.get().getFilterString().getValue());
}, },
StoreViewState.get().getFilterString()); StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList.getList(), all.getList()); var count = new CountComp<>(shownList, all);
var c = count.createRegion(); var c = count.createRegion();
var topBar = new HBox( var topBar = new HBox(

View file

@ -9,13 +9,17 @@ import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.Observable;
import javafx.beans.property.*; import javafx.beans.property.*;
import lombok.Getter; import lombok.Getter;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Getter @Getter
public class StoreEntryWrapper { public class StoreEntryWrapper {
@ -61,22 +65,12 @@ public class StoreEntryWrapper {
setupListeners(); setupListeners();
} }
public List<Observable> getUpdateObservables() {
return List.of(category);
}
public void moveTo(DataStoreCategory category) { public void moveTo(DataStoreCategory category) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
DataStorage.get().updateCategory(entry, category); DataStorage.get().updateCategory(entry, category);
}); });
} }
public void orderBefore(StoreEntryWrapper other) {
ThreadHelper.runAsync(() -> {
DataStorage.get().orderBefore(getEntry(),other.getEntry());
});
}
public boolean isInStorage() { public boolean isInStorage() {
return DataStorage.get().getStoreEntries().contains(entry); return DataStorage.get().getStoreEntries().contains(entry);
} }

View file

@ -26,7 +26,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
} }
private ContextMenu createMenu() { private ContextMenu createMenu() {
if (section.getShownChildren().getList().isEmpty()) { if (section.getShownChildren().isEmpty()) {
return null; return null;
} }
@ -42,7 +42,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
var w = section.getWrapper(); var w = section.getWrapper();
var graphic = var graphic =
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore()); w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
if (c.getList().isEmpty()) { if (c.isEmpty()) {
var item = ContextMenuHelper.item( var item = ContextMenuHelper.item(
PrettyImageHelper.ofFixedSizeSquare(graphic, 16), PrettyImageHelper.ofFixedSizeSquare(graphic, 16),
w.getName().getValue()); w.getName().getValue());
@ -55,7 +55,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
} }
var items = new ArrayList<MenuItem>(); var items = new ArrayList<MenuItem>();
for (StoreSection sub : c.getList()) { for (StoreSection sub : c) {
if (!sub.getWrapper().getValidity().getValue().isUsable()) { if (!sub.getWrapper().getValidity().getValue().isUsable()) {
continue; continue;
} }

View file

@ -2,37 +2,37 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableStringValue; import javafx.beans.value.ObservableStringValue;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Value; import lombok.Value;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.ToIntFunction;
@Value @Value
public class StoreSection { public class StoreSection {
StoreEntryWrapper wrapper; StoreEntryWrapper wrapper;
DerivedObservableList<StoreSection> allChildren; ObservableList<StoreSection> allChildren;
DerivedObservableList<StoreSection> shownChildren; ObservableList<StoreSection> shownChildren;
int depth; int depth;
ObservableBooleanValue showDetails; ObservableBooleanValue showDetails;
public StoreSection( public StoreSection(
StoreEntryWrapper wrapper, StoreEntryWrapper wrapper,
DerivedObservableList<StoreSection> allChildren, ObservableList<StoreSection> allChildren,
DerivedObservableList<StoreSection> shownChildren, ObservableList<StoreSection> shownChildren,
int depth) { int depth) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.allChildren = allChildren; this.allChildren = allChildren;
@ -41,10 +41,10 @@ public class StoreSection {
if (wrapper != null) { if (wrapper != null) {
this.showDetails = Bindings.createBooleanBinding( this.showDetails = Bindings.createBooleanBinding(
() -> { () -> {
return wrapper.getExpanded().get() || allChildren.getList().isEmpty(); return wrapper.getExpanded().get() || allChildren.isEmpty();
}, },
wrapper.getExpanded(), wrapper.getExpanded(),
allChildren.getList()); allChildren);
} else { } else {
this.showDetails = new SimpleBooleanProperty(true); this.showDetails = new SimpleBooleanProperty(true);
} }
@ -59,77 +59,51 @@ public class StoreSection {
} }
} }
private static DerivedObservableList<StoreSection> sorted( private static ObservableList<StoreSection> sorted(
DerivedObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) { ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
if (category == null) { if (category == null) {
return list; return list;
} }
var explicitOrderComp = Comparator.<StoreSection>comparingInt(new ToIntFunction<>() { var c = Comparator.<StoreSection>comparingInt(
@Override
public int applyAsInt(StoreSection value) {
var explicit = value.getWrapper().getEntry().getOrderBefore();
if (explicit == null) {
return 1;
}
if (explicit.equals(value.getWrapper().getEntry().getUuid())) {
return Integer.MIN_VALUE;
}
return -count(value.getWrapper(), new HashSet<>());
}
private int count(StoreEntryWrapper wrapper, Set<StoreEntryWrapper> seen) {
if (seen.contains(wrapper)) {
// Loop!
return 0;
}
seen.add(wrapper);
var found = list.getList().stream().filter(section -> wrapper.getEntry().getOrderBefore().equals(section.getWrapper().getEntry().getUuid())).findFirst();
if (found.isPresent()) {
return count(found.get().getWrapper(), seen);
} else {
return seen.size();
}
}
});
var usableComp = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
var comp = explicitOrderComp.thenComparing(usableComp);
var mappedSortMode = BindingsHelper.flatMap( var mappedSortMode = BindingsHelper.flatMap(
category, category,
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return list.sorted((o1, o2) -> { return ListBindingsHelper.orderedContentBinding(
list,
(o1, o2) -> {
var current = mappedSortMode.getValue(); var current = mappedSortMode.getValue();
if (current != null) { if (current != null) {
return comp.thenComparing(current.comparator()) return c.thenComparing(current.comparator())
.compare(current.representative(o1), current.representative(o2)); .compare(current.representative(o1), current.representative(o2));
} else { } else {
return comp.compare(o1, o2); return c.compare(o1, o2);
} }
}, },
mappedSortMode, mappedSortMode);
StoreViewState.get().getEntriesOrderChangeObservable());
} }
public static StoreSection createTopLevel( public static StoreSection createTopLevel(
DerivedObservableList<StoreEntryWrapper> all, ObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter, Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString, ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) { ObservableValue<StoreCategoryWrapper> category) {
var topLevel = all.filtered(section -> { var topLevel = ListBindingsHelper.filteredContentBinding(
all,
section -> {
return DataStorage.get().isRootEntry(section.getEntry()); return DataStorage.get().isRootEntry(section.getEntry());
}, },
category, category);
StoreViewState.get().getEntriesListChangeObservable()); var cached = ListBindingsHelper.cachedMappedContentBinding(
var cached = topLevel.mapped( topLevel,
topLevel,
storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category); var ordered = sorted(cached, category);
var shown = ordered.filtered( var shown = ListBindingsHelper.filteredContentBinding(
ordered,
section -> { section -> {
var showFilter = filterString == null || section.matchesFilter(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter); var matchesSelector = section.anyMatches(entryFilter);
var sameCategory = category == null var sameCategory = category == null
|| category.getValue() == null || category.getValue() == null
@ -144,17 +118,15 @@ public class StoreSection {
private static StoreSection create( private static StoreSection create(
StoreEntryWrapper e, StoreEntryWrapper e,
int depth, int depth,
DerivedObservableList<StoreEntryWrapper> all, ObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter, Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString, ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) { ObservableValue<StoreCategoryWrapper> category) {
if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return new StoreSection(e, new DerivedObservableList<>( return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
FXCollections.observableArrayList(), true), new DerivedObservableList<>(
FXCollections.observableArrayList(), true), depth);
} }
var allChildren = all.filtered(other -> { var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> {
// Legacy implementation that does not use children caches. Use for testing // Legacy implementation that does not use children caches. Use for testing
// if (true) return DataStorage.get() // if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry()) // .getDisplayParent(other.getEntry())
@ -162,35 +134,29 @@ public class StoreSection {
// .orElse(false); // .orElse(false);
// This check is fast as the children are cached in the storage // This check is fast as the children are cached in the storage
var isChildren = DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
var showProvider = other.getEntry().getProvider() == null || });
other.getEntry().getProvider().shouldShow(other); var cached = ListBindingsHelper.cachedMappedContentBinding(
return isChildren && showProvider; allChildren,
}, e.getPersistentState(), e.getCache(), StoreViewState.get().getEntriesListChangeObservable()); allChildren,
var cached = allChildren.mapped(
entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category); var ordered = sorted(cached, category);
var filtered = ordered.filtered( var filtered = ListBindingsHelper.filteredContentBinding(
ordered,
section -> { section -> {
var showFilter = filterString == null || section.matchesFilter(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());
var matchesSelector = section.anyMatches(entryFilter); var matchesSelector = section.anyMatches(entryFilter);
// Prevent updates for children on category switching by checking depth var sameCategory = category == null
var showCategory = category == null
|| category.getValue() == null || category.getValue() == null
|| showInCategory(category.getValue(), section.getWrapper()) || showInCategory(category.getValue(), section.getWrapper());
|| depth > 0;
// If this entry is already shown as root due to a different category than parent, don't show it // If this entry is already shown as root due to a different category than parent, don't show it
// again here // again here
var notRoot = var notRoot =
!DataStorage.get().isRootEntry(section.getWrapper().getEntry()); !DataStorage.get().isRootEntry(section.getWrapper().getEntry());
var showProvider = section.getWrapper().getEntry().getProvider() == null || return showFilter && matchesSelector && sameCategory && notRoot;
section.getWrapper().getEntry().getProvider().shouldShow(section.getWrapper());
return showFilter && matchesSelector && showCategory && notRoot && showProvider;
}, },
category, category,
filterString, filterString);
e.getPersistentState(),
e.getCache());
return new StoreSection(e, cached, filtered, depth); return new StoreSection(e, cached, filtered, depth);
} }
@ -213,13 +179,13 @@ public class StoreSection {
return false; return false;
} }
public boolean matchesFilter(String filter) { public boolean shouldShow(String filter) {
return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter)); return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter));
} }
public boolean anyMatches(Predicate<StoreEntryWrapper> c) { public boolean anyMatches(Predicate<StoreEntryWrapper> c) {
return c == null return c == null
|| c.test(wrapper) || c.test(wrapper)
|| allChildren.getList().stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c)); || allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));
} }
} }

View file

@ -7,8 +7,10 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -40,9 +42,9 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
private Comp<CompStructure<Button>> createQuickAccessButton() { private Comp<CompStructure<Button>> createQuickAccessButton() {
var quickAccessDisabled = Bindings.createBooleanBinding( var quickAccessDisabled = Bindings.createBooleanBinding(
() -> { () -> {
return section.getShownChildren().getList().isEmpty(); return section.getShownChildren().isEmpty();
}, },
section.getShownChildren().getList()); section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = w -> { Consumer<StoreEntryWrapper> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
w.executeDefaultAction(); w.executeDefaultAction();
@ -69,11 +71,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
var expandButton = new IconButtonComp( var expandButton = new IconButtonComp(
Bindings.createStringBinding( Bindings.createStringBinding(
() -> section.getWrapper().getExpanded().get() () -> section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0 && section.getShownChildren().size() > 0
? "mdal-keyboard_arrow_down" ? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right", : "mdal-keyboard_arrow_right",
section.getWrapper().getExpanded(), section.getWrapper().getExpanded(),
section.getShownChildren().getList()), section.getShownChildren()),
() -> { () -> {
section.getWrapper().toggleExpanded(); section.getWrapper().toggleExpanded();
}); });
@ -87,7 +89,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
return "Expand " + section.getWrapper().getName().getValue(); return "Expand " + section.getWrapper().getName().getValue();
}, },
section.getWrapper().getName())) section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0)) .disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
.styleClass("expand-button") .styleClass("expand-button")
.maxHeight(100) .maxHeight(100)
.vgrow(); .vgrow();
@ -126,12 +128,13 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded // section is actually expanded
var listSections = section.getShownChildren().filtered( var listSections = ListBindingsHelper.filteredContentBinding(
storeSection -> section.getAllChildren().getList().size() <= 20 section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20
|| section.getWrapper().getExpanded().get(), || section.getWrapper().getExpanded().get(),
section.getWrapper().getExpanded(), section.getWrapper().getExpanded(),
section.getAllChildren().getList()); section.getAllChildren());
var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> { var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> {
return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false)); return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false));
}) })
.minHeight(0) .minHeight(0)
@ -140,10 +143,10 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
var expanded = Bindings.createBooleanBinding( var expanded = Bindings.createBooleanBinding(
() -> { () -> {
return section.getWrapper().getExpanded().get() return section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0; && section.getShownChildren().size() > 0;
}, },
section.getWrapper().getExpanded(), section.getWrapper().getExpanded(),
section.getShownChildren().getList()); section.getShownChildren());
var full = new VerticalComp(List.of( var full = new VerticalComp(List.of(
topEntryList, topEntryList,
Comp.separator().hide(expanded.not()), Comp.separator().hide(expanded.not()),
@ -152,7 +155,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> struc.get().setFillHeight(true))
.hide(Bindings.or( .hide(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()), Bindings.not(section.getWrapper().getExpanded()),
Bindings.size(section.getShownChildren().getList()).isEqualTo(0))))); Bindings.size(section.getShownChildren()).isEqualTo(0)))));
return full.styleClass("store-entry-section-comp") return full.styleClass("store-entry-section-comp")
.apply(struc -> { .apply(struc -> {
struc.get().setFillWidth(true); struc.get().setFillWidth(true);

View file

@ -8,7 +8,9 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -82,7 +84,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
expanded = expanded =
new SimpleBooleanProperty(section.getWrapper().getExpanded().get() new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0); && section.getShownChildren().size() > 0);
var button = new IconButtonComp( var button = new IconButtonComp(
Bindings.createStringBinding( Bindings.createStringBinding(
() -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", () -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right",
@ -99,15 +101,15 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
+ section.getWrapper().getName().getValue(); + section.getWrapper().getName().getValue();
}, },
section.getWrapper().getName())) section.getWrapper().getName()))
.disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0)) .disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
.grow(false, true) .grow(false, true)
.styleClass("expand-button"); .styleClass("expand-button");
var quickAccessDisabled = Bindings.createBooleanBinding( var quickAccessDisabled = Bindings.createBooleanBinding(
() -> { () -> {
return section.getShownChildren().getList().isEmpty(); return section.getShownChildren().isEmpty();
}, },
section.getShownChildren().getList()); section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = action; Consumer<StoreEntryWrapper> quickAccessAction = action;
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow() .vgrow()
@ -129,12 +131,13 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded // section is actually expanded
var listSections = section.getWrapper() != null var listSections = section.getWrapper() != null
? section.getShownChildren().filtered( ? ListBindingsHelper.filteredContentBinding(
storeSection -> section.getAllChildren().getList().size() <= 20 || expanded.get(), section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
expanded, expanded,
section.getAllChildren().getList()) section.getAllChildren())
: section.getShownChildren(); : section.getShownChildren();
var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> { var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> {
return new StoreSectionMiniComp(e, this.augment, this.action, this.condensedStyle); return new StoreSectionMiniComp(e, this.augment, this.action, this.condensedStyle);
}) })
.minHeight(0) .minHeight(0)
@ -145,7 +148,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
.apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> struc.get().setFillHeight(true))
.hide(Bindings.or( .hide(Bindings.or(
Bindings.not(expanded), Bindings.not(expanded),
Bindings.size(section.getAllChildren().getList()).isEqualTo(0)))); Bindings.size(section.getAllChildren()).isEqualTo(0))));
var vert = new VerticalComp(list); var vert = new VerticalComp(list);
if (condensedStyle) { if (condensedStyle) {

View file

@ -48,7 +48,7 @@ public interface StoreSortMode {
@Override @Override
public StoreSection representative(StoreSection s) { public StoreSection representative(StoreSection s) {
return Stream.concat( return Stream.concat(
s.getShownChildren().getList().stream() s.getShownChildren().stream()
.filter(section -> section.getWrapper() .filter(section -> section.getWrapper()
.getEntry() .getEntry()
.getValidity() .getValidity()
@ -76,7 +76,7 @@ public interface StoreSortMode {
@Override @Override
public StoreSection representative(StoreSection s) { public StoreSection representative(StoreSection s) {
return Stream.concat( return Stream.concat(
s.getShownChildren().getList().stream() s.getShownChildren().stream()
.filter(section -> section.getWrapper() .filter(section -> section.getWrapper()
.getEntry() .getEntry()
.getValidity() .getValidity()

View file

@ -1,16 +1,22 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppCache;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.StorageListener; import io.xpipe.app.storage.StorageListener;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.*; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter; import lombok.Getter;
import java.util.*; import java.util.*;
@ -23,18 +29,12 @@ public class StoreViewState {
private final StringProperty filter = new SimpleStringProperty(); private final StringProperty filter = new SimpleStringProperty();
@Getter @Getter
private final DerivedObservableList<StoreEntryWrapper> allEntries = private final ObservableList<StoreEntryWrapper> allEntries =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true); FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter @Getter
private final DerivedObservableList<StoreCategoryWrapper> categories = private final ObservableList<StoreCategoryWrapper> categories =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true); FXCollections.observableList(new CopyOnWriteArrayList<>());
@Getter
private final IntegerProperty entriesOrderChangeObservable = new SimpleIntegerProperty();
@Getter
private final IntegerProperty entriesListChangeObservable = new SimpleIntegerProperty();
@Getter @Getter
private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>(); private final Property<StoreCategoryWrapper> activeCategory = new SimpleObjectProperty<>();
@ -76,8 +76,8 @@ public class StoreViewState {
} }
private void updateContent() { private void updateContent() {
categories.getList().forEach(c -> c.update()); categories.forEach(c -> c.update());
allEntries.getList().forEach(e -> e.update()); allEntries.forEach(e -> e.update());
} }
private void initSections() { private void initSections() {
@ -86,19 +86,16 @@ public class StoreViewState {
StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory); StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory);
} catch (Exception exception) { } catch (Exception exception) {
currentTopLevelSection = currentTopLevelSection =
new StoreSection(null, new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0);
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
0);
ErrorEvent.fromThrowable(exception).handle(); ErrorEvent.fromThrowable(exception).handle();
} }
} }
private void initContent() { private void initContent() {
allEntries.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream() allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream()
.map(StoreEntryWrapper::new) .map(StoreEntryWrapper::new)
.toList())); .toList()));
categories.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream() categories.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream()
.map(StoreCategoryWrapper::new) .map(StoreCategoryWrapper::new)
.toList())); .toList()));
@ -106,11 +103,11 @@ public class StoreViewState {
DataStorage.get().setSelectedCategory(newValue.getCategory()); DataStorage.get().setSelectedCategory(newValue.getCategory());
}); });
var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID); var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID);
activeCategory.setValue(categories.getList().stream() activeCategory.setValue(categories.stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(selected)) storeCategoryWrapper.getCategory().getUuid().equals(selected))
.findFirst() .findFirst()
.orElse(categories.getList().stream() .orElse(categories.stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID)) storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID))
.findFirst() .findFirst()
@ -122,9 +119,9 @@ public class StoreViewState {
AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> { AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> { Platform.runLater(() -> {
synchronized (this) { synchronized (this) {
var l = new ArrayList<>(allEntries.getList()); var l = new ArrayList<>(allEntries);
allEntries.getList().clear(); allEntries.clear();
allEntries.getList().setAll(l); allEntries.setAll(l);
} }
}); });
}); });
@ -132,21 +129,6 @@ public class StoreViewState {
// Watch out for synchronizing all calls to the entries and categories list! // Watch out for synchronizing all calls to the entries and categories list!
DataStorage.get().addListener(new StorageListener() { DataStorage.get().addListener(new StorageListener() {
@Override
public void onStoreOrderUpdate() {
Platform.runLater(() -> {
entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1);
});
}
@Override
public void onStoreListUpdate() {
Platform.runLater(() -> {
entriesListChangeObservable.set(entriesListChangeObservable.get() + 1);
});
}
@Override @Override
public void onStoreAdd(DataStoreEntry... entry) { public void onStoreAdd(DataStoreEntry... entry) {
var l = Arrays.stream(entry) var l = Arrays.stream(entry)
@ -160,11 +142,11 @@ public class StoreViewState {
} }
synchronized (this) { synchronized (this) {
allEntries.getList().addAll(l); allEntries.addAll(l);
} }
synchronized (this) { synchronized (this) {
categories.getList().stream() categories.stream()
.filter(storeCategoryWrapper -> allEntries.getList().stream() .filter(storeCategoryWrapper -> allEntries.stream()
.anyMatch(storeEntryWrapper -> storeEntryWrapper .anyMatch(storeEntryWrapper -> storeEntryWrapper
.getEntry() .getEntry()
.getCategoryUuid() .getCategoryUuid()
@ -181,14 +163,14 @@ public class StoreViewState {
var a = Arrays.stream(entry).collect(Collectors.toSet()); var a = Arrays.stream(entry).collect(Collectors.toSet());
List<StoreEntryWrapper> l; List<StoreEntryWrapper> l;
synchronized (this) { synchronized (this) {
l = allEntries.getList().stream() l = allEntries.stream()
.filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry())) .filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry()))
.toList(); .toList();
} }
List<StoreCategoryWrapper> cats; List<StoreCategoryWrapper> cats;
synchronized (this) { synchronized (this) {
cats = categories.getList().stream() cats = categories.stream()
.filter(storeCategoryWrapper -> allEntries.getList().stream() .filter(storeCategoryWrapper -> allEntries.stream()
.anyMatch(storeEntryWrapper -> storeEntryWrapper .anyMatch(storeEntryWrapper -> storeEntryWrapper
.getEntry() .getEntry()
.getCategoryUuid() .getCategoryUuid()
@ -204,7 +186,7 @@ public class StoreViewState {
} }
synchronized (this) { synchronized (this) {
allEntries.getList().removeAll(l); allEntries.removeAll(l);
} }
cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update());
}); });
@ -221,7 +203,7 @@ public class StoreViewState {
} }
synchronized (this) { synchronized (this) {
categories.getList().add(l); categories.add(l);
} }
l.update(); l.update();
}); });
@ -231,7 +213,7 @@ public class StoreViewState {
public void onCategoryRemove(DataStoreCategory category) { public void onCategoryRemove(DataStoreCategory category) {
Optional<StoreCategoryWrapper> found; Optional<StoreCategoryWrapper> found;
synchronized (this) { synchronized (this) {
found = categories.getList().stream() found = categories.stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().equals(category)) storeCategoryWrapper.getCategory().equals(category))
.findFirst(); .findFirst();
@ -247,7 +229,7 @@ public class StoreViewState {
} }
synchronized (this) { synchronized (this) {
categories.getList().remove(found.get()); categories.remove(found.get());
} }
var p = found.get().getParent(); var p = found.get().getParent();
if (p != null) { if (p != null) {
@ -258,34 +240,15 @@ public class StoreViewState {
}); });
} }
public Optional<StoreSection> getParentSectionForWrapper(StoreEntryWrapper wrapper) { public ObservableList<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {
StoreSection current = getCurrentTopLevelSection();
while (true) {
var child = current.getAllChildren().getList().stream().filter(section -> section.getWrapper().equals(wrapper)).findFirst();
if (child.isPresent()) {
return Optional.of(current);
}
var traverse = current.getAllChildren().getList().stream().filter(section -> section.anyMatches(w -> w.equals(wrapper))).findFirst();
if (traverse.isPresent()) {
current = traverse.get();
} else {
return Optional.empty();
}
}
}
public DerivedObservableList<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {
Comparator<StoreCategoryWrapper> comparator = new Comparator<>() { Comparator<StoreCategoryWrapper> comparator = new Comparator<>() {
@Override @Override
public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) {
var o1Root = o1.getRoot(); var o1Root = o1.getRoot();
var o2Root = o2.getRoot(); var o2Root = o2.getRoot();
if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
return -1; return -1;
} }
if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
return 1; return 1;
} }
@ -302,6 +265,22 @@ public class StoreViewState {
return 1; return 1;
} }
if (o1.getDepth() > o2.getDepth()) {
if (o1.getParent() == o2) {
return 1;
}
return compare(o1.getParent(), o2);
}
if (o1.getDepth() < o2.getDepth()) {
if (o2.getParent() == o1) {
return -1;
}
return compare(o1, o2.getParent());
}
var parent = compare(o1.getParent(), o2.getParent()); var parent = compare(o1.getParent(), o2.getParent());
if (parent != 0) { if (parent != 0) {
return parent; return parent;
@ -312,11 +291,13 @@ public class StoreViewState {
.compareToIgnoreCase(o2.nameProperty().getValue()); .compareToIgnoreCase(o2.nameProperty().getValue());
} }
}; };
return categories.filtered(cat -> root == null || cat.getRoot().equals(root)).sorted(comparator); return ListBindingsHelper.filteredContentBinding(
categories, cat -> root == null || cat.getRoot().equals(root))
.sorted(comparator);
} }
public StoreCategoryWrapper getAllConnectionsCategory() { public StoreCategoryWrapper getAllConnectionsCategory() {
return categories.getList().stream() return categories.stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)) storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID))
.findFirst() .findFirst()
@ -324,7 +305,7 @@ public class StoreViewState {
} }
public StoreCategoryWrapper getAllScriptsCategory() { public StoreCategoryWrapper getAllScriptsCategory() {
return categories.getList().stream() return categories.stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)) storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID))
.findFirst() .findFirst()
@ -332,14 +313,14 @@ public class StoreViewState {
} }
public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) { public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) {
return allEntries.getList().stream() return allEntries.stream()
.filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry)) .filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry))
.findFirst() .findFirst()
.orElseThrow(); .orElseThrow();
} }
public StoreCategoryWrapper getCategoryWrapper(DataStoreCategory entry) { public StoreCategoryWrapper getCategoryWrapper(DataStoreCategory entry) {
return categories.getList().stream() return categories.stream()
.filter(storeCategoryWrapper -> .filter(storeCategoryWrapper ->
storeCategoryWrapper.getCategory().equals(entry)) storeCategoryWrapper.getCategory().equals(entry))
.findFirst() .findFirst()

View file

@ -28,10 +28,6 @@ import java.util.List;
public interface DataStoreProvider { public interface DataStoreProvider {
default boolean shouldShow(StoreEntryWrapper w) {
return true;
}
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) { default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
return new SimpleBooleanProperty(false); return new SimpleBooleanProperty(false);
} }

View file

@ -4,14 +4,17 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.Translatable; import io.xpipe.app.util.Translatable;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
@ -76,7 +79,7 @@ public class ChoiceComp<T> extends Comp<CompStructure<ComboBox<T>>> {
list.add(null); list.add(null);
} }
cb.getItems().setAll(list); ListBindingsHelper.setContent(cb.getItems(), list);
}); });
cb.valueProperty().addListener((observable, oldValue, newValue) -> { cb.valueProperty().addListener((observable, oldValue, newValue) -> {

View file

@ -11,10 +11,11 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ContextMenuHelper; import io.xpipe.app.util.ContextMenuHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
@ -25,6 +26,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
@ -77,12 +79,13 @@ public class StoreCategoryComp extends SimpleComp {
showing.bind(cm.showingProperty()); showing.bind(cm.showingProperty());
return cm; return cm;
})); }));
var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered( var shownList = ListBindingsHelper.filteredContentBinding(
category.getContainedEntries(),
storeEntryWrapper -> { storeEntryWrapper -> {
return storeEntryWrapper.shouldShow( return storeEntryWrapper.shouldShow(
StoreViewState.get().getFilterString().getValue()); StoreViewState.get().getFilterString().getValue());
}, },
StoreViewState.get().getFilterString()).getList(); StoreViewState.get().getFilterString());
var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")"); var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")");
var hover = new SimpleBooleanProperty(); var hover = new SimpleBooleanProperty();
var focus = new SimpleBooleanProperty(); var focus = new SimpleBooleanProperty();

View file

@ -1,228 +0,0 @@
package io.xpipe.app.fxcomps.util;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
@Getter
public class DerivedObservableList<T> {
private final ObservableList<T> list;
private final boolean unique;
public DerivedObservableList(ObservableList<T> list, boolean unique) {
this.list = list;
this.unique = unique;
}
private <V> DerivedObservableList<V> createNewDerived() {
var l = FXCollections.<V>observableArrayList();
BindingsHelper.preserve(l, list);
return new DerivedObservableList<>(l, unique);
}
public void setContent(List<? extends T> newList) {
if (list.equals(newList)) {
return;
}
if (list.size() == 0) {
list.addAll(newList);
return;
}
if (newList.size() == 0) {
list.clear();
return;
}
if (unique) {
setContentUnique(newList);
} else {
setContentNonUnique(newList);
}
}
public void setContentNonUnique(List<? extends T> newList) {
var target = list;
var targetSet = new HashSet<>(target);
var newSet = new HashSet<>(newList);
// Only add missing element
if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {
var l = new HashSet<>(newSet);
l.removeAll(targetSet);
if (l.size() > 0) {
var found = l.iterator().next();
var index = newList.indexOf(found);
target.add(index, found);
return;
}
}
// Only remove not needed element
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);
}
private void setContentUnique(List<? extends T> newList) {
var listSet = new HashSet<>(list);
var newSet = new HashSet<>(newList);
// Addition
if (newSet.containsAll(list)) {
var l = new ArrayList<>(newList);
l.removeIf(t -> !listSet.contains(t));
// Reordering occurred
if (!l.equals(list)) {
list.setAll(newList);
return;
}
var start = 0;
for (int end = 0; end <= list.size(); end++) {
var index = end < list.size() ? newList.indexOf(list.get(end)) : newList.size();
for (; start < index; start++) {
list.add(start, newList.get(start));
}
start = index + 1;
}
return;
}
// Removal
if (listSet.containsAll(newList)) {
var l = new ArrayList<>(list);
l.removeIf(t -> !newSet.contains(t));
// Reordering occurred
if (!l.equals(newList)) {
list.setAll(newList);
return;
}
var toRemove = new ArrayList<>(list);
toRemove.removeIf(t -> newSet.contains(t));
list.removeAll(toRemove);
return;
}
// Other cases are more difficult
list.setAll(newList);
}
public <V> DerivedObservableList<V> mapped(Function<T, V> map) {
var l1 = this.<V>createNewDerived();
Runnable runnable = () -> {
l1.setContent(list.stream().map(map).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
return l1;
}
public void bindContent(ObservableList<T> other) {
setContent(other);
other.addListener((ListChangeListener<? super T>) c -> {
setContent(other);
});
}
public DerivedObservableList<T> filtered(Predicate<T> predicate) {
return filtered(new SimpleObjectProperty<>(predicate));
}
public DerivedObservableList<T> filtered(Predicate<T> predicate, Observable... observables) {
return filtered(
Bindings.createObjectBinding(
() -> {
return new Predicate<>() {
@Override
public boolean test(T v) {
return predicate.test(v);
}
};
},
Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));
}
public DerivedObservableList<T> filtered(ObservableValue<Predicate<T>> predicate) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(
predicate.getValue() != null
? list.stream().filter(predicate.getValue()).toList()
: list);
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
predicate.addListener(observable -> {
runnable.run();
});
return d;
}
public DerivedObservableList<T> sorted(Comparator<T> comp, Observable... observables) {
return sorted(Bindings.createObjectBinding(
() -> {
return new Comparator<>() {
@Override
public int compare(T o1, T o2) {
return comp.compare(o1, o2);
}
};
},
observables));
}
public DerivedObservableList<T> sorted(ObservableValue<Comparator<T>> comp) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list.stream().sorted(comp.getValue()).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
comp.addListener(observable -> {
d.list.sort(comp.getValue());
});
return d;
}
public DerivedObservableList<T> blockUpdatesIf(ObservableBooleanValue block) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list);
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
block.addListener(observable -> {
runnable.run();
});
return d;
}
}

View file

@ -0,0 +1,190 @@
package io.xpipe.app.fxcomps.util;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
public class ListBindingsHelper {
public static <T> void bindContent(ObservableList<T> l1, ObservableList<? extends T> l2) {
setContent(l1, l2);
l2.addListener((ListChangeListener<? super T>) c -> {
setContent(l1, l2);
});
}
public static <T, U> ObservableValue<Boolean> anyMatch(List<? extends ObservableValue<Boolean>> l) {
return Bindings.createBooleanBinding(
() -> {
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue());
},
l.toArray(ObservableValue[]::new));
}
public static <T, V> ObservableList<T> mappedContentBinding(ObservableList<V> l2, Function<V, T> map) {
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <T, V> ObservableList<T> cachedMappedContentBinding(
ObservableList<V> all, ObservableList<V> shown, Function<V, T> map) {
var cache = new HashMap<V, T>();
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
cache.keySet().removeIf(t -> !all.contains(t));
setContent(
l1,
shown.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, map.apply(v));
}
return cache.get(v);
})
.toList());
};
runnable.run();
shown.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
BindingsHelper.preserve(l1, all);
BindingsHelper.preserve(l1, shown);
return l1;
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, Comparator<V> comp, Observable... observables) {
return orderedContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Comparator<>() {
@Override
public int compare(V o1, V o2) {
return comp.compare(o1, o2);
}
};
},
observables));
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, ObservableValue<Comparator<V>> comp) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().sorted(comp.getValue()).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
comp.addListener((observable, oldValue, newValue) -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <V> ObservableList<V> filteredContentBinding(ObservableList<V> l2, Predicate<V> predicate) {
return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, Predicate<V> predicate, Observable... observables) {
return filteredContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Predicate<>() {
@Override
public boolean test(V v) {
return predicate.test(v);
}
};
},
Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, ObservableValue<Predicate<V>> predicate) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(
l1,
predicate.getValue() != null
? l2.stream().filter(predicate.getValue()).toList()
: l2);
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
predicate.addListener((c, o, n) -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <T> void setContent(ObservableList<T> target, List<? extends T> newList) {
if (target.equals(newList)) {
return;
}
if (target.size() == 0) {
target.setAll(newList);
return;
}
if (newList.size() == 0) {
target.clear();
return;
}
var targetSet = new HashSet<>(target);
var newSet = new HashSet<>(newList);
// Only add missing element
if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {
var l = new HashSet<>(newSet);
l.removeAll(targetSet);
if (l.size() > 0) {
var found = l.iterator().next();
var index = newList.indexOf(found);
target.add(index, found);
return;
}
}
// Only remove not needed element
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);
}
}

View file

@ -45,16 +45,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
@Override @Override
public boolean isAvailable() { public boolean isAvailable() {
try (ShellControl pc = LocalShell.getShell().start()) { try (ShellControl pc = LocalShell.getShell().start()) {
return pc.command(String.format( var out = pc.command(String.format(
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications", "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications",
applicationName)) applicationName))
.executeAndCheck(); .readStdoutIfPossible();
return out.isPresent() && !out.get().isBlank();
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
return false; return false;
} }
} }
@Override @Override
public boolean isSelectable() { public boolean isSelectable() {
return OsType.getLocal().equals(OsType.MACOS); return OsType.getLocal().equals(OsType.MACOS);

View file

@ -194,10 +194,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
@Override @Override
public void launch(Path file) throws Exception { public void launch(Path file) throws Exception {
ExternalApplicationHelper.startAsync(CommandBuilder.of() try (var sc = LocalShell.getShell().start()) {
.add("open", "-a") sc.executeSimpleCommand(CommandBuilder.of()
.addQuoted(applicationName) .add("open", "-a").addQuoted(applicationName).addFile(file.toString()));
.addFile(file.toString())); }
} }
} }

View file

@ -328,16 +328,16 @@ public abstract class DataStorage {
return; return;
} }
entry.setCategoryUuid(newCategory.getUuid());
var children = getDeepStoreChildren(entry); var children = getDeepStoreChildren(entry);
children.forEach(child -> child.setCategoryUuid(newCategory.getUuid())); var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreListUpdate()); listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove));
saveAsync();
}
public void orderBefore(DataStoreEntry entry, DataStoreEntry reference) { entry.setCategoryUuid(newCategory.getUuid());
entry.setOrderBefore(reference != null ? reference.getUuid() : null); children.forEach(child -> child.setCategoryUuid(newCategory.getUuid()));
listeners.forEach(storageListener -> storageListener.onStoreOrderUpdate());
var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
saveAsync();
} }
public boolean refreshChildren(DataStoreEntry e) { public boolean refreshChildren(DataStoreEntry e) {
@ -439,8 +439,8 @@ public abstract class DataStorage {
pair.getKey().setStoreInternal(merged, false); pair.getKey().setStoreInternal(merged, false);
} }
var s = pair.getKey().getStorePersistentState(); var mergedState = pair.getKey().getStorePersistentState().deepCopy();
var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState()); mergedState.merge(pair.getValue().get().getStorePersistentState());
pair.getKey().setStorePersistentState(mergedState); pair.getKey().setStorePersistentState(mergedState);
} }
} }
@ -788,7 +788,9 @@ public abstract class DataStorage {
public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) { public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) {
return storeEntriesSet.stream() return storeEntriesSet.stream()
.filter(n -> n.getStore() == store || (!identityOnly && (n.getStore() != null .filter(n -> n.getStore() == store
|| (!identityOnly
&& (n.getStore() != null
&& Objects.equals( && Objects.equals(
store.getClass(), n.getStore().getClass()) store.getClass(), n.getStore().getClass())
&& store.equals(n.getStore())))) && store.equals(n.getStore()))))

View file

@ -72,9 +72,6 @@ public class DataStoreEntry extends StorageElement {
@NonFinal @NonFinal
String notes; String notes;
@NonFinal
UUID orderBefore;
private DataStoreEntry( private DataStoreEntry(
Path directory, Path directory,
UUID uuid, UUID uuid,
@ -89,8 +86,7 @@ public class DataStoreEntry extends StorageElement {
JsonNode storePersistentState, JsonNode storePersistentState,
boolean expanded, boolean expanded,
DataStoreColor color, DataStoreColor color,
String notes, UUID orderBefore String notes) {
) {
super(directory, uuid, name, lastUsed, lastModified, dirty); super(directory, uuid, name, lastUsed, lastModified, dirty);
this.categoryUuid = categoryUuid; this.categoryUuid = categoryUuid;
this.store = DataStorageParser.storeFromNode(storeNode); this.store = DataStorageParser.storeFromNode(storeNode);
@ -99,7 +95,6 @@ public class DataStoreEntry extends StorageElement {
this.configuration = configuration; this.configuration = configuration;
this.expanded = expanded; this.expanded = expanded;
this.color = color; this.color = color;
this.orderBefore = orderBefore;
this.provider = store != null this.provider = store != null
? DataStoreProviders.byStoreClass(store.getClass()).orElse(null) ? DataStoreProviders.byStoreClass(store.getClass()).orElse(null)
: null; : null;
@ -114,12 +109,10 @@ public class DataStoreEntry extends StorageElement {
String name, String name,
Instant lastUsed, Instant lastUsed,
Instant lastModified, Instant lastModified,
DataStore store, UUID orderBefore DataStore store) {
) {
super(directory, uuid, name, lastUsed, lastModified, false); super(directory, uuid, name, lastUsed, lastModified, false);
this.categoryUuid = categoryUuid; this.categoryUuid = categoryUuid;
this.store = store; this.store = store;
this.orderBefore = orderBefore;
this.storeNode = null; this.storeNode = null;
this.validity = Validity.INCOMPLETE; this.validity = Validity.INCOMPLETE;
this.configuration = Configuration.defaultConfiguration(); this.configuration = Configuration.defaultConfiguration();
@ -137,8 +130,7 @@ public class DataStoreEntry extends StorageElement {
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Instant.now(), Instant.now(),
Instant.now(), Instant.now(),
store, store);
null);
} }
public static DataStoreEntry createNew(@NonNull String name, @NonNull DataStore store) { public static DataStoreEntry createNew(@NonNull String name, @NonNull DataStore store) {
@ -167,7 +159,6 @@ public class DataStoreEntry extends StorageElement {
null, null,
false, false,
null, null,
null,
null); null);
return entry; return entry;
} }
@ -185,8 +176,7 @@ public class DataStoreEntry extends StorageElement {
JsonNode storePersistentState, JsonNode storePersistentState,
boolean expanded, boolean expanded,
DataStoreColor color, DataStoreColor color,
String notes, String notes) {
UUID orderBeforeEntry) {
return new DataStoreEntry( return new DataStoreEntry(
directory, directory,
uuid, uuid,
@ -201,8 +191,7 @@ public class DataStoreEntry extends StorageElement {
storePersistentState, storePersistentState,
expanded, expanded,
color, color,
notes, notes);
orderBeforeEntry);
} }
public static Optional<DataStoreEntry> fromDirectory(Path dir) throws Exception { public static Optional<DataStoreEntry> fromDirectory(Path dir) throws Exception {
@ -237,15 +226,6 @@ public class DataStoreEntry extends StorageElement {
.map(jsonNode -> jsonNode.textValue()) .map(jsonNode -> jsonNode.textValue())
.map(Instant::parse) .map(Instant::parse)
.orElse(Instant.EPOCH); .orElse(Instant.EPOCH);
var order = Optional.ofNullable(stateJson.get("orderBefore"))
.map(node -> {
try {
return mapper.treeToValue(node, UUID.class);
} catch (JsonProcessingException e) {
return null;
}
})
.orElse(null);
var configuration = Optional.ofNullable(json.get("configuration")) var configuration = Optional.ofNullable(json.get("configuration"))
.map(node -> { .map(node -> {
try { try {
@ -295,19 +275,10 @@ public class DataStoreEntry extends StorageElement {
persistentState, persistentState,
expanded, expanded,
color, color,
notes, notes
order
)); ));
} }
public void setOrderBefore(UUID uuid) {
var changed = !Objects.equals(orderBefore, uuid);
this.orderBefore = uuid;
if (changed) {
notifyUpdate(false, true);
}
}
@Override @Override
public int hashCode() { public int hashCode() {
return getUuid().hashCode(); return getUuid().hashCode();
@ -359,7 +330,7 @@ public class DataStoreEntry extends StorageElement {
storePersistentStateNode = JacksonMapper.getDefault().valueToTree(storePersistentState); storePersistentStateNode = JacksonMapper.getDefault().valueToTree(storePersistentState);
} }
} }
return (T) storePersistentState; return (T) sds.getStateClass().cast(storePersistentState);
} }
public void setStorePersistentState(DataStoreState value) { public void setStorePersistentState(DataStoreState value) {
@ -408,7 +379,6 @@ public class DataStoreEntry extends StorageElement {
stateObj.set("persistentState", storePersistentStateNode); stateObj.set("persistentState", storePersistentStateNode);
obj.set("configuration", mapper.valueToTree(configuration)); obj.set("configuration", mapper.valueToTree(configuration));
stateObj.put("expanded", expanded); stateObj.put("expanded", expanded);
stateObj.put("orderBefore", orderBefore != null ? orderBefore.toString() : null);
var entryString = mapper.writeValueAsString(obj); var entryString = mapper.writeValueAsString(obj);
var stateString = mapper.writeValueAsString(stateObj); var stateString = mapper.writeValueAsString(stateObj);

View file

@ -2,10 +2,6 @@ package io.xpipe.app.storage;
public interface StorageListener { public interface StorageListener {
void onStoreOrderUpdate();
void onStoreListUpdate();
void onStoreAdd(DataStoreEntry... entry); void onStoreAdd(DataStoreEntry... entry);
void onStoreRemove(DataStoreEntry... entry); void onStoreRemove(DataStoreEntry... entry);

View file

@ -514,17 +514,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override @Override
public void launch(LaunchConfiguration configuration) throws Exception { public void launch(LaunchConfiguration configuration) throws Exception {
try (ShellControl pc = LocalShell.getShell()) { LocalShell.getShell()
var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\""; .executeSimpleCommand(CommandBuilder.of()
pc.osascriptCommand(String.format( .add("open", "-a")
""" .addQuoted("Terminal.app")
activate application "Terminal" .addFile(configuration.getScriptFile()));
delay 1
tell app "Terminal" to do script %s
""",
suffix))
.execute();
}
} }
}; };
ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") {
@ -550,26 +544,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override @Override
public void launch(LaunchConfiguration configuration) throws Exception { public void launch(LaunchConfiguration configuration) throws Exception {
try (ShellControl pc = LocalShell.getShell()) { LocalShell.getShell()
pc.osascriptCommand(String.format( .executeSimpleCommand(CommandBuilder.of()
""" .add("open", "-a")
if application "iTerm" is not running then .addQuoted("iTerm.app")
launch application "iTerm" .addFile(configuration.getScriptFile()));
delay 1
tell application "iTerm"
tell current tab of current window
close
end tell
end tell
end if
tell application "iTerm"
activate
create window with default profile command "%s"
end tell
""",
configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"")))
.execute();
}
} }
}; };
ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") {

View file

@ -1,9 +1,14 @@
package io.xpipe.app.terminal; package io.xpipe.app.terminal;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.ExternalApplicationHelper; import io.xpipe.app.prefs.ExternalApplicationHelper;
import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.WindowsRegistry; import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
@ -26,7 +31,7 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override @Override
default boolean isRecommended() { default boolean isRecommended() {
return false; return OsType.getLocal() != OsType.WINDOWS;
} }
@Override @Override
@ -51,25 +56,62 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override @Override
protected Optional<Path> determineInstallation() { protected Optional<Path> determineInstallation() {
Optional<String> launcherDir; try {
launcherDir = WindowsRegistry.local().readValue( var foundKey = WindowsRegistry.local().findKeyForEqualValueMatchRecursive(WindowsRegistry.HKEY_LOCAL_MACHINE,
WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "http://wezfurlong.org/wezterm");
"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", if (foundKey.isPresent()) {
"InstallLocation") var installKey = WindowsRegistry.local().readValue(
.map(p -> p + "\\wezterm-gui.exe"); foundKey.get().getHkey(),
return launcherDir.map(Path::of); foundKey.get().getKey(),
"InstallLocation");
if (installKey.isPresent()) {
return installKey.map(p -> p + "\\wezterm-gui.exe").map(Path::of);
}
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
}
try (ShellControl pc = LocalShell.getShell()) {
if (pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"))) {
return Optional.of(Path.of("wezterm-gui"));
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
return Optional.empty();
} }
} }
class Linux extends SimplePathType implements WezTerminalType { class Linux extends ExternalApplicationType implements WezTerminalType {
public Linux() { public Linux() {
super("app.wezterm", "wezterm-gui", true); super("app.wezterm");
}
public boolean isAvailable() {
try (ShellControl pc = LocalShell.getShell()) {
return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm")) &&
pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
return false;
}
} }
@Override @Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) { public void launch(LaunchConfiguration configuration) throws Exception {
return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); var spawn = LocalShell.getShell().command(CommandBuilder.of().addFile("wezterm")
.add("cli", "spawn")
.addFile(configuration.getScriptFile()))
.executeAndCheck();
if (!spawn) {
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.addFile("wezterm-gui")
.add("start")
.addFile(configuration.getScriptFile()));
}
} }
} }
@ -81,20 +123,27 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override @Override
public void launch(LaunchConfiguration configuration) throws Exception { public void launch(LaunchConfiguration configuration) throws Exception {
var path = LocalShell.getShell() try (var sc = LocalShell.getShell()) {
.command(String.format( var path = sc.command(
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", String.format("mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
applicationName)) applicationName)).readStdoutOrThrow();
.readStdoutOrThrow(); var spawn = sc.command(CommandBuilder.of().addFile(Path.of(path)
var c = CommandBuilder.of() .resolve("Contents")
.addFile(Path.of(path) .resolve("MacOS")
.resolve("Contents") .resolve("wezterm").toString())
.resolve("MacOS") .add("cli", "spawn", "--pane-id", "0")
.resolve("wezterm-gui") .addFile(configuration.getScriptFile()))
.toString()) .executeAndCheck();
.add("start") if (!spawn) {
.add(configuration.getDialectLaunchCommand()); ExternalApplicationHelper.startAsync(CommandBuilder.of()
ExternalApplicationHelper.startAsync(c); .addFile(Path.of(path)
.resolve("Contents")
.resolve("MacOS")
.resolve("wezterm-gui").toString())
.add("start")
.addFile(configuration.getScriptFile()));
}
}
} }
} }
} }

View file

@ -40,7 +40,7 @@ public class DataStoreCategoryChoiceComp extends SimpleComp {
value.setValue(newValue); value.setValue(newValue);
} }
}); });
var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).getList()); var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root));
box.setValue(value.getValue()); box.setValue(value.getValue());
box.valueProperty().addListener((observable, oldValue, newValue) -> { box.valueProperty().addListener((observable, oldValue, newValue) -> {
value.setValue(newValue); value.setValue(newValue);

View file

@ -28,10 +28,9 @@ public class FileOpener {
try { try {
editor.launch(Path.of(localFile).toRealPath()); editor.launch(Path.of(localFile).toRealPath());
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable(e) ErrorEvent.fromThrowable("Unable to launch editor "
.description("Unable to launch editor "
+ editor.toTranslatedString().getValue() + editor.toTranslatedString().getValue()
+ ".\nMaybe try to use a different editor in the settings.") + ".\nMaybe try to use a different editor in the settings.", e)
.expected() .expected()
.handle(); .handle();
} }
@ -52,8 +51,7 @@ public class FileOpener {
} }
} }
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable(e) ErrorEvent.fromThrowable("Unable to open file " + localFile, e)
.description("Unable to open file " + localFile)
.handle(); .handle();
} }
} }
@ -68,8 +66,7 @@ public class FileOpener {
pc.executeSimpleCommand("open \"" + localFile + "\""); pc.executeSimpleCommand("open \"" + localFile + "\"");
} }
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable(e) ErrorEvent.fromThrowable("Unable to open file " + localFile, e)
.description("Unable to open file " + localFile)
.handle(); .handle();
} }
} }

View file

@ -88,7 +88,6 @@ project.ext {
arch = getArchName() arch = getArchName()
privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : [] privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : []
isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE')) isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE'))
isPreRelease = System.getenv('PRERELEASE') != null && Boolean.parseBoolean(System.getenv('PRERELEASE'))
isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE')) isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE'))
rawVersion = file('version').text.trim() rawVersion = file('version').text.trim()
versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT') versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT')
@ -106,7 +105,7 @@ project.ext {
website = 'https://xpipe.io' website = 'https://xpipe.io'
sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe' sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'
authors = 'Christopher Schnick' authors = 'Christopher Schnick'
javafxVersion = '22.0.1' javafxVersion = '23-ea+18'
platformName = getPlatformName() platformName = getPlatformName()
languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"] languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"]
jvmRunArgs = [ jvmRunArgs = [
@ -159,6 +158,11 @@ if (isFullRelease && rawVersion.contains("-")) {
throw new IllegalArgumentException("Releases must have canonical versions") throw new IllegalArgumentException("Releases must have canonical versions")
} }
if (isStage && !rawVersion.contains("-")) {
throw new IllegalArgumentException("Stage releases must have release numbers")
}
def replaceVariablesInFileAsString(String f, Map<String, String> replacements) { def replaceVariablesInFileAsString(String f, Map<String, String> replacements) {
def fileName = file(f).getName() def fileName = file(f).getName()
def text = file(f).text def text = file(f).text

View file

@ -58,13 +58,12 @@ public interface ShellControl extends ProcessControl {
default <T extends ShellStoreState> ShellControl withShellStateInit(StatefulDataStore<T> store) { default <T extends ShellStoreState> ShellControl withShellStateInit(StatefulDataStore<T> store) {
return onInit(shellControl -> { return onInit(shellControl -> {
var s = store.getState().toBuilder() var s = store.getState();
.osType(shellControl.getOsType()) s.setOsType(shellControl.getOsType());
.shellDialect(shellControl.getOriginalShellDialect()) s.setShellDialect(shellControl.getOriginalShellDialect());
.running(true) s.setRunning(true);
.osName(shellControl.getOsName()) s.setOsName(shellControl.getOsName());
.build(); store.setState(s);
store.setState(s.asNeeded());
}); });
} }
@ -75,8 +74,9 @@ public interface ShellControl extends ProcessControl {
return; return;
} }
var s = store.getState().toBuilder().running(false).build(); var s = store.getState();
store.setState(s.asNeeded()); s.setRunning(false);
store.setState(s);
}); });
} }

View file

@ -1,24 +0,0 @@
package io.xpipe.core.process;
import io.xpipe.core.store.DataStoreState;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@Value
@EqualsAndHashCode(callSuper=true)
@SuperBuilder(toBuilder = true)
@Jacksonized
public class ShellNameStoreState extends ShellStoreState {
String shellName;
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
var n = (ShellNameStoreState) newer;
var b = toBuilder();
mergeBuilder(n,b);
return b.shellName(useNewer(shellName, n.shellName)).build();
}
}

View file

@ -1,18 +1,19 @@
package io.xpipe.core.process; package io.xpipe.core.process;
import io.xpipe.core.store.DataStoreState; import io.xpipe.core.store.DataStoreState;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Setter
@Getter @Getter
@EqualsAndHashCode(callSuper=true)
@SuperBuilder(toBuilder = true)
@Jacksonized @Jacksonized
@SuperBuilder
public class ShellStoreState extends DataStoreState implements OsNameState { public class ShellStoreState extends DataStoreState implements OsNameState {
OsType.Any osType; OsType.Any osType;
@ -25,17 +26,11 @@ public class ShellStoreState extends DataStoreState implements OsNameState {
} }
@Override @Override
public DataStoreState mergeCopy(DataStoreState newer) { public void merge(DataStoreState newer) {
var shellStoreState = (ShellStoreState) newer; var shellStoreState = (ShellStoreState) newer;
var b = toBuilder(); osType = useNewer(osType, shellStoreState.getOsType());
mergeBuilder(shellStoreState, b); osName = useNewer(osName, shellStoreState.getOsName());
return b.build(); shellDialect = useNewer(shellDialect, shellStoreState.getShellDialect());
} running = useNewer(running, shellStoreState.getRunning());
protected void mergeBuilder(ShellStoreState shellStoreState, ShellStoreStateBuilder<?,?> b) {
b.osType(useNewer(osType, shellStoreState.getOsType()))
.osName(useNewer(osName, shellStoreState.getOsName()))
.shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect()))
.running(useNewer(running, shellStoreState.getRunning()));
} }
} }

View file

@ -1,22 +1,49 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import io.xpipe.core.util.JacksonMapper;
import lombok.SneakyThrows;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
@SuperBuilder(toBuilder = true) @SuperBuilder
public abstract class DataStoreState { public abstract class DataStoreState {
public DataStoreState() {} public DataStoreState() {}
@SuppressWarnings("unchecked")
public <DS extends DataStoreState> DS asNeeded() {
return (DS) this;
}
protected static <T> T useNewer(T older, T newer) { protected static <T> T useNewer(T older, T newer) {
return newer != null ? newer : older; return newer != null ? newer : older;
} }
public DataStoreState mergeCopy(DataStoreState newer) { public abstract void merge(DataStoreState newer);
return this;
@SneakyThrows
public DataStoreState deepCopy() {
return JacksonMapper.getDefault().treeToValue(JacksonMapper.getDefault().valueToTree(this), getClass());
}
@Override
public final int hashCode() {
var tree = JacksonMapper.getDefault().valueToTree(this);
return tree.hashCode();
}
@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (o != null && getClass() != o.getClass()) {
return false;
}
var tree = JacksonMapper.getDefault().valueToTree(this);
var otherTree = JacksonMapper.getDefault().valueToTree(o);
return tree.equals(otherTree);
}
@SneakyThrows
public String toString() {
var tree = JacksonMapper.getDefault().valueToTree(this);
return tree.toPrettyString();
} }
} }

View file

@ -1,9 +1,11 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import io.xpipe.core.util.DataStateProvider; import io.xpipe.core.util.DataStateProvider;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import java.util.Arrays; import java.util.Arrays;
import java.util.function.Supplier;
public interface StatefulDataStore<T extends DataStoreState> extends DataStore { public interface StatefulDataStore<T extends DataStoreState> extends DataStore {
@ -17,14 +19,20 @@ public interface StatefulDataStore<T extends DataStoreState> extends DataStore {
return getStateClass().cast(m.invoke(b)); return getStateClass().cast(m.invoke(b));
} }
@SuppressWarnings("unchecked")
default T getState() { default T getState() {
return DataStateProvider.get().getState(this, this::createDefaultState); return (T)
DataStateProvider.get().getState(this, this::createDefaultState).deepCopy();
} }
default void setState(T val) { default void setState(T val) {
DataStateProvider.get().setState(this, val); DataStateProvider.get().setState(this, val);
} }
default T getState(Supplier<T> def) {
return DataStateProvider.get().getState(this, def);
}
@SneakyThrows @SneakyThrows
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
default Class<T> getStateClass() { default Class<T> getStateClass() {

View file

@ -8,6 +8,8 @@ The file transfer mechanism when editing files had some flaws, which under rare
The entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable. The entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable.
There seems to be another separate issue with a PowerShell bug when connecting to a Windows system, causing file uploads to be slow. For now, xpipe can fall back to pwsh if it is installed to work around this issue.
## Git vault improvements ## Git vault improvements
The conflict resolution has been improved The conflict resolution has been improved
@ -15,11 +17,27 @@ The conflict resolution has been improved
- In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections - In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections
- You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore - You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore
## Terminal improvements
The terminal integration got reworked for some terminals:
- iTerm can now launch tabs instead of individual windows. There were also a few issues fixed that prevented it from launching sometimes
- WezTerm now supports tabs on Linux and macOS. The Windows installation detection has been improved to detect all installed versions
- Terminal.app will now launch faster
## Other ## Other
- You can now add simple RDP connections without a file - You can now add simple RDP connections without a file
- Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed - Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed
- The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown - The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown
- Clarify error message for RealVNC servers, highlighting that RealVNC uses a proprietary protocol spec that can't be supported by third-party VNC clients like xpipe
- Fix Linux builds containing unnecessary debug symbols
- Fix AUR package also installing a debug package
- Fix application restart not working properly on macOS
- Fix possibility of selecting own children connections as hosts, causing a stack overflow. Please don't try to create cycles in your connection graphs
- Fix vault secrets not correctly updating unless restarted when changing vault passphrase
- Fix connection launcher desktop shortcuts and URLs not properly executing if xpipe is not running
- Fix move to ... menu sometimes not ordering categories correctly
- Fix SSH command failing on macOS with homebrew openssh package installed - Fix SSH command failing on macOS with homebrew openssh package installed
- Fix SSH connections not opening the correct shell environment on Windows when username contained spaces due to an OpenSSH bug - Fix SSH connections not opening the correct shell environment on Windows systems when username contained spaces due to an OpenSSH bug
- Fix newly added connections not having the correct order - Fix newly added connections not having the correct order
- Fix error messages of external editor programs not being shown when they failed to start

View file

@ -58,7 +58,7 @@ jlink {
] ]
if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { if (org.gradle.internal.os.OperatingSystem.current().isLinux()) {
options += ['--strip-native-debug-symbols'] options.addAll('--strip-native-debug-symbols', 'exclude-debuginfo-files')
} }
if (useBundledJavaFx) { if (useBundledJavaFx) {

View file

@ -31,13 +31,15 @@ public class ScriptGroupStoreProvider implements DataStoreProvider {
var def = StoreToggleComp.<ScriptGroupStore>simpleToggle( var def = StoreToggleComp.<ScriptGroupStore>simpleToggle(
"base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> { "base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> {
var state = s.getState().toBuilder().isDefault(aBoolean).build(); var state = s.getState();
state.setDefault(aBoolean);
s.setState(state); s.setState(state);
}); });
var bring = StoreToggleComp.<ScriptGroupStore>simpleToggle( var bring = StoreToggleComp.<ScriptGroupStore>simpleToggle(
"base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> { "base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> {
var state = s.getState().toBuilder().bringToShell(aBoolean).build(); var state = s.getState();
state.setBringToShell(aBoolean);
s.setState(state); s.setState(state);
}); });

View file

@ -6,14 +6,15 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.Validators; import io.xpipe.app.util.Validators;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellInitCommand; import io.xpipe.core.process.ShellInitCommand;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreState; import io.xpipe.core.store.DataStoreState;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.StatefulDataStore; import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.util.JacksonizedValue; import io.xpipe.core.util.JacksonizedValue;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder; import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@ -221,12 +222,20 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
public abstract List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts(); public abstract List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts();
@Value @FieldDefaults(level = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper=true) @Setter
@SuperBuilder(toBuilder = true) @Getter
@SuperBuilder
@Jacksonized @Jacksonized
public static class State extends DataStoreState { public static class State extends DataStoreState {
boolean isDefault; boolean isDefault;
boolean bringToShell; boolean bringToShell;
@Override
public void merge(DataStoreState newer) {
var s = (State) newer;
isDefault = s.isDefault;
bringToShell = s.bringToShell;
}
} }
} }

View file

@ -56,13 +56,15 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
var def = StoreToggleComp.<SimpleScriptStore>simpleToggle( var def = StoreToggleComp.<SimpleScriptStore>simpleToggle(
"base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> { "base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> {
var state = s.getState().toBuilder().isDefault(aBoolean).build(); var state = s.getState();
state.setDefault(aBoolean);
s.setState(state); s.setState(state);
}); });
var bring = StoreToggleComp.<SimpleScriptStore>simpleToggle( var bring = StoreToggleComp.<SimpleScriptStore>simpleToggle(
"base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> { "base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> {
var state = s.getState().toBuilder().bringToShell(aBoolean).build(); var state = s.getState();
state.setBringToShell(aBoolean);
s.setState(state); s.setState(state);
}); });

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-rc-1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -457,6 +457,3 @@ history=Browsing-historik
skipAll=Spring alle over skipAll=Spring alle over
notes=Bemærkninger notes=Bemærkninger
addNotes=Tilføj noter addNotes=Tilføj noter
order=Bestille ...
stickToTop=Hold dig på toppen
orderAheadOf=Bestil på forhånd ...

View file

@ -451,6 +451,3 @@ history=Browsing-Verlauf
skipAll=Alles überspringen skipAll=Alles überspringen
notes=Anmerkungen notes=Anmerkungen
addNotes=Notizen hinzufügen addNotes=Notizen hinzufügen
order=Bestellen ...
stickToTop=Oben bleiben
orderAheadOf=Vorbestellen ...

View file

@ -454,7 +454,3 @@ history=Browsing history
skipAll=Skip all skipAll=Skip all
notes=Notes notes=Notes
addNotes=Add notes addNotes=Add notes
#context: verb
order=Order ...
stickToTop=Keep on top
orderAheadOf=Order ahead of ...

View file

@ -438,6 +438,3 @@ history=Historial de navegación
skipAll=Saltar todo skipAll=Saltar todo
notes=Notas notes=Notas
addNotes=Añadir notas addNotes=Añadir notas
order=Ordenar ...
stickToTop=Mantener arriba
orderAheadOf=Haz tu pedido antes de ...

View file

@ -438,6 +438,3 @@ history=Historique de navigation
skipAll=Sauter tout skipAll=Sauter tout
notes=Notes notes=Notes
addNotes=Ajouter des notes addNotes=Ajouter des notes
order=Commander...
stickToTop=Garde le dessus
orderAheadOf=Commande en avance...

View file

@ -438,6 +438,3 @@ history=Cronologia di navigazione
skipAll=Salta tutto skipAll=Salta tutto
notes=Note notes=Note
addNotes=Aggiungi note addNotes=Aggiungi note
order=Ordinare ...
stickToTop=Continua a essere in cima
orderAheadOf=Ordina prima di ...

View file

@ -438,6 +438,3 @@ history=閲覧履歴
skipAll=すべてスキップする skipAll=すべてスキップする
notes=備考 notes=備考
addNotes=メモを追加する addNotes=メモを追加する
order=注文する
stickToTop=トップをキープする
orderAheadOf=先に注文する

View file

@ -438,6 +438,3 @@ history=Browsegeschiedenis
skipAll=Alles overslaan skipAll=Alles overslaan
notes=Opmerkingen notes=Opmerkingen
addNotes=Opmerkingen toevoegen addNotes=Opmerkingen toevoegen
order=Bestellen ...
stickToTop=Bovenaan houden
orderAheadOf=Vooruitbestellen ...

View file

@ -438,6 +438,3 @@ history=Histórico de navegação
skipAll=Salta tudo skipAll=Salta tudo
notes=Nota notes=Nota
addNotes=Adiciona notas addNotes=Adiciona notas
order=Encomenda ...
stickToTop=Mantém-te no topo
orderAheadOf=Encomenda antes de ...

View file

@ -438,6 +438,3 @@ history=История просмотров
skipAll=Пропустить все skipAll=Пропустить все
notes=Заметки notes=Заметки
addNotes=Добавляй заметки addNotes=Добавляй заметки
order=Заказать ...
stickToTop=Держись на высоте
orderAheadOf=Заказать заранее ...

View file

@ -439,6 +439,3 @@ history=Tarama geçmişi
skipAll=Tümünü atla skipAll=Tümünü atla
notes=Notlar notes=Notlar
addNotes=Notlar ekleyin addNotes=Notlar ekleyin
order=Sipariş ...
stickToTop=Zirvede kal
orderAheadOf=Önceden sipariş verin ...

View file

@ -438,6 +438,3 @@ history=浏览历史
skipAll=全部跳过 skipAll=全部跳过
notes=说明 notes=说明
addNotes=添加注释 addNotes=添加注释
order=订购 ...
stickToTop=保持在顶部
orderAheadOf=提前订购...

View file

@ -1 +1 @@
9.4-3 9.4