diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java index fcd1ed9c1..4526df46a 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java @@ -170,10 +170,14 @@ final class BrowserBookmarkList extends SimpleComp { super.updateItem(item, empty); if (empty || item == null) { setText(null); + // Don't set image as that would trigger image comp update // and cells are emptied on each change, leading to unnecessary changes // img.set(null); - setGraphic(null); + + // Use opacity instead of visibility as visibility is kinda bugged with web views + setOpacity(0.0); + setFocusTraversable(false); setAccessibleText(null); } else { @@ -190,7 +194,7 @@ final class BrowserBookmarkList extends SimpleComp { img.set(item.getEntry() .getProvider() .getDisplayIconFileName(item.getEntry().getStore())); - setGraphic(imageView); + setOpacity(1.0); setFocusTraversable(true); setAccessibleText( item.getName() + " " + item.getEntry().getProvider().getDisplayName()); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java index 6aac4567a..d6a029e4e 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java @@ -39,11 +39,11 @@ public class ListBoxViewComp extends Comp> { VBox listView = new VBox(); listView.setFocusTraversable(false); - refresh(listView, shown, cache, false); + refresh(listView, shown, all, cache, false); listView.requestLayout(); shown.addListener((ListChangeListener) (c) -> { - refresh(listView, c.getList(), cache, true); + refresh(listView, c.getList(), all, cache, true); }); all.addListener((ListChangeListener) c -> { @@ -57,9 +57,12 @@ public class ListBoxViewComp extends Comp> { return new SimpleCompStructure<>(scroll); } - private void refresh(VBox listView, List c, Map cache, boolean asynchronous) { + private void refresh(VBox listView, List shown, List all, Map cache, boolean asynchronous) { Runnable update = () -> { - var newShown = c.stream() + // Clear cache of unused values + cache.keySet().removeIf(t -> !all.contains(t)); + + var newShown = shown.stream() .map(v -> { if (!cache.containsKey(v)) { cache.put(v, compFunction.apply(v).createRegion()); 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 e69c20993..50fd909f2 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 @@ -1,7 +1,7 @@ package io.xpipe.app.comp.storage.store; -import io.xpipe.app.comp.store.GuiDsStoreCreator; import io.xpipe.app.comp.storage.StorageFilter; +import io.xpipe.app.comp.store.GuiDsStoreCreator; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; @@ -181,7 +181,8 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { public void executeDefaultAction() throws Exception { var found = getDefaultActionProvider().getValue(); if (found != null) { - if (entry.getState().equals(DataStoreEntry.State.COMPLETE_BUT_INVALID) || entry.getState().equals(DataStoreEntry.State.COMPLETE_NOT_VALIDATED)) { + if (entry.getState().equals(DataStoreEntry.State.COMPLETE_BUT_INVALID) + || entry.getState().equals(DataStoreEntry.State.COMPLETE_NOT_VALIDATED)) { executeRefreshAction(); } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java index 0213bce58..1c99f6ef6 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java @@ -4,7 +4,6 @@ import io.xpipe.app.comp.storage.StorageFilter; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.storage.DataStoreEntry; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.Value; @@ -28,7 +27,7 @@ public class StoreSection implements StorageFilter.Filterable { ObservableList children; private static final Comparator COMPARATOR = Comparator.comparing( - o -> o.wrapper.getEntry().getState().equals(DataStoreEntry.State.COMPLETE_AND_VALID) + o -> o.wrapper.getEntry().getState().isUsable() ? o.wrapper.getEntry().getLastModified() : Instant.EPOCH) .reversed() @@ -39,16 +38,9 @@ public class StoreSection implements StorageFilter.Filterable { var topLevel = BindingsHelper.mappedContentBinding( StoreViewState.get().getAllEntries(), storeEntryWrapper -> create(storeEntryWrapper)); var filtered = BindingsHelper.filteredContentBinding(topLevel, section -> { - if (!section.getWrapper().getEntry().getState().isUsable()) { - return true; - } - - var parent = section.getWrapper() - .getEntry() - .getProvider() - .getDisplayParent(section.getWrapper().getEntry().getStore()); - return parent == null - || (DataStorage.get().getStoreEntryIfPresent(parent).isEmpty()); + return DataStorage.get() + .getParent(section.getWrapper().getEntry(), true) + .isEmpty(); }); var ordered = BindingsHelper.orderedContentBinding(filtered, COMPARATOR); return new StoreSection(null, ordered); @@ -59,14 +51,13 @@ public class StoreSection implements StorageFilter.Filterable { return new StoreSection(e, FXCollections.observableArrayList()); } - var filtered = BindingsHelper.filteredContentBinding( - StoreViewState.get().getAllEntries(), - other -> other.getEntry().getState().isUsable() - && e.getEntry() - .getStore() - .equals(other.getEntry() - .getProvider() - .getDisplayParent(other.getEntry().getStore()))); + var filtered = + BindingsHelper.filteredContentBinding(StoreViewState.get().getAllEntries(), other -> { + return DataStorage.get() + .getParent(other.getEntry(), true) + .map(found -> found.equals(e.getEntry())) + .orElse(false); + }); var children = BindingsHelper.mappedContentBinding(filtered, entry1 -> create(entry1)); var ordered = BindingsHelper.orderedContentBinding(children, COMPARATOR); return new StoreSection(e, ordered); 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 83585e677..cad187873 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -134,6 +134,10 @@ public interface DataStoreProvider { String queryInformationString(DataStore store, int length) throws Exception; + default String queryInvalidInformationString(DataStore store, int length) throws Exception { + return null; + } + String toSummaryString(DataStore store, int length); default String i18n(String key) { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java index 77da9ecc2..38faa0691 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java @@ -6,15 +6,12 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import javafx.scene.web.WebView; public class PrettyImageComp extends SimpleComp { @@ -31,6 +28,8 @@ public class PrettyImageComp extends SimpleComp { @Override protected Region createSimple() { var aspectRatioProperty = new SimpleDoubleProperty(1); + var imageAspectRatioProperty = new SimpleDoubleProperty(1); + var svgAspectRatioProperty = new SimpleDoubleProperty(1); var widthProperty = Bindings.createDoubleBinding( () -> { boolean widthLimited = width / height < aspectRatioProperty.doubleValue(); @@ -51,89 +50,84 @@ public class PrettyImageComp extends SimpleComp { } }, aspectRatioProperty); - var image = new SimpleStringProperty(); - var currentNode = new SimpleObjectProperty(); + var stack = new StackPane(); + + { + var svgImageContent = new SimpleStringProperty(); + var storeIcon = SvgView.create(svgImageContent); + SimpleChangeListener.apply(image, newValue -> { + if (AppImages.hasSvgImage(newValue)) { + svgImageContent.set(AppImages.svgImage(newValue)); + } + }); + var ar = Bindings.createDoubleBinding( + () -> { + return storeIcon.getWidth().getValue().doubleValue() + / storeIcon.getHeight().getValue().doubleValue(); + }, + storeIcon.getWidth(), + storeIcon.getHeight()); + svgAspectRatioProperty.bind(ar); + var node = storeIcon.createWebview(); + node.prefWidthProperty().bind(widthProperty); + node.maxWidthProperty().bind(widthProperty); + node.minWidthProperty().bind(widthProperty); + node.prefHeightProperty().bind(heightProperty); + node.maxHeightProperty().bind(heightProperty); + node.minHeightProperty().bind(heightProperty); + stack.getChildren().add(node); + } + + { + var storeIcon = new ImageView(); + storeIcon.setFocusTraversable(false); + storeIcon + .imageProperty() + .bind(Bindings.createObjectBinding( + () -> { + if (!AppImages.hasNormalImage(image.getValue())) { + return null; + } + + return AppImages.image(image.getValue()); + }, + image)); + var ar = Bindings.createDoubleBinding( + () -> { + if (storeIcon.getImage() == null) { + return 1.0; + } + + return storeIcon.getImage().getWidth() + / storeIcon.getImage().getHeight(); + }, + storeIcon.imageProperty()); + imageAspectRatioProperty.bind(ar); + storeIcon.fitWidthProperty().bind(widthProperty); + storeIcon.fitHeightProperty().bind(heightProperty); + storeIcon.setSmooth(true); + stack.getChildren().add(storeIcon); + } + SimpleChangeListener.apply(PlatformThread.sync(value), val -> { image.set(val); - var requiresChange = val == null - || (val.endsWith(".svg") && !(currentNode.get() instanceof WebView) - || !(currentNode.get() instanceof ImageView)); - if (!requiresChange) { - return; - } - aspectRatioProperty.unbind(); if (val == null) { - currentNode.set(new Region()); + stack.getChildren().get(0).setOpacity(0.0); + stack.getChildren().get(1).setOpacity(0.0); } else if (val.endsWith(".svg")) { - var storeIcon = SvgView.create(Bindings.createStringBinding( - () -> { - if (!AppImages.hasSvgImage(image.getValue())) { - return null; - } - - return AppImages.svgImage(image.getValue()); - }, - image)); - var ar = Bindings.createDoubleBinding( - () -> { - return storeIcon.getWidth().getValue().doubleValue() - / storeIcon.getHeight().getValue().doubleValue(); - }, - storeIcon.getWidth(), - storeIcon.getHeight()); - aspectRatioProperty.bind(ar); - var node = storeIcon.createWebview(); - node.prefWidthProperty().bind(widthProperty); - node.maxWidthProperty().bind(widthProperty); - node.minWidthProperty().bind(widthProperty); - node.prefHeightProperty().bind(heightProperty); - node.maxHeightProperty().bind(heightProperty); - node.minHeightProperty().bind(heightProperty); - currentNode.set(node); + aspectRatioProperty.bind(svgAspectRatioProperty); + stack.getChildren().get(0).setOpacity(1.0); + stack.getChildren().get(1).setOpacity(0.0); } else { - var storeIcon = new ImageView(); - storeIcon.setFocusTraversable(false); - storeIcon - .imageProperty() - .bind(Bindings.createObjectBinding( - () -> { - if (!AppImages.hasNormalImage(image.getValue())) { - return null; - } - - return AppImages.image(image.getValue()); - }, - image)); - var ar = Bindings.createDoubleBinding( - () -> { - if (storeIcon.getImage() == null) { - return 1.0; - } - - return storeIcon.getImage().getWidth() - / storeIcon.getImage().getHeight(); - }, - storeIcon.imageProperty()); - aspectRatioProperty.bind(ar); - storeIcon.fitWidthProperty().bind(widthProperty); - storeIcon.fitHeightProperty().bind(heightProperty); - storeIcon.setSmooth(true); - currentNode.set(storeIcon); + aspectRatioProperty.bind(imageAspectRatioProperty); + stack.getChildren().get(0).setOpacity(0.0); + stack.getChildren().get(1).setOpacity(1.0); } }); - var stack = new StackPane(); - SimpleChangeListener.apply(currentNode, val -> { - if (val == null) { - stack.getChildren().clear(); - return; - } - - stack.getChildren().setAll(val); - }); stack.setFocusTraversable(false); stack.setPrefWidth(width); stack.setMinWidth(width); 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 789c272ff..00fa7f544 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -68,24 +68,14 @@ public abstract class DataStorage { } public synchronized boolean refreshChildren(DataStoreEntry e) { - var children = getLogicalStoreChildren(e, false); - return refreshChildren(e, children); - } - - public synchronized boolean refreshChildren(DataStoreEntry e, List oldChildren) { if (!(e.getStore() instanceof FixedHierarchyStore)) { return false; } try { + deleteChildren(e, true); var newChildren = ((FixedHierarchyStore) e.getStore()).listChildren(); - oldChildren.stream().filter(entry -> !newChildren.containsValue(entry.getStore())).forEach(entry -> { - deleteChildren(entry, true); - deleteStoreEntry(entry); - }); - newChildren.entrySet().stream().filter(entry -> oldChildren.stream().noneMatch(old -> old.getStore().equals(entry.getValue()))).forEach(entry -> { - addStoreEntryIfNotPresent(entry.getKey(), entry.getValue()); - }); + newChildren.forEach((key, value) -> addStoreEntryIfNotPresent(key, value)); return newChildren.size() > 0; } catch (Exception ex) { ErrorEvent.fromThrowable(ex).handle(); @@ -95,29 +85,44 @@ public abstract class DataStorage { public synchronized void deleteChildren(DataStoreEntry e, boolean deep) { // Reverse to delete deepest children first - var ordered = getLogicalStoreChildren(e, deep); + var ordered = getStoreChildren(e, false, deep); Collections.reverse(ordered); ordered.forEach(entry -> { - deleteStoreEntry(entry); + synchronized (this) { + this.storeEntries.remove(entry); + } + this.listeners.forEach(l -> l.onStoreRemove(entry)); }); + save(); } - public synchronized List getLogicalStoreChildren(DataStoreEntry entry, boolean deep) { - var children = new ArrayList<>(getStoreEntries().stream() - .filter(other -> { - if (!other.getState().isUsable()) { - return false; - } + public synchronized Optional getParent(DataStoreEntry entry, boolean display) { + if (!entry.getState().isUsable()) { + return Optional.empty(); + } - var parent = other.getProvider().getLogicalParent(other.getStore()); - return Objects.equals(entry.getStore(), parent); - }) - .toList()); + var provider = entry.getProvider(); + var parent = display ? provider.getDisplayParent(entry.getStore()) : provider.getLogicalParent(entry.getStore()); + return parent != null ? getStoreEntryIfPresent(parent) : Optional.empty(); + } + + public synchronized List getStoreChildren(DataStoreEntry entry, boolean display, boolean deep) { + var children = new ArrayList<>(getStoreEntries().stream() + .filter(other -> { + if (!other.getState().isUsable()) { + return false; + } + + var provider = other.getProvider(); + var parent = display ? provider.getDisplayParent(other.getStore()) : provider.getLogicalParent(other.getStore()); + return parent != null && entry.getStore().getClass().equals(parent.getClass()) && entry.getStore().equals(parent); + }) + .toList()); if (deep) { for (DataStoreEntry dataStoreEntry : new ArrayList<>(children)) { - children.addAll(getLogicalStoreChildren(dataStoreEntry, true)); + children.addAll(getStoreChildren(dataStoreEntry, display, true)); } } @@ -174,7 +179,7 @@ public abstract class DataStorage { public synchronized DataStoreEntry getStoreEntry(@NonNull DataStore store) { var entry = storeEntries.stream() - .filter(n -> store.equals(n.getStore())) + .filter(n -> Objects.equals(store.getClass(), n.getStore().getClass()) && store.equals(n.getStore())) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Store not found")); return entry; @@ -195,13 +200,11 @@ public abstract class DataStorage { public void setAndRefreshAsync(DataStoreEntry entry, DataStore s) { ThreadHelper.runAsync(() -> { var old = entry.getStore(); - var children = getLogicalStoreChildren(entry, false); + deleteChildren(entry, true); try { entry.setStoreInternal(s, false); entry.refresh(true); - // Update old children - children.forEach(entry1 -> propagateUpdate(entry1)); - DataStorage.get().refreshChildren(entry, children); + DataStorage.get().refreshChildren(entry); } catch (Exception e) { entry.setStoreInternal(old, false); entry.simpleRefresh(); @@ -222,7 +225,7 @@ public abstract class DataStorage { } void propagateUpdate(DataStoreEntry origin) { - getLogicalStoreChildren(origin, false).forEach(entry -> { + getStoreChildren(origin, false, false).forEach(entry -> { entry.simpleRefresh(); propagateUpdate(entry); }); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index d8d63fd13..8d5e6cbab 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -46,6 +46,9 @@ public class DataStoreEntry extends StorageElement { @NonFinal boolean expanded; + @NonFinal + DataStoreProvider provider; + private DataStoreEntry( Path directory, UUID uuid, @@ -65,6 +68,7 @@ public class DataStoreEntry extends StorageElement { this.state = state; this.configuration = configuration; this.expanded = expanded; + this.provider = store != null ? DataStoreProviders.byStoreClass(store.getClass()).orElse(null) : null; } @SneakyThrows @@ -207,6 +211,7 @@ public class DataStoreEntry extends StorageElement { lastModified = Instant.now(); information = e.information; dirty = true; + provider = e.provider; simpleRefresh(); } @@ -230,6 +235,7 @@ public class DataStoreEntry extends StorageElement { store = null; state = State.LOAD_FAILED; information = null; + provider = null; dirty = dirty || oldStore != null; listeners.forEach(l -> l.onUpdate()); } else { @@ -241,6 +247,7 @@ public class DataStoreEntry extends StorageElement { dirty = dirty || !nodesEqual; store = newStore; + provider = DataStoreProviders.byStoreClass(newStore.getClass()).orElse(null); var complete = newStore.isComplete(); try { @@ -261,13 +268,18 @@ public class DataStoreEntry extends StorageElement { ? State.COMPLETE_NOT_VALIDATED : state; state = stateToUse; + information = state == State.COMPLETE_AND_VALID + ? information + : state == State.COMPLETE_BUT_INVALID + ? getProvider().queryInvalidInformationString(getStore(), 50) + : null; } else { var stateToUse = state == State.LOAD_FAILED ? State.COMPLETE_BUT_INVALID : State.INCOMPLETE; state = stateToUse; } } catch (Exception e) { - state = store.isComplete() ? State.COMPLETE_BUT_INVALID : State.INCOMPLETE; - information = null; + state = State.COMPLETE_BUT_INVALID; + information = getProvider().queryInvalidInformationString(getStore(), 50); throw e; } finally { propagateUpdate(); @@ -318,11 +330,7 @@ public class DataStoreEntry extends StorageElement { } public DataStoreProvider getProvider() { - if (store == null) { - return null; - } - - return DataStoreProviders.byStoreClass(store.getClass()).orElse(null); + return provider; } @Getter diff --git a/app/src/main/java/io/xpipe/app/util/FileBridge.java b/app/src/main/java/io/xpipe/app/util/FileBridge.java index b77817102..573faeea7 100644 --- a/app/src/main/java/io/xpipe/app/util/FileBridge.java +++ b/app/src/main/java/io/xpipe/app/util/FileBridge.java @@ -121,8 +121,23 @@ public class FileBridge { return Optional.empty(); } + public void openReadOnlyString(String input, Consumer fileConsumer) { + if (input == null) { + input = ""; + } + + var id = UUID.randomUUID(); + String s = input; + openIO( + id.toString(), + id, + () -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)), + null, + fileConsumer); + } + public void openString( - String keyName, Object key, String input, Consumer output, Consumer consumer) { + String keyName, Object key, String input, Consumer output, Consumer fileConsumer) { if (input == null) { input = ""; } @@ -139,7 +154,7 @@ public class FileBridge { output.accept(new String(toByteArray(), StandardCharsets.UTF_8)); } }, - consumer); + fileConsumer); } public void openIO( diff --git a/app/src/main/java/io/xpipe/app/util/FileOpener.java b/app/src/main/java/io/xpipe/app/util/FileOpener.java index e6a8c3e5f..707b63d8a 100644 --- a/app/src/main/java/io/xpipe/app/util/FileOpener.java +++ b/app/src/main/java/io/xpipe/app/util/FileOpener.java @@ -83,6 +83,10 @@ public class FileOpener { } } + public static void openReadOnlyString(String input) { + FileBridge.get().openReadOnlyString(input, s -> openInTextEditor(s)); + } + public static void openString(String keyName, Object key, String input, Consumer output) { FileBridge.get().openString(keyName, key, input, output, file -> openInTextEditor(file)); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/actions/DeleteStoreChildrenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/actions/DeleteStoreChildrenAction.java index 30037ed77..fa39082fc 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/actions/DeleteStoreChildrenAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/actions/DeleteStoreChildrenAction.java @@ -5,6 +5,7 @@ import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.FixedHierarchyStore; import javafx.beans.value.ObservableValue; import lombok.Value; @@ -47,8 +48,8 @@ public class DeleteStoreChildrenAction implements ActionProvider { @Override public boolean isApplicable(DataStore o) { - return DataStorage.get() - .getLogicalStoreChildren(DataStorage.get().getStoreEntry(o), true) + return !(o instanceof FixedHierarchyStore) && DataStorage.get() + .getStoreChildren(DataStorage.get().getStoreEntry(o), true, true) .size() > 1; } 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 aa5e2cbc9..33834a19b 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,7 @@ tilda=Tilda copyShareLink=Copy share link selectStore=Select Store saveSource=Save for later -deleteChildren=Delete children +deleteChildren=Remove children selectSource=Select Source commandLineRead=Update commandLineWrite=Write