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 index a251bd49b..8548356bc 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java @@ -43,7 +43,7 @@ public class StoreCategoryComp extends SimpleComp { @Override protected Region createSimple() { var i = Bindings.createStringBinding(() -> { - if (!DataStorage.get().supportsSharing()) { + if (!DataStorage.get().supportsSharing() || category.getCategory().getParentCategory() == null) { return "mdal-keyboard_arrow_right"; } @@ -108,25 +108,27 @@ public class StoreCategoryComp extends SimpleComp { }); 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); + if (category.getCategory().getParentCategory() != null) { + 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 -> { diff --git a/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java b/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java index c39072053..958224ac3 100644 --- a/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java +++ b/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java @@ -71,6 +71,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer { for (int i = 0; i < elements.size(); i++) { // add to GridPane Element element = elements.get(i); + var offset = preferencesGroup.getTitle() != null ? 15 : 0; if (element instanceof Field f) { SimpleControl c = (SimpleControl) f.getRenderer(); c.setField(f); @@ -126,8 +127,6 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer { styleClass.append("-last"); } - var offset = preferencesGroup.getTitle() != null ? 15 : 0; - GridPane.setMargin(descriptionLabel, new Insets(SPACING, 0, 0, offset)); GridPane.setMargin(node, new Insets(SPACING, 0, 0, offset)); @@ -144,6 +143,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer { if (element instanceof LazyNodeElement nodeElement) { var node = nodeElement.getNode(); grid.add(node, 0, i + rowAmount); + GridPane.setMargin(node, new Insets(SPACING, 0, 0, offset)); } } } diff --git a/app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java b/app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java index 5812573b9..8a0bb2b51 100644 --- a/app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/PasswordCategory.java @@ -1,7 +1,6 @@ package io.xpipe.app.prefs; import atlantafx.base.theme.Styles; -import com.dlsc.formsfx.model.structure.Element; import com.dlsc.preferencesfx.model.Category; import com.dlsc.preferencesfx.model.Group; import com.dlsc.preferencesfx.model.Setting; @@ -10,10 +9,9 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.store.LocalStore; import io.xpipe.core.process.CommandControl; import io.xpipe.core.process.ShellDialects; -import javafx.beans.property.Property; +import io.xpipe.core.store.LocalStore; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -30,9 +28,6 @@ public class PasswordCategory extends AppPrefsCategory { @SneakyThrows public Category create() { - var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class); - ctr.setAccessible(true); - var testPasswordManagerValue = new SimpleStringProperty(); Runnable test = () -> { prefs.save(); diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java index 5d5a948a7..ec88b9691 100644 --- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java @@ -9,16 +9,18 @@ import com.dlsc.preferencesfx.model.Group; import com.dlsc.preferencesfx.model.Setting; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.DesktopHelper; import io.xpipe.app.util.LockChangeAlert; +import io.xpipe.app.util.OptionsBuilder; import io.xpipe.core.util.XPipeInstallation; import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import lombok.SneakyThrows; -import java.util.List; - import static io.xpipe.app.prefs.AppPrefs.group; public class VaultCategory extends AppPrefsCategory { @@ -57,22 +59,25 @@ public class VaultCategory extends AppPrefsCategory { @SneakyThrows public Category create() { - var pro = true; BooleanField enable = BooleanField.ofBooleanType(prefs.enableGitStorage) - .editable(pro) .render(() -> { return new CustomToggleControl(); }); StringField remote = StringField.ofStringType(prefs.storageGitRemote) - .editable(pro) .render(() -> { var c = new SimpleTextControl(); c.setPrefWidth(1000); return c; }); - if (!pro) { - prefs.getProRequiredSettings().addAll(List.of(enable, remote)); - } + + var openDataDir = lazyNode( + "openDataDir", new OptionsBuilder().name("openDataDir").description("openDataDirDescription").addComp( + new ButtonComp(AppI18n.observable("openDataDirButton"), () -> { + DesktopHelper.browsePath(DataStorage.get().getDataDir()); + }) + ).buildComp().padding(new Insets(25, 0, 0, 0)), + null); + return Category.of( "vault", group( @@ -84,7 +89,8 @@ public class VaultCategory extends AppPrefsCategory { Setting.of( "storageGitRemote", remote, - prefs.storageGitRemote)), + prefs.storageGitRemote), + openDataDir), group( "storage", STORAGE_DIR_FIXED 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 7248dbd9f..f578123b9 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -113,6 +113,10 @@ public abstract class DataStorage { return dir.resolve("stores"); } + public Path getDataDir() { + return dir.resolve("data"); + } + protected Path getStreamsDir() { return dir.resolve("streams"); } @@ -174,6 +178,10 @@ public abstract class DataStorage { } public void updateCategory(DataStoreEntry entry, DataStoreCategory newCategory) { + if (getStoreCategoryIfPresent(entry.getUuid()).map(category -> category.equals(newCategory)).orElse(false)) { + return; + } + var children = getDeepStoreChildren(entry); var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove)); @@ -518,6 +526,7 @@ public abstract class DataStorage { private List getHierarchy(DataStoreEntry entry) { var es = new ArrayList(); + es.add(entry); DataStoreEntry current = entry; while ((current = getDisplayParent(current).orElse(null)) != null) { diff --git a/app/src/main/java/io/xpipe/app/storage/LocalFileReference.java b/app/src/main/java/io/xpipe/app/storage/LocalFileReference.java new file mode 100644 index 000000000..d5fd930eb --- /dev/null +++ b/app/src/main/java/io/xpipe/app/storage/LocalFileReference.java @@ -0,0 +1,41 @@ +package io.xpipe.app.storage; + +import io.xpipe.core.process.OsType; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.Value; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; + +@Value +@AllArgsConstructor +public class LocalFileReference { + + public static LocalFileReference of(String s) { + if (s == null) { + return null; + } + + var replaced = s.trim().replace("", DataStorage.get().getDataDir().toString()); + try { + return new LocalFileReference(Path.of(replaced).toString()); + } catch (InvalidPathException ex) { + return new LocalFileReference(replaced); + } + } + + @NonNull + String path; + + public String serialize() { + var start = DataStorage.get().getDataDir(); + try { + if (Path.of(path).startsWith(start)) { + return "" + OsType.getLocal().getFileSystemSeparator() + start.relativize(Path.of(path)); + } + } catch (InvalidPathException ignored) {} + + return path.toString(); + } +} diff --git a/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java b/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java index 03ac3ec9f..2168e0780 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java @@ -19,10 +19,28 @@ public class StorageJacksonModule extends SimpleModule { public void setupModule(SetupContext context) { addSerializer(DataStoreEntryRef.class, new DataStoreEntryRefSerializer()); addDeserializer(DataStoreEntryRef.class, new DataStoreEntryRefDeserializer()); + addSerializer(LocalFileReference.class, new LocalFileReferenceSerializer()); + addDeserializer(LocalFileReference.class, new LocalFileReferenceDeserializer()); context.addSerializers(_serializers); context.addDeserializers(_deserializers); } + public static class LocalFileReferenceSerializer extends JsonSerializer { + + @Override + public void serialize(LocalFileReference value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(value.serialize()); + } + } + + public static class LocalFileReferenceDeserializer extends JsonDeserializer { + + @Override + public LocalFileReference deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return LocalFileReference.of(p.getValueAsString()); + } + } + @SuppressWarnings("all") public static class DataStoreEntryRefSerializer extends JsonSerializer { diff --git a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java index ab633d4f4..e7cb43aa1 100644 --- a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java +++ b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java @@ -5,6 +5,7 @@ import io.xpipe.core.store.LocalStore; import io.xpipe.core.process.OsType; import java.awt.*; +import java.nio.file.Files; import java.nio.file.Path; public class DesktopHelper { @@ -30,6 +31,10 @@ public class DesktopHelper { return; } + if (!Files.exists(file)) { + return; + } + ThreadHelper.runAsync(() -> { try { Desktop.getDesktop().open(file.toFile()); diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties index 146b818e4..71a02929a 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties @@ -12,6 +12,9 @@ editorProgramDescription=The default text editor to use when editing any kind of windowOpacity=Window opacity windowOpacityDescription=Changes the window opacity to keep track of what is happening in the background. useSystemFont=Use system font +openDataDir=Vault data directory +openDataDirButton=Open data directory +openDataDirDescription=If you want to sync additional files, such as SSH keys, across systems with your git repository, you can put them into the storage data directory. Any files referenced there will have their file paths automatically adapted on any synced system. updates=Updates passwordKey=Password key selectAll=Select all diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/storage_readme.md b/app/src/main/resources/io/xpipe/app/resources/misc/storage_readme.md index 9652be873..9d482e055 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/storage_readme.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/storage_readme.md @@ -11,3 +11,7 @@ You can then use this repository in all XPipe application instances the same way ## Connection list %s + +## Additional data + +You can sync arbitrary files between systems by putting them into the data subdirectory at `%%USERPROFILE%%\.xpipe\storage\data` or `~/.xpipe/storage/data`. 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 0d9301648..801499265 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 @@ -24,6 +24,10 @@ import java.util.List; public class ScriptGroupStoreProvider implements DataStoreProvider { + public boolean isShareable() { + return true; + } + @Override public Comp customEntryComp(StoreSection sec, boolean preferLarge) { ScriptGroupStore s = sec.getWrapper().getEntry().getStore().asNeeded();