diff --git a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java index 975e029e..87cfa45f 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java @@ -10,9 +10,11 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.layout.Region; +import lombok.AllArgsConstructor; import java.util.function.Consumer; +@AllArgsConstructor public class StoreToggleComp extends SimpleComp { private final String nameKey; diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java index 5ad1f063..16d88f8f 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java @@ -19,6 +19,7 @@ import java.util.Optional; @Getter public class StoreCategoryWrapper { + private final DataStoreCategory root; private final int depth; private final Property name; private final DataStoreCategory category; @@ -30,15 +31,18 @@ public class StoreCategoryWrapper { public StoreCategoryWrapper(DataStoreCategory category) { var d = 0; + DataStoreCategory last = category; DataStoreCategory p = category; while ((p = DataStorage.get() .getStoreCategoryIfPresent(p.getParentCategory()) .orElse(null)) != null) { d++; + last = p; } depth = d; + this.root = last; this.category = category; this.name = new SimpleStringProperty(); this.lastAccess = new SimpleObjectProperty<>(); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java index ada22711..aa739e80 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java @@ -290,6 +290,16 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(item); if (menu != null) { + var run = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); + run.textProperty().bind(AppI18n.observable("base.execute")); + run.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + p.getKey().getDataStoreCallSite().createAction(wrapper.getEntry().getStore().asNeeded()).execute(); + }); + }); + menu.getItems().add(run); + + var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); var url = "xpipe://action/" + p.getKey().getId() + "/" + wrapper.getEntry().getUuid(); @@ -329,15 +339,17 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(browse); } - var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); - StoreViewState.get().getSortedCategories().forEach(storeCategoryWrapper -> { - MenuItem m = new MenuItem(storeCategoryWrapper.getName()); - m.setOnAction(event -> { - wrapper.moveTo(storeCategoryWrapper.getCategory()); + if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) { + var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); + StoreViewState.get().getSortedCategories().forEach(storeCategoryWrapper -> { + MenuItem m = new MenuItem(storeCategoryWrapper.getName()); + m.setOnAction(event -> { + wrapper.moveTo(storeCategoryWrapper.getCategory()); + }); + move.getItems().add(m); }); - move.getItems().add(m); - }); - contextMenu.getItems().add(move); + contextMenu.getItems().add(move); + } var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); del.disableProperty().bind(wrapper.getDeletable().not()); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java index e4721bd2..eb875808 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java @@ -7,7 +7,9 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.ThreadHelper; +import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Pos; import javafx.scene.control.Label; @@ -21,17 +23,28 @@ import org.kordamp.ikonli.javafx.FontIcon; public class StoreEntryListSideComp extends SimpleComp { private Region createGroupListHeader() { - var label = new Label("Connections"); + var label = new Label(); + label.textProperty().bind(Bindings.createStringBinding(() -> { + return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory().getCategory()) ? "Connections" : "Scripts"; + }, StoreViewState.get().getActiveCategory())); label.getStyleClass().add("name"); - var shownList = BindingsHelper.filteredContentBinding( + var all = BindingsHelper.filteredContentBinding( StoreViewState.get().getAllEntries(), + storeEntryWrapper -> { + var cat = DataStorage.get().getStoreCategoryIfPresent(storeEntryWrapper.getEntry().getCategoryUuid()).orElse(null); + var storeRoot = cat != null ? DataStorage.get().getRootCategory(cat) : null; + return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(storeRoot); + }, + StoreViewState.get().getActiveCategory()); + var shownList = BindingsHelper.filteredContentBinding( + all, storeEntryWrapper -> { return storeEntryWrapper.shouldShow( StoreViewState.get().getFilterString().getValue()); }, StoreViewState.get().getFilterString()); - var count = new CountComp<>(shownList, StoreViewState.get().getAllEntries()); + var count = new CountComp<>(shownList, all); var spacer = new Region(); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java index 469b596e..8355a17b 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java @@ -12,6 +12,7 @@ import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import lombok.Getter; import java.time.Duration; @@ -34,8 +35,8 @@ public class StoreEntryWrapper { private final Property> defaultActionProvider; private final BooleanProperty deletable = new SimpleBooleanProperty(); private final BooleanProperty expanded = new SimpleBooleanProperty(); - private final Property category = new SimpleObjectProperty<>(); private final Property persistentState = new SimpleObjectProperty<>(); + private final MapProperty cache = new SimpleMapProperty<>(FXCollections.observableHashMap()); public StoreEntryWrapper(DataStoreEntry entry) { this.entry = entry; @@ -144,6 +145,7 @@ public class StoreEntryWrapper { expanded.setValue(entry.isExpanded()); observing.setValue(entry.isObserving()); persistentState.setValue(entry.getStorePersistentState()); + cache.putAll(entry.getStoreCache()); inRefresh.setValue(entry.isInRefresh()); deletable.setValue(entry.getConfiguration().isDeletable() diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java index b92dbc64..ebc439e6 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java @@ -2,8 +2,8 @@ package io.xpipe.app.comp.storage.store; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.StoreCategoryListComp; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.util.FeatureProvider; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -17,7 +17,7 @@ public class StoreSidebarComp extends SimpleComp { var sideBar = new VerticalComp(List.of( new StoreEntryListSideComp(), new StoreSortComp(), - FeatureProvider.get().organizationComp(), + new StoreCategoryListComp(), Comp.of(() -> new Region()).styleClass("bar").styleClass("filler-bar"))); sideBar.apply(struc -> struc.get().setFillWidth(true)); sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(2), Priority.ALWAYS)); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java index 5c2cb855..ab200078 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java @@ -48,7 +48,7 @@ public class StoreViewState { } catch (Exception exception) { tl = new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0); categories.setAll(new StoreCategoryWrapper(DataStorage.get().getAllCategory())); - activeCategory.setValue(getAllCategory()); + activeCategory.setValue(getAllConnectionsCategory()); ErrorEvent.fromThrowable(exception).handle(); } topLevelSection = tl; @@ -58,6 +58,17 @@ public class StoreViewState { Comparator comparator = new Comparator<>() { @Override public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { + var o1Root = o1.getRoot(); + var o2Root = o2.getRoot(); + + if (o1Root.equals(getAllConnectionsCategory().getCategory()) && !o1Root.equals(o2Root)) { + return -1; + } + + if (o2Root.equals(getAllConnectionsCategory().getCategory()) && !o1Root.equals(o2Root)) { + return 1; + } + if (o1.getParent() == null && o2.getParent() == null) { return 0; } @@ -81,19 +92,18 @@ public class StoreViewState { return categories.sorted(comparator); } - public StoreCategoryWrapper getAllCategory() { + public StoreCategoryWrapper getAllConnectionsCategory() { return categories.stream() .filter(storeCategoryWrapper -> - storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CATEGORY_UUID)) + storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)) .findFirst() .orElseThrow(); } - - public StoreCategoryWrapper getScriptsCategory() { + public StoreCategoryWrapper getAllScriptsCategory() { return categories.stream() .filter(storeCategoryWrapper -> - storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.SCRIPTS_CATEGORY_UUID)) + storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)) .findFirst() .orElseThrow(); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/GuiDsStoreCreator.java b/app/src/main/java/io/xpipe/app/comp/store/GuiDsStoreCreator.java index 9aaf6f8e..46e11179 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/GuiDsStoreCreator.java +++ b/app/src/main/java/io/xpipe/app/comp/store/GuiDsStoreCreator.java @@ -64,8 +64,8 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { Property store, Predicate filter, String initialName, - boolean exists, boolean staticDisplay - ) { + boolean exists, + boolean staticDisplay) { this.parent = parent; this.provider = provider; this.store = store; @@ -115,7 +115,8 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { } }); }, - true, true); + true, + true); } public static void showCreation(DataStoreProvider selected, Predicate filter) { @@ -134,7 +135,8 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { ErrorEvent.fromThrowable(ex).handle(); } }, - false, false); + false, + false); } public static void show( @@ -155,8 +157,8 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { window -> { return new MultiStepComp() { - private final GuiDsStoreCreator creator = - new GuiDsStoreCreator(this, prop, store, filter, initialName, exists, staticDisplay); + private final GuiDsStoreCreator creator = new GuiDsStoreCreator( + this, prop, store, filter, initialName, exists, staticDisplay); @Override protected List setup() { @@ -227,7 +229,21 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { return null; } - return DataStoreEntry.createNew(UUID.randomUUID(), DataStorage.get().getSelectedCategory().getUuid(), name.getValue(), store.getValue()); + var testE = DataStoreEntry.createNew( + UUID.randomUUID(), + DataStorage.get().getSelectedCategory().getUuid(), + name.getValue(), + store.getValue()); + var parent = provider.getValue().getDisplayParent(testE); + return DataStoreEntry.createNew( + UUID.randomUUID(), + parent != null + ? parent.getCategoryUuid() + : DataStorage.get() + .getSelectedCategory() + .getUuid(), + name.getValue(), + store.getValue()); }, entry) .build(); diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index cb2cdad0..4e0bb7bf 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -22,6 +22,10 @@ import java.util.List; public interface DataStoreProvider { + default boolean canMoveCategories() { + return true; + } + default boolean alwaysShowSummary() { return false; } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java index 3c402291..a628ada9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreChoiceComp.java @@ -39,31 +39,16 @@ import java.util.function.Predicate; public class DataStoreChoiceComp extends SimpleComp { public static DataStoreChoiceComp other( - Property> selected, Class clazz, Predicate> filter) { - return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter); + Property> selected, Class clazz, Predicate> filter, StoreCategoryWrapper initialCategory) { + return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter, initialCategory); } - public static DataStoreChoiceComp proxy(Property> selected) { - return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, null); + public static DataStoreChoiceComp proxy(Property> selected, StoreCategoryWrapper initialCategory) { + return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, null, initialCategory); } - public static DataStoreChoiceComp host(Property> selected) { - return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, null); - } - - public static DataStoreChoiceComp environment( - DataStoreEntry self, Property> selected) { - return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, shellStoreDataStoreEntryRef -> shellStoreDataStoreEntryRef.get().getProvider().canHaveSubShells()); - } - - public static DataStoreChoiceComp proxy( - DataStoreEntry self, Property> selected) { - return new DataStoreChoiceComp<>(Mode.PROXY, self, selected, ShellStore.class, null); - } - - public static DataStoreChoiceComp host( - DataStoreEntry self, Property> selected) { - return new DataStoreChoiceComp<>(Mode.HOST, self, selected, ShellStore.class, null); + public static DataStoreChoiceComp host(Property> selected, StoreCategoryWrapper initialCategory) { + return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, null, initialCategory); } public enum Mode { @@ -77,6 +62,7 @@ public class DataStoreChoiceComp extends SimpleComp { private final Property> selected; private final Class storeClass; private final Predicate> applicableCheck; + private final StoreCategoryWrapper initialCategory; private Popover popover; @@ -84,8 +70,9 @@ public class DataStoreChoiceComp extends SimpleComp { // Rebuild popover if we have a non-null condition to allow for the content to be updated in case the condition // changed if (popover == null || applicableCheck != null) { + var cur = StoreViewState.get().getActiveCategory().getValue(); var selectedCategory = new SimpleObjectProperty<>( - StoreViewState.get().getActiveCategory().getValue()); + initialCategory != null ? (initialCategory.getRoot().equals(cur.getRoot()) ? cur : initialCategory) : cur); var filterText = new SimpleStringProperty(); popover = new Popover(); Predicate applicable = storeEntryWrapper -> { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java index 074f00d4..54c9f495 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java @@ -1,6 +1,7 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.comp.base.ListBoxViewComp; +import io.xpipe.app.comp.storage.store.StoreCategoryWrapper; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.storage.DataStoreEntryRef; @@ -19,11 +20,15 @@ public class DataStoreListChoiceComp extends SimpleComp { private final ListProperty> selectedList; private final Class storeClass; private final Predicate> applicableCheck; + private final StoreCategoryWrapper initialCategory; - public DataStoreListChoiceComp(ListProperty> selectedList, Class storeClass, Predicate> applicableCheck) { + public DataStoreListChoiceComp(ListProperty> selectedList, Class storeClass, Predicate> applicableCheck, + StoreCategoryWrapper initialCategory + ) { this.selectedList = selectedList; this.storeClass = storeClass; this.applicableCheck = applicableCheck; + this.initialCategory = initialCategory; } @Override @@ -42,7 +47,7 @@ public class DataStoreListChoiceComp extends SimpleComp { return hbox; }).padding(new Insets(0)).apply(struc -> struc.get().setMinHeight(0)).apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5)); var selected = new SimpleObjectProperty>(); - var add = new DataStoreChoiceComp(DataStoreChoiceComp.Mode.OTHER, null, selected, storeClass, applicableCheck); + var add = new DataStoreChoiceComp(DataStoreChoiceComp.Mode.OTHER, null, selected, storeClass, applicableCheck, initialCategory); selected.addListener((observable, oldValue, newValue) -> { if (newValue != null) { if (!selectedList.contains(newValue) diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java new file mode 100644 index 00000000..f9c00208 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java @@ -0,0 +1,162 @@ +package io.xpipe.app.fxcomps.impl; + +import io.xpipe.app.comp.base.CountComp; +import io.xpipe.app.comp.base.LazyTextFieldComp; +import io.xpipe.app.comp.base.ListBoxViewComp; +import io.xpipe.app.comp.storage.store.StoreCategoryWrapper; +import io.xpipe.app.comp.storage.store.StoreViewState; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.augment.ContextMenuAugment; +import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreCategory; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.Region; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.Comparator; +import java.util.List; +import java.util.Locale; + +@EqualsAndHashCode(callSuper = true) +@Value +public class StoreCategoryComp extends SimpleComp { + + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + + StoreCategoryWrapper category; + + @Override + protected Region createSimple() { + var i = Bindings.createStringBinding( + () -> { + if (!DataStorage.get().supportsSharing()) { + return "mdal-keyboard_arrow_right"; + } + + return category.getShare().getValue() ? + "mdi2a-account-convert" : "mdi2a-account-cancel"; + }, + category.getShare()); + var icon = new IconButtonComp(i).apply(struc -> AppFont.small(struc.get())).apply(struc -> { + struc.get().setAlignment(Pos.CENTER); + struc.get().setPadding(new Insets(0, 0, 6, 0)); + }); + var name = new LazyTextFieldComp(category.nameProperty()) + .apply(struc -> { + struc.get().prefWidthProperty().unbind(); + struc.get().setPrefWidth(100); + struc.getTextField().minWidthProperty().bind(struc.get().widthProperty()); + }) + .styleClass("name") + .createRegion(); + var showing = new SimpleBooleanProperty(); + var settings = new IconButtonComp("mdomz-settings") + .styleClass("settings") + .apply(new ContextMenuAugment<>(mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, () -> { + var cm = createContextMenu(name); + showing.bind(cm.showingProperty()); + return cm; + })); + var shownList = BindingsHelper.filteredContentBinding( + category.getContainedEntries(), + storeEntryWrapper -> { + return storeEntryWrapper.shouldShow( + StoreViewState.get().getFilterString().getValue()); + }, + StoreViewState.get().getFilterString()); + var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")"); + var hover = new SimpleBooleanProperty(); + var h = new HorizontalComp(List.of( + icon, + Comp.hspacer(4), + Comp.of(() -> name), + Comp.hspacer(), + count.hide(BindingsHelper.persist(hover.or(showing))), + settings.hide(BindingsHelper.persist(hover.not().and(showing.not()))))); + h.apply(struc -> hover.bind(struc.get().hoverProperty())); + h.apply(struc -> struc.get().setOnMouseClicked(event -> { + category.select(); + event.consume(); + })); + h.apply(new ContextMenuAugment<>( + mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, () -> createContextMenu(name))); + h.padding(new Insets(0, 10, 0, (category.getDepth() * 8))); + h.styleClass("category-button"); + var l = category.getChildren() + .sorted(Comparator.comparing( + storeCategoryWrapper -> storeCategoryWrapper.getName().toLowerCase(Locale.ROOT))); + var children = new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper)); + + var emptyBinding = Bindings.isEmpty(category.getChildren()); + var v = new VerticalComp(List.of(h, Comp.separator().hide(emptyBinding), children.hide(emptyBinding))); + v.styleClass("category"); + v.apply(struc -> { + SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { + struc.get().pseudoClassStateChanged(SELECTED, val.equals(category)); + }); + }); + + return v.createRegion(); + } + + private ContextMenu createContextMenu(Region text) { + var contextMenu = new ContextMenu(); + AppFont.normal(contextMenu.getStyleableNode()); + + var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick")); + newCategory.setOnAction(event -> { + DataStorage.get() + .addStoreCategory( + DataStoreCategory.createNew(category.getCategory().getUuid(), "New category")); + }); + contextMenu.getItems().add(newCategory); + + var share = new MenuItem(); + share.textProperty().bind(Bindings.createStringBinding(() -> { + if (category.getShare().getValue()) { + return AppI18n.get("unshare"); + } else { + return AppI18n.get("share"); + } + },category.getShare())); + share.graphicProperty().bind(Bindings.createObjectBinding(() -> { + if (category.getShare().getValue()) { + return new FontIcon("mdi2b-block-helper"); + } else { + return new FontIcon("mdi2s-share"); + } + },category.getShare())); + share.setOnAction(event -> { + category.getShare().setValue(!category.getShare().getValue()); + }); + contextMenu.getItems().add(share); + + var refresh = new MenuItem(AppI18n.get("rename"), new FontIcon("mdal-360")); + refresh.setOnAction(event -> { + text.requestFocus(); + }); + contextMenu.getItems().add(refresh); + + var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); + del.setOnAction(event -> { + category.delete(); + }); + contextMenu.getItems().add(del); + + return contextMenu; + } +} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java new file mode 100644 index 00000000..38bf1759 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java @@ -0,0 +1,25 @@ +package io.xpipe.app.fxcomps.impl; + +import io.xpipe.app.comp.storage.store.StoreViewState; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.SimpleComp; +import javafx.scene.layout.Region; + +import java.util.List; + +public class StoreCategoryListComp extends SimpleComp { + + @Override + protected Region createSimple() { + var all = StoreViewState.get().getAllConnectionsCategory(); + var scripts = StoreViewState.get().getAllScriptsCategory(); + return new VerticalComp(List.of( + new StoreCategoryComp(all), + Comp.vspacer(10), + new StoreCategoryComp(scripts))) + .apply(struc -> struc.get().setFillWidth(true)) + .apply(struc -> struc.get().setSpacing(3)) + .styleClass("store-category-bar") + .createRegion(); + } +} diff --git a/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java b/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java index cdd1b725..c7f1a81c 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java @@ -59,7 +59,7 @@ public class DataStateProviderImpl extends DataStateProvider { return; } - var old = entry.get().getStoreCache().put(key, value); + entry.get().setStoreCache(key, value); } @Override @@ -73,8 +73,12 @@ public class DataStateProviderImpl extends DataStateProvider { return def.get(); } - var result = entry.get().getStoreCache().computeIfAbsent(key, k -> def.get()); - return c.cast(result); + var r = entry.get().getStoreCache().get(key); + if (r == null) { + r = def .get(); + entry.get().setStoreCache(key, r); + } + return c.cast(r); } public boolean isInStorage(DataStore store) { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 3ada03b2..5b3c706d 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -23,8 +23,8 @@ import java.util.stream.Stream; public abstract class DataStorage { - public static final UUID ALL_CATEGORY_UUID = UUID.fromString("bfb0b51a-e7a3-4ce4-8878-8d4cb5828d6c"); - public static final UUID SCRIPTS_CATEGORY_UUID = UUID.fromString("19024cf9-d192-41a9-88a6-a22694cf716a"); + public static final UUID ALL_CONNECTIONS_CATEGORY_UUID = UUID.fromString("bfb0b51a-e7a3-4ce4-8878-8d4cb5828d6c"); + public static final UUID ALL_SCRIPTS_CATEGORY_UUID = UUID.fromString("19024cf9-d192-41a9-88a6-a22694cf716a"); public static final UUID PREDEFINED_SCRIPTS_CATEGORY_UUID = UUID.fromString("5faf1d71-0efc-4293-8b70-299406396973"); public static final UUID CUSTOM_SCRIPTS_CATEGORY_UUID = UUID.fromString("d3496db5-b709-41f9-abc0-ee0a660fbab9"); public static final UUID DEFAULT_CATEGORY_UUID = UUID.fromString("97458c07-75c0-4f9d-a06e-92d8cdf67c40"); @@ -68,7 +68,7 @@ public abstract class DataStorage { } public DataStoreCategory getAllCategory() { - return getStoreCategoryIfPresent(ALL_CATEGORY_UUID).orElseThrow(); + return getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID).orElseThrow(); } private static boolean shouldPersist() { @@ -370,6 +370,17 @@ public abstract class DataStorage { public abstract boolean supportsSharing(); + public DataStoreCategory getRootCategory(DataStoreCategory category) { + DataStoreCategory last = category; + DataStoreCategory p = category; + while ((p = DataStorage.get() + .getStoreCategoryIfPresent(p.getParentCategory()) + .orElse(null)) + != null) { + last = p; + } + return last; + } public Optional getStoreCategoryIfPresent(UUID uuid) { if (uuid == null) { @@ -565,7 +576,7 @@ public abstract class DataStorage { } public void deleteStoreCategory(@NonNull DataStoreCategory cat) { - if (cat.getUuid().equals(DEFAULT_CATEGORY_UUID) || cat.getUuid().equals(ALL_CATEGORY_UUID)) { + if (cat.getUuid().equals(DEFAULT_CATEGORY_UUID) || cat.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID)) { return; } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index dba65ab6..959186e6 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -211,6 +211,12 @@ public class DataStoreEntry extends StorageElement { return new DataStoreEntryRef(this); } + public void setStoreCache(String key, Object value) { + if (!Objects.equals(storeCache.put(key, value), value)) { + notifyUpdate(); + } + } + public void setStorePersistentState(Object value) { var changed = !Objects.equals(storePersistentState, value); this.storePersistentState = value; diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java index 35c7f5f7..8869481d 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -150,20 +150,20 @@ public class StandardStorage extends DataStorage { ErrorEvent.fromThrowable(exception.get()).handle(); } - if (getStoreCategoryIfPresent(ALL_CATEGORY_UUID).isEmpty()) { - var cat = DataStoreCategory.createNew(null, ALL_CATEGORY_UUID,"All connections"); - cat.setDirectory(categoriesDir.resolve(ALL_CATEGORY_UUID.toString())); + if (getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID).isEmpty()) { + var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections"); + cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString())); storeCategories.add(cat); } - if (getStoreCategoryIfPresent(SCRIPTS_CATEGORY_UUID).isEmpty()) { - var cat = DataStoreCategory.createNew(null, SCRIPTS_CATEGORY_UUID,"All scripts"); - cat.setDirectory(categoriesDir.resolve(SCRIPTS_CATEGORY_UUID.toString())); + if (getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID).isEmpty()) { + var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts"); + cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString())); storeCategories.add(cat); } if (getStoreCategoryIfPresent(PREDEFINED_SCRIPTS_CATEGORY_UUID).isEmpty()) { - var cat = DataStoreCategory.createNew(SCRIPTS_CATEGORY_UUID, PREDEFINED_SCRIPTS_CATEGORY_UUID,"Predefined"); + var cat = DataStoreCategory.createNew(ALL_SCRIPTS_CATEGORY_UUID, PREDEFINED_SCRIPTS_CATEGORY_UUID, "Predefined"); cat.setDirectory(categoriesDir.resolve(PREDEFINED_SCRIPTS_CATEGORY_UUID.toString())); storeCategories.add(cat); } @@ -176,7 +176,7 @@ public class StandardStorage extends DataStorage { Instant.now(), Instant.now(), true, - ALL_CATEGORY_UUID, + ALL_CONNECTIONS_CATEGORY_UUID, StoreSortMode.ALPHABETICAL_ASC, false )); } @@ -187,9 +187,10 @@ public class StandardStorage extends DataStorage { if (dataStoreCategory.getParentCategory() != null && getStoreCategoryIfPresent(dataStoreCategory.getParentCategory()) .isEmpty()) { - dataStoreCategory.setParentCategory(ALL_CATEGORY_UUID); - } else if (dataStoreCategory.getParentCategory() == null && !dataStoreCategory.getUuid().equals(ALL_CATEGORY_UUID) && !dataStoreCategory.getUuid().equals(SCRIPTS_CATEGORY_UUID)) { - dataStoreCategory.setParentCategory(ALL_CATEGORY_UUID); + dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID); + } else if (dataStoreCategory.getParentCategory() == null && !dataStoreCategory.getUuid().equals(ALL_CONNECTIONS_CATEGORY_UUID) && !dataStoreCategory.getUuid().equals( + ALL_SCRIPTS_CATEGORY_UUID)) { + dataStoreCategory.setParentCategory(ALL_CONNECTIONS_CATEGORY_UUID); } }); diff --git a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java index 6f48eedd..02d94cad 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java @@ -50,7 +50,7 @@ public class DataStoreCategoryChoiceComp extends SimpleComp { textProperty().unbind(); if (w != null) { textProperty().bind(w.nameProperty()); - setPadding(new Insets(6, 6, 6, 8 + (indent ? w.getDepth() * 6 : 0))); + setPadding(new Insets(6, 6, 6, 8 + (indent ? w.getDepth() * 8 : 0))); } } } diff --git a/app/src/main/java/io/xpipe/app/util/FeatureProvider.java b/app/src/main/java/io/xpipe/app/util/FeatureProvider.java index 8705161b..462df882 100644 --- a/app/src/main/java/io/xpipe/app/util/FeatureProvider.java +++ b/app/src/main/java/io/xpipe/app/util/FeatureProvider.java @@ -38,8 +38,6 @@ public abstract class FeatureProvider { public abstract void init(); - public abstract Comp organizationComp(); - public abstract Comp overviewPage(); public abstract GitStorageHandler createStorageHandler(); diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java index b8ced41d..eb9ebf95 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -2,6 +2,7 @@ package io.xpipe.app.util; import io.xpipe.app.comp.base.ListSelectorComp; import io.xpipe.app.comp.base.MultiStepComp; +import io.xpipe.app.comp.storage.store.StoreViewState; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.ext.ScanProvider; @@ -83,11 +84,12 @@ public class ScanAlert { .name("scanAlertChoiceHeader") .description("scanAlertChoiceHeaderDescription") .addComp(new DataStoreChoiceComp<>( - DataStoreChoiceComp.Mode.OTHER, - null, - entry, - ShellStore.class, - store1 -> true) + DataStoreChoiceComp.Mode.OTHER, + null, + entry, + ShellStore.class, + store1 -> true, + StoreViewState.get().getAllConnectionsCategory()) .disable(new SimpleBooleanProperty(initialStore != null))) .name("scanAlertHeader") .description("scanAlertHeaderDescription") diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties index d5de2671..58704584 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties @@ -112,7 +112,7 @@ reportOnGithubDescription=Open a new issue in the GitHub repository reportErrorDescription=Send an error report with optional user feedback and diagnostics info ignoreError=Ignore error ignoreErrorDescription=Ignore this error and continue like nothing happened -provideEmail=Email address (optional, in case you want to get notified about fixes) +provideEmail=How to contact you (optional, only if you want to get notified about fixes) additionalErrorInfo=Provide additional information (optional) additionalErrorAttachments=Select attachments (optional) dataHandlingPolicies=Privacy policy diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index ee60fcfe..d3dde258 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -84,7 +84,7 @@ .browser .bookmark-list *.scroll-bar:horizontal *.thumb, .browser .bookmark-list *.scroll-bar:horizontal *.increment-button, .browser .bookmark-list *.scroll-bar:horizontal *.decrement-button, -.browser .bookmark-list *.scroll-bar:horizontal *.increment-arrow, +.browser .bookmark-list *.scroll-bar:horizontal *.increment-arrow, .browser .bookmark-list *.scroll-bar:horizontal *.decrement-arrow { -fx-background-color: null; -fx-background-radius: 0; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/GroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/GroupStore.java index 1f490637..44d1e839 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/GroupStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/GroupStore.java @@ -5,7 +5,7 @@ import io.xpipe.core.store.DataStore; public interface GroupStore extends DataStore { - DataStoreEntryRef getParent(); + DataStoreEntryRef getParent(); @Override default void checkComplete() throws Exception { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/SelfReferentialStore.java b/ext/base/src/main/java/io/xpipe/ext/base/SelfReferentialStore.java new file mode 100644 index 00000000..32ecedf2 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/SelfReferentialStore.java @@ -0,0 +1,16 @@ +package io.xpipe.ext.base; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.DataStore; + +import java.util.UUID; + +public interface SelfReferentialStore extends DataStore { + + default DataStoreEntry getSelfEntry() { + return DataStorage.get().getStoreEntries().stream().filter(dataStoreEntry -> dataStoreEntry.getStore() == this).findFirst().orElseGet(() -> { + return DataStoreEntry.createNew(UUID.randomUUID(),DataStorage.DEFAULT_CATEGORY_UUID, "Invalid", this); + }); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/MultiScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/MultiScriptStore.java deleted file mode 100644 index 3f76b0a2..00000000 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/MultiScriptStore.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.xpipe.ext.base.script; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.Validators; -import io.xpipe.core.process.ShellControl; -import lombok.Getter; -import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; -import java.util.Objects; - -@SuperBuilder -@Getter -@Jacksonized -@JsonTypeName("multiScript") -public class MultiScriptStore extends ScriptStore { - - @Override - public String prepareDumbScript(ShellControl shellControl) { - return getEffectiveScripts().stream().map(scriptStore -> { - return ((ScriptStore) scriptStore.getStore()).prepareDumbScript(shellControl); - }).filter( - Objects::nonNull).findFirst().orElse(null); - } - - @Override - public String prepareTerminalScript(ShellControl shellControl) { - return getEffectiveScripts().stream().map(scriptStore -> { - return ((ScriptStore) scriptStore.getStore()).prepareDumbScript(shellControl); - }).filter( - Objects::nonNull).findFirst().orElse(null); - } - - @Override - public void checkComplete() throws Exception { - if (scripts != null) { - Validators.contentNonNull(scripts); - for (var script : scripts) { - script.getStore().checkComplete(); - } - } - } - - @Override - public List> getEffectiveScripts() { - return scripts != null ? scripts.stream().filter(scriptStore -> scriptStore != null).toList() : List.of(); - } -} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/MultiScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/MultiScriptStoreProvider.java deleted file mode 100644 index 68d18a11..00000000 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/MultiScriptStoreProvider.java +++ /dev/null @@ -1,117 +0,0 @@ -package io.xpipe.ext.base.script; - -import io.xpipe.app.comp.storage.store.DenseStoreEntryComp; -import io.xpipe.app.comp.storage.store.StoreEntryWrapper; -import io.xpipe.app.comp.storage.store.StoreSection; -import io.xpipe.app.ext.DataStoreProvider; -import io.xpipe.app.ext.GuiDialog; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; -import io.xpipe.app.fxcomps.impl.DataStoreListChoiceComp; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.app.util.OptionsBuilder; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.util.Identifiers; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleListProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import lombok.SneakyThrows; - -import java.util.ArrayList; -import java.util.List; - -public class MultiScriptStoreProvider implements DataStoreProvider { - - @Override - public Comp customEntryComp(StoreSection s, boolean preferLarge) { - return new DenseStoreEntryComp(s.getWrapper(),true, null); - } - - @Override - public boolean alwaysShowSummary() { - return true; - } - - @Override - public boolean shouldHaveChildren() { - return false; - } - - @Override - public boolean shouldEdit() { - return true; - } - - @Override - public boolean isShareable() { - return true; - } - - @Override - public CreationCategory getCreationCategory() { - return CreationCategory.SCRIPT; - } - - @Override - public String getId() { - return "multiScript"; - } - - @SneakyThrows - @Override - public String getDisplayIconFileName(DataStore store) { - return "proc:shellEnvironment_icon.svg"; - } - - @SneakyThrows - @Override - public GuiDialog guiDialog(DataStoreEntry entry, Property store) { - MultiScriptStore st = store.getValue().asNeeded(); - var group = new SimpleObjectProperty<>(st.getGroup()); - var others = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts()))); - return new OptionsBuilder() - .name("scriptGroup") - .description("scriptGroupDescription") - .addComp(DataStoreChoiceComp.other(group, ScriptGroupStore.class, null), group) - .name("snippets") - .description("snippetsDependenciesDescription") - .addComp(new DataStoreListChoiceComp<>(others, ScriptStore.class, scriptStore -> !scriptStore.get().equals(entry) && others.stream().noneMatch(scriptStoreDataStoreEntryRef -> scriptStoreDataStoreEntryRef.getStore().equals(scriptStore))), others) - .nonEmpty() - .bind( - () -> { - return MultiScriptStore.builder().group(group.get()).scripts(others.get()).description(st.getDescription()).build(); - }, - store) - .buildDialog(); - } - - @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - MultiScriptStore st = wrapper.getEntry().getStore().asNeeded(); - return new SimpleStringProperty(st.getDescription()); - } - - @Override - public DataStoreEntry getDisplayParent(DataStoreEntry store) { - MultiScriptStore st = store.getStore().asNeeded(); - return st.getGroup().get(); - } - - @Override - public List> getStoreClasses() { - return List.of(MultiScriptStore.class); - } - - @Override - public DataStore defaultStore() { - return MultiScriptStore.builder().scripts(List.of()).build(); - } - - @Override - public List getPossibleNames() { - return Identifiers.get("multiScript"); - } -} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java b/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java index 28b5c460..b69545f5 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java @@ -7,7 +7,7 @@ import lombok.Setter; @Getter public enum PredefinedScriptGroup { CLINK("Clink", null), - STARSHIP("Starship", "Scripts to enable the starship shell extension"); + STARSHIP("Starship", "Sets up and enables the starship shell prompt"); private final String name; private final String description; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java index 04fcc79e..7d739f6f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java @@ -1,24 +1,40 @@ package io.xpipe.ext.base.script; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.util.JacksonizedValue; import io.xpipe.ext.base.GroupStore; +import io.xpipe.ext.base.SelfReferentialStore; import lombok.Getter; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; +import java.util.List; +import java.util.Set; + @Getter @SuperBuilder @Jacksonized @JsonTypeName("scriptGroup") -public class ScriptGroupStore extends JacksonizedValue implements GroupStore { - - private final String description; +public class ScriptGroupStore extends ScriptStore implements GroupStore, SelfReferentialStore { @Override - public DataStoreEntryRef getParent() { - return null; + public DataStoreEntryRef getParent() { + return group; + } + + @Override + public List getFlattenedScripts(Set seen) { + return getEffectiveScripts().stream().map(scriptStoreDataStoreEntryRef -> { + return scriptStoreDataStoreEntryRef.getStore().getFlattenedScripts(seen); + }).flatMap(List::stream).toList(); + } + + @Override + public List> getEffectiveScripts() { + var self = getSelfEntry(); + return DataStorage.get().getStoreChildren(self, true).stream() + .map(dataStoreEntry -> dataStoreEntry.ref()) + .toList(); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java index d59dfeb4..bd7297df 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java @@ -4,17 +4,58 @@ import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.storage.store.DenseStoreEntryComp; import io.xpipe.app.comp.storage.store.StoreEntryWrapper; import io.xpipe.app.comp.storage.store.StoreSection; +import io.xpipe.app.comp.storage.store.StoreViewState; import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.OptionsBuilder; import io.xpipe.core.store.DataStore; +import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; +import lombok.SneakyThrows; import java.util.List; public class ScriptGroupStoreProvider implements DataStoreProvider { + @SneakyThrows + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + ScriptGroupStore st = store.getValue().asNeeded(); + + var group = new SimpleObjectProperty<>(st.getGroup()); + Property description = new SimpleObjectProperty<>(st.getDescription()); + return new OptionsBuilder() + .name("description") + .description("descriptionDescription") + .addString(description) + .name("scriptGroup") + .description("scriptGroupDescription") + .addComp( + new DataStoreChoiceComp<>( + DataStoreChoiceComp.Mode.OTHER, null, group, ScriptGroupStore.class, ref->! ref.getEntry().equals(entry), StoreViewState.get().getAllScriptsCategory()), + group) + .nonNull() + .bind( + () -> { + return ScriptGroupStore.builder() + .group(group.get()) + .description(st.getDescription()) + .build(); + }, + store) + .buildDialog(); + } + + @Override + public DataStore defaultStore() { + return ScriptGroupStore.builder().build(); + } + @Override public Comp stateDisplay(StoreEntryWrapper w) { return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java index 19b64916..a4b624cf 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java @@ -13,8 +13,10 @@ import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; @SuperBuilder @@ -22,19 +24,22 @@ import java.util.stream.Collectors; @AllArgsConstructor public abstract class ScriptStore extends JacksonizedValue implements DataStore, StatefulDataStore { - public static ShellControl controlWithDefaultScripts(ShellControl pc) { + return controlWithScripts(pc,getDefaultScripts()); + } + + public static ShellControl controlWithScripts(ShellControl pc, List> refs) { pc.onInit(shellControl -> { - var scripts = getDefaultScripts().stream() - .map(simpleScriptStore -> simpleScriptStore.getStore().prepareDumbScript(shellControl)) + var scripts = flatten(refs).stream() + .map(simpleScriptStore -> simpleScriptStore.prepareDumbScript(shellControl)) .filter(Objects::nonNull) .collect(Collectors.joining("\n")); if (!scripts.isBlank()) { shellControl.executeSimpleBooleanCommand(scripts); } - var terminalCommands = getDefaultScripts().stream() - .map(simpleScriptStore -> simpleScriptStore.getStore().prepareTerminalScript(shellControl)) + var terminalCommands = flatten(refs).stream() + .map(simpleScriptStore -> simpleScriptStore.prepareTerminalScript(shellControl)) .filter(Objects::nonNull) .collect(Collectors.joining("\n")); if (!terminalCommands.isBlank()) { @@ -44,14 +49,21 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, return pc; } - public static List> getDefaultScripts() { - var list = DataStorage.get().getStoreEntries().stream() + private static List> getDefaultScripts() { + return DataStorage.get().getStoreEntries().stream() .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore && scriptStore.getState().isDefault()) .map(e -> e.ref()) .toList(); - // TODO: Make unique - return list; + } + + public static List flatten(List> scripts) { + var seen = new HashSet(); + return scripts.stream() + .map(scriptStoreDataStoreEntryRef -> + scriptStoreDataStoreEntryRef.getStore().getFlattenedScripts(seen)) + .flatMap(List::stream) + .toList(); } protected final DataStoreEntryRef group; @@ -83,9 +95,11 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, } } - public abstract String prepareDumbScript(ShellControl shellControl); + public List getFlattenedScripts() { + return getFlattenedScripts(new HashSet<>()); + } - public abstract String prepareTerminalScript(ShellControl shellControl); + protected abstract List getFlattenedScripts(Set seen); public abstract List> getEffectiveScripts(); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java index d313f08f..a4c0669d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java @@ -11,10 +11,9 @@ import lombok.Getter; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; -import java.util.ArrayList; import java.util.List; -import java.util.function.BiFunction; - +import java.util.Set; +import java.util.stream.Stream; @SuperBuilder @Getter @@ -22,37 +21,47 @@ import java.util.function.BiFunction; @JsonTypeName("script") public class SimpleScriptStore extends ScriptStore { - @Override public String prepareDumbScript(ShellControl shellControl) { - return assemble(shellControl, ExecutionType.DUMB_ONLY, ScriptStore::prepareDumbScript); + return assemble(shellControl, ExecutionType.DUMB_ONLY); } - @Override public String prepareTerminalScript(ShellControl shellControl) { - return assemble(shellControl, ExecutionType.TERMINAL_ONLY, ScriptStore::prepareTerminalScript); + return assemble(shellControl, ExecutionType.TERMINAL_ONLY); } - private String assemble(ShellControl shellControl, ExecutionType type, BiFunction function) { - var list = new ArrayList(); - scripts.forEach(scriptStoreDataStoreEntryRef -> { - var s = function.apply(scriptStoreDataStoreEntryRef.getStore(), shellControl); - if (s != null) { - list.add(s); - } - }); - - if ((executionType == type || executionType == ExecutionType.BOTH) && minimumDialect.isCompatibleTo(shellControl.getShellDialect())) { + private String assemble( + ShellControl shellControl, ExecutionType type) { + if ((executionType == type || executionType == ExecutionType.BOTH) + && minimumDialect.isCompatibleTo(shellControl.getShellDialect())) { var script = ScriptHelper.createExecScript(minimumDialect, shellControl, commands); - list.add(shellControl.getShellDialect().sourceScriptCommand(shellControl, script)); + return shellControl.getShellDialect().sourceScriptCommand(shellControl, script); } - var cmd = String.join("\n", list); - return cmd.isEmpty() ? null : cmd; + return null; } @Override public List> getEffectiveScripts() { - return scripts != null ? scripts.stream().filter(scriptStore -> scriptStore != null).toList() : List.of(); + return scripts != null + ? scripts.stream().filter(scriptStore -> scriptStore != null).toList() + : List.of(); + } + + @Override + public List getFlattenedScripts(Set seen) { + var isLoop = seen.contains(this); + seen.add(this); + return Stream.concat( + getEffectiveScripts().stream() + .map(scriptStoreDataStoreEntryRef -> { + return scriptStoreDataStoreEntryRef.getStore().getFlattenedScripts(seen).stream() + .filter(simpleScriptStore -> !seen.contains(simpleScriptStore)) + .peek(simpleScriptStore -> seen.add(simpleScriptStore)) + .toList(); + }) + .flatMap(List::stream), + isLoop ? Stream.of() : Stream.of(this)) + .toList(); } @Getter diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java index 38beac48..d68b3848 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java @@ -7,6 +7,7 @@ import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.storage.store.DenseStoreEntryComp; import io.xpipe.app.comp.storage.store.StoreEntryWrapper; import io.xpipe.app.comp.storage.store.StoreSection; +import io.xpipe.app.comp.storage.store.StoreViewState; import io.xpipe.app.core.AppExtensionManager; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.app.ext.GuiDialog; @@ -50,11 +51,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { return new DenseStoreEntryComp(sec.getWrapper(), true, dropdown); } - @Override - public boolean alwaysShowSummary() { - return true; - } - @Override public boolean shouldHaveChildren() { return false; @@ -132,7 +128,8 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { new DataStoreListChoiceComp<>( others, ScriptStore.class, - scriptStore -> !scriptStore.get().equals(entry) && !others.contains(scriptStore)), + scriptStore -> !scriptStore.get().equals(entry) && !others.contains(scriptStore), StoreViewState.get().getAllScriptsCategory() + ), others) .name("minimumShellDialect") .description("minimumShellDialectDescription") @@ -160,7 +157,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { .description("scriptGroupDescription") .addComp( new DataStoreChoiceComp<>( - DataStoreChoiceComp.Mode.OTHER, null, group, ScriptGroupStore.class, null), + DataStoreChoiceComp.Mode.OTHER, null, group, ScriptGroupStore.class, null, StoreViewState.get().getAllScriptsCategory()), group) .nonNull() .bind( @@ -180,31 +177,30 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { } @Override - public String summaryString(StoreEntryWrapper wrapper) { - SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); - return (scriptStore.isRequiresElevation() ? "Elevated " : "") - + (scriptStore.getMinimumDialect() != null - ? scriptStore.getMinimumDialect().getDisplayName() + " " - : "") - + (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY - ? "Terminal" - : scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.DUMB_ONLY - ? "Background" - : "") - + " Snippet"; + public boolean canMoveCategories() { + return false; } @Override public ObservableValue informationString(StoreEntryWrapper wrapper) { SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); - return new SimpleStringProperty(scriptStore.getDescription()); + return new SimpleStringProperty((scriptStore.isRequiresElevation() ? "Elevated " : "") + + (scriptStore.getMinimumDialect() != null + ? scriptStore.getMinimumDialect().getDisplayName() + " " + : "") + + (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY + ? "Terminal" + : scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.DUMB_ONLY + ? "Background" + : "") + + " Snippet"); } @Override public void storageInit() throws Exception { var cat = DataStorage.get() .addStoreCategoryIfNotPresent(DataStoreCategory.createNew( - DataStorage.SCRIPTS_CATEGORY_UUID, DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID, "My scripts")); + DataStorage.ALL_SCRIPTS_CATEGORY_UUID, DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID, "My scripts")); DataStorage.get() .addStoreEntryIfNotPresent(DataStoreEntry.createNew( UUID.fromString("a9945ad2-db61-4304-97d7-5dc4330691a7"), diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties index 70b61d91..84769e15 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties @@ -24,7 +24,9 @@ newDirectory=New directory copyShareLink=Copy link selectStore=Select Store saveSource=Save for later -deleteChildren=Remove children +execute=Execute +deleteChildren=Remove all children +descriptionDescription=Give this group an optional description selectSource=Select Source commandLineRead=Update commandLineWrite=Write