From 6d88d3926d4f458e4bca946f28b17aa4c3464c34 Mon Sep 17 00:00:00 2001 From: crschnick Date: Thu, 4 Apr 2024 16:45:27 +0000 Subject: [PATCH] More rework --- app/build.gradle | 1 + .../app/browser/BrowserBreadcrumbBar.java | 3 +- .../io/xpipe/app/browser/BrowserComp.java | 19 +- .../io/xpipe/app/browser/BrowserEntry.java | 4 +- .../app/browser/BrowserFileListComp.java | 7 +- .../app/browser/BrowserFileListModel.java | 4 +- .../xpipe/app/browser/BrowserFilterComp.java | 7 +- .../app/browser/BrowserGreetingComp.java | 12 +- .../io/xpipe/app/browser/BrowserModel.java | 14 +- .../io/xpipe/app/browser/BrowserNavBar.java | 7 +- .../app/browser/BrowserOverviewComp.java | 4 +- .../app/browser/BrowserSelectionListComp.java | 4 +- .../app/browser/BrowserStatusBarComp.java | 8 +- .../app/browser/BrowserTransferComp.java | 14 +- .../xpipe/app/browser/BrowserWelcomeComp.java | 22 +- .../xpipe/app/browser/OpenFileSystemComp.java | 3 +- .../xpipe/app/browser/action/LeafAction.java | 4 +- .../icon/BrowserIconDirectoryType.java | 20 +- .../app/browser/icon/BrowserIconFileType.java | 20 +- .../app/browser/icon/FileIconManager.java | 18 +- .../java/io/xpipe/app/comp/AppLayoutComp.java | 8 +- .../io/xpipe/app/comp/base/ButtonComp.java | 3 +- .../io/xpipe/app/comp/base/DropdownComp.java | 7 +- .../io/xpipe/app/comp/base/FontIconComp.java | 48 ++ .../app/comp/base/LazyTextFieldComp.java | 3 +- .../xpipe/app/comp/base/ListBoxViewComp.java | 4 +- .../io/xpipe/app/comp/base/MarkdownComp.java | 3 +- .../xpipe/app/comp/base/MultiContentComp.java | 9 +- .../io/xpipe/app/comp/base/OsLogoComp.java | 9 +- .../xpipe/app/comp/base/SideMenuBarComp.java | 25 +- .../xpipe/app/comp/base/StoreToggleComp.java | 5 +- .../xpipe/app/comp/base/SystemStateComp.java | 5 +- .../xpipe/app/comp/base/TileButtonComp.java | 14 +- .../app/comp/store/StoreCategoryWrapper.java | 38 +- .../app/comp/store/StoreCreationComp.java | 3 +- .../app/comp/store/StoreCreationMenu.java | 3 + .../xpipe/app/comp/store/StoreEntryComp.java | 16 +- .../app/comp/store/StoreEntryListComp.java | 9 +- .../comp/store/StoreEntryListStatusComp.java | 32 +- .../io/xpipe/app/comp/store/StoreSection.java | 21 +- .../app/comp/store/StoreSectionComp.java | 26 +- .../app/comp/store/StoreSectionMiniComp.java | 20 +- .../xpipe/app/comp/store/StoreViewState.java | 6 +- .../java/io/xpipe/app/core/AppGreetings.java | 3 +- .../main/java/io/xpipe/app/core/AppI18n.java | 192 ++++---- .../java/io/xpipe/app/core/AppImages.java | 8 +- .../io/xpipe/app/core/AppLayoutModel.java | 28 +- .../main/java/io/xpipe/app/core/AppTheme.java | 5 +- .../java/io/xpipe/app/core/mode/BaseMode.java | 1 + .../app/exchange/LaunchExchangeImpl.java | 7 +- .../exchange/cli/InstanceExchangeImpl.java | 18 - .../io/xpipe/app/ext/DataStoreProvider.java | 3 +- .../main/java/io/xpipe/app/fxcomps/Comp.java | 13 +- .../io/xpipe/app/fxcomps/impl/ChoiceComp.java | 9 +- .../app/fxcomps/impl/ChoicePaneComp.java | 5 +- .../ContextualFileReferenceChoiceComp.java | 21 +- .../fxcomps/impl/DataStoreFlowChoiceComp.java | 6 +- .../io/xpipe/app/fxcomps/impl/FilterComp.java | 7 +- .../io/xpipe/app/fxcomps/impl/LabelComp.java | 1 + .../app/fxcomps/impl/PrettyImageComp.java | 3 +- .../xpipe/app/fxcomps/impl/PrettySvgComp.java | 3 +- .../app/fxcomps/impl/StoreCategoryComp.java | 14 +- .../io/xpipe/app/fxcomps/impl/SvgView.java | 5 +- .../xpipe/app/fxcomps/impl/TextAreaComp.java | 3 +- .../xpipe/app/fxcomps/impl/TextFieldComp.java | 3 +- .../app/fxcomps/impl/ToggleGroupComp.java | 3 +- ...ooltipAugment.java => TooltipAugment.java} | 17 +- .../app/fxcomps/util/BindingsHelper.java | 287 +----------- .../app/fxcomps/util/ListBindingsHelper.java | 190 ++++++++ .../app/fxcomps/util/PlatformThread.java | 2 - .../io/xpipe/app/fxcomps/util/Shortcuts.java | 2 +- .../fxcomps/util/SimpleChangeListener.java | 19 - .../java/io/xpipe/app/prefs/AppPrefs.java | 24 +- .../java/io/xpipe/app/prefs/AppPrefsComp.java | 3 +- .../xpipe/app/prefs/AppPrefsSidebarComp.java | 17 +- .../xpipe/app/prefs/AppearanceCategory.java | 25 ++ .../app/prefs/ExternalRdpClientType.java | 163 +++++++ .../java/io/xpipe/app/prefs/RdpCategory.java | 32 ++ .../io/xpipe/app/prefs/SupportedLocale.java | 4 +- .../io/xpipe/app/prefs/TerminalCategory.java | 21 +- .../io/xpipe/app/prefs/UpdateCheckComp.java | 4 +- .../app/update/UpdateAvailableAlert.java | 6 + .../xpipe/app/update/XPipeInstanceHelper.java | 84 ---- .../app/util/DataStoreCategoryChoiceComp.java | 3 +- .../java/io/xpipe/app/util/Hyperlinks.java | 1 + .../io/xpipe/app/util/OptionsBuilder.java | 2 +- .../java/io/xpipe/app/util/RdpConfig.java | 64 +++ .../java/io/xpipe/app/util/ScanAlert.java | 3 +- app/src/main/java/module-info.java | 2 +- .../resources/lang/translations_en.properties | 5 +- .../xpipe/app/resources/style/header-bars.css | 16 +- .../io/xpipe/app/resources/style/style.css | 12 +- .../java/io/xpipe/beacon/BeaconClient.java | 47 -- .../java/io/xpipe/beacon/XPipeInstance.java | 17 - .../beacon/exchange/cli/InstanceExchange.java | 31 -- beacon/src/main/java/module-info.java | 1 - build.gradle | 1 + .../java/io/xpipe/core/store/FilePath.java | 5 + .../io/xpipe/core/store/LaunchableStore.java | 8 +- .../java/io/xpipe/core/store/ShellStore.java | 1 - .../io/xpipe/core/util/XPipeInstallation.java | 16 + dist/build.gradle | 1 + .../xpipe/ext/base/action/LaunchAction.java | 11 +- .../ext/base/action/LaunchShortcutAction.java | 55 --- .../xpipe/ext/base/action/XPipeUrlAction.java | 28 +- .../ext/base/browser/FileTypeAction.java | 1 + .../base/store/LaunchableTerminalStore.java | 16 + .../resources/lang/translations_de.properties | 3 - gradle/gradle_scripts/vernacular-1.16.jar | Bin 0 -> 94482 bytes lang/README.md | 35 ++ lang/app/strings/fixed_en.properties | 59 +++ lang/app/strings/translations_de.properties | 413 ++++++++++++++++++ .../app/strings/translations_en.properties | 325 ++++++++++++-- lang/base/strings/translations_de.properties | 76 ++++ .../base/strings}/translations_en.properties | 28 +- lang/base/texts/elevation_de.md | 14 + .../lang => lang/base/texts}/elevation_en.md | 0 lang/base/texts/executionType_de.md | 15 + .../base/texts}/executionType_en.md | 12 +- lang/base/texts/scriptCompatibility_de.md | 13 + .../base/texts}/scriptCompatibility_en.md | 2 +- lang/base/texts/scriptDependencies_de.md | 5 + .../base/texts}/scriptDependencies_en.md | 0 lang/base/texts/script_de.md | 5 + .../lang => lang/base/texts}/script_en.md | 0 lang/jdbc/strings/translations_de.properties | 20 + lang/jdbc/strings/translations_en.properties | 20 + lang/proc/strings/translations_de.properties | 273 ++++++++++++ lang/proc/strings/translations_en.properties | 285 ++++++++++++ lang/proc/texts/elevation_de.md | 11 + lang/proc/texts/elevation_en.md | 11 + lang/proc/texts/environmentScript_de.md | 9 + lang/proc/texts/environmentScript_en.md | 9 + lang/proc/texts/proxmoxPassword_de.md | 3 + lang/proc/texts/proxmoxPassword_en.md | 3 + lang/proc/texts/proxmoxUsername_de.md | 5 + lang/proc/texts/proxmoxUsername_en.md | 5 + .../texts/rdpPasswordAuthentication_en.md | 3 + lang/proc/texts/rdpTunnelHost_en.md | 5 + lang/proc/texts/runTempContainer_de.md | 5 + lang/proc/texts/runTempContainer_en.md | 5 + lang/proc/texts/shellCommand_de.md | 30 ++ lang/proc/texts/shellCommand_en.md | 30 ++ lang/proc/texts/sshConfigs_de.md | 9 + lang/proc/texts/sshConfigs_en.md | 9 + lang/proc/texts/sshDynamicTunnelBinding_de.md | 5 + lang/proc/texts/sshDynamicTunnelBinding_en.md | 5 + lang/proc/texts/sshDynamicTunnelOrigin_de.md | 5 + lang/proc/texts/sshDynamicTunnelOrigin_en.md | 5 + lang/proc/texts/sshGateway_de.md | 9 + lang/proc/texts/sshGateway_en.md | 9 + lang/proc/texts/sshKey_de.md | 55 +++ lang/proc/texts/sshKey_en.md | 55 +++ lang/proc/texts/sshLocalTunnelBinding_de.md | 5 + lang/proc/texts/sshLocalTunnelBinding_en.md | 5 + lang/proc/texts/sshLocalTunnelOrigin_de.md | 7 + lang/proc/texts/sshLocalTunnelOrigin_en.md | 7 + lang/proc/texts/sshOptions_de.md | 22 + lang/proc/texts/sshOptions_en.md | 22 + lang/proc/texts/sshRemoteTunnelBinding_de.md | 5 + lang/proc/texts/sshRemoteTunnelBinding_en.md | 5 + lang/proc/texts/sshRemoteTunnelOrigin_de.md | 7 + lang/proc/texts/sshRemoteTunnelOrigin_en.md | 7 + lang/proc/texts/unknownShell_de.md | 7 + lang/proc/texts/unknownShell_en.md | 7 + lang/proc/texts/vmwarePassword_de.md | 4 + lang/proc/texts/vmwarePassword_en.md | 4 + lang/proc/texts/vncTunnelHost_en.md | 5 + lang/uacc/strings/translations_de.properties | 34 ++ lang/uacc/strings/translations_en.properties | 34 ++ lang/uacc/texts/preview_de.md | 11 + lang/uacc/texts/preview_en.md | 11 + 172 files changed, 3068 insertions(+), 1164 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java delete mode 100644 app/src/main/java/io/xpipe/app/exchange/cli/InstanceExchangeImpl.java rename app/src/main/java/io/xpipe/app/fxcomps/impl/{FancyTooltipAugment.java => TooltipAugment.java} (59%) create mode 100644 app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java delete mode 100644 app/src/main/java/io/xpipe/app/fxcomps/util/SimpleChangeListener.java create mode 100644 app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java create mode 100644 app/src/main/java/io/xpipe/app/prefs/RdpCategory.java delete mode 100644 app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java create mode 100644 app/src/main/java/io/xpipe/app/util/RdpConfig.java delete mode 100644 beacon/src/main/java/io/xpipe/beacon/XPipeInstance.java delete mode 100644 beacon/src/main/java/io/xpipe/beacon/exchange/cli/InstanceExchange.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/LaunchableTerminalStore.java delete mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_de.properties create mode 100644 gradle/gradle_scripts/vernacular-1.16.jar create mode 100644 lang/README.md create mode 100644 lang/app/strings/fixed_en.properties create mode 100644 lang/app/strings/translations_de.properties rename app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties => lang/app/strings/translations_en.properties (56%) create mode 100644 lang/base/strings/translations_de.properties rename {ext/base/src/main/resources/io/xpipe/ext/base/resources/lang => lang/base/strings}/translations_en.properties (74%) create mode 100644 lang/base/texts/elevation_de.md rename {ext/base/src/main/resources/io/xpipe/ext/base/resources/lang => lang/base/texts}/elevation_en.md (100%) create mode 100644 lang/base/texts/executionType_de.md rename {ext/base/src/main/resources/io/xpipe/ext/base/resources/lang => lang/base/texts}/executionType_en.md (56%) create mode 100644 lang/base/texts/scriptCompatibility_de.md rename {ext/base/src/main/resources/io/xpipe/ext/base/resources/lang => lang/base/texts}/scriptCompatibility_en.md (82%) create mode 100644 lang/base/texts/scriptDependencies_de.md rename {ext/base/src/main/resources/io/xpipe/ext/base/resources/lang => lang/base/texts}/scriptDependencies_en.md (100%) create mode 100644 lang/base/texts/script_de.md rename {ext/base/src/main/resources/io/xpipe/ext/base/resources/lang => lang/base/texts}/script_en.md (100%) create mode 100644 lang/jdbc/strings/translations_de.properties create mode 100644 lang/jdbc/strings/translations_en.properties create mode 100644 lang/proc/strings/translations_de.properties create mode 100644 lang/proc/strings/translations_en.properties create mode 100644 lang/proc/texts/elevation_de.md create mode 100644 lang/proc/texts/elevation_en.md create mode 100644 lang/proc/texts/environmentScript_de.md create mode 100644 lang/proc/texts/environmentScript_en.md create mode 100644 lang/proc/texts/proxmoxPassword_de.md create mode 100644 lang/proc/texts/proxmoxPassword_en.md create mode 100644 lang/proc/texts/proxmoxUsername_de.md create mode 100644 lang/proc/texts/proxmoxUsername_en.md create mode 100644 lang/proc/texts/rdpPasswordAuthentication_en.md create mode 100644 lang/proc/texts/rdpTunnelHost_en.md create mode 100644 lang/proc/texts/runTempContainer_de.md create mode 100644 lang/proc/texts/runTempContainer_en.md create mode 100644 lang/proc/texts/shellCommand_de.md create mode 100644 lang/proc/texts/shellCommand_en.md create mode 100644 lang/proc/texts/sshConfigs_de.md create mode 100644 lang/proc/texts/sshConfigs_en.md create mode 100644 lang/proc/texts/sshDynamicTunnelBinding_de.md create mode 100644 lang/proc/texts/sshDynamicTunnelBinding_en.md create mode 100644 lang/proc/texts/sshDynamicTunnelOrigin_de.md create mode 100644 lang/proc/texts/sshDynamicTunnelOrigin_en.md create mode 100644 lang/proc/texts/sshGateway_de.md create mode 100644 lang/proc/texts/sshGateway_en.md create mode 100644 lang/proc/texts/sshKey_de.md create mode 100644 lang/proc/texts/sshKey_en.md create mode 100644 lang/proc/texts/sshLocalTunnelBinding_de.md create mode 100644 lang/proc/texts/sshLocalTunnelBinding_en.md create mode 100644 lang/proc/texts/sshLocalTunnelOrigin_de.md create mode 100644 lang/proc/texts/sshLocalTunnelOrigin_en.md create mode 100644 lang/proc/texts/sshOptions_de.md create mode 100644 lang/proc/texts/sshOptions_en.md create mode 100644 lang/proc/texts/sshRemoteTunnelBinding_de.md create mode 100644 lang/proc/texts/sshRemoteTunnelBinding_en.md create mode 100644 lang/proc/texts/sshRemoteTunnelOrigin_de.md create mode 100644 lang/proc/texts/sshRemoteTunnelOrigin_en.md create mode 100644 lang/proc/texts/unknownShell_de.md create mode 100644 lang/proc/texts/unknownShell_en.md create mode 100644 lang/proc/texts/vmwarePassword_de.md create mode 100644 lang/proc/texts/vmwarePassword_en.md create mode 100644 lang/proc/texts/vncTunnelHost_en.md create mode 100644 lang/uacc/strings/translations_de.properties create mode 100644 lang/uacc/strings/translations_en.properties create mode 100644 lang/uacc/texts/preview_de.md create mode 100644 lang/uacc/texts/preview_en.md diff --git a/app/build.gradle b/app/build.gradle index 724e050fb..34755d2d2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8' api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") + api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar") api 'info.picocli:picocli:4.7.5' api 'org.kohsuke:github-api:1.321' api 'io.sentry:sentry:7.6.0' diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java index ac4b455fc..f6d3bd3b8 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java @@ -3,7 +3,6 @@ package io.xpipe.app.browser; import atlantafx.base.controls.Breadcrumbs; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.core.store.FileNames; import javafx.scene.Node; import javafx.scene.control.Button; @@ -40,7 +39,7 @@ public class BrowserBreadcrumbBar extends SimpleComp { var breadcrumbs = new Breadcrumbs(); breadcrumbs.setMinWidth(0); - SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> { + PlatformThread.sync(model.getCurrentPath()).subscribe( val -> { if (val == null) { breadcrumbs.setSelectedCrumb(null); return; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java index f9a35de3b..eaf7b3a90 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -4,22 +4,19 @@ import atlantafx.base.controls.RingProgressIndicator; import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; import io.xpipe.app.browser.icon.BrowserIconDirectoryType; -import io.xpipe.app.browser.icon.BrowserIconFileType; import io.xpipe.app.browser.icon.FileIconManager; +import io.xpipe.app.browser.icon.BrowserIconFileType; import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; +import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.BooleanScope; -import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; @@ -52,12 +49,6 @@ public class BrowserComp extends SimpleComp { @Override protected Region createSimple() { - BrowserIconFileType.loadDefinitions(); - BrowserIconDirectoryType.loadDefinitions(); - ThreadHelper.runAsync(() -> { - FileIconManager.loadIfNecessary(); - }); - var bookmarksList = new BrowserBookmarkComp(model).vgrow(); var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()) .hide(PlatformThread.sync(Bindings.createBooleanBinding( @@ -133,7 +124,7 @@ public class BrowserComp extends SimpleComp { private Comp createTabs() { var multi = new MultiContentComp(Map., ObservableValue>of( Comp.of(() -> createTabPane()), - BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())), + Bindings.isNotEmpty(model.getOpenFileSystems()), new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), Bindings.createBooleanBinding( () -> { @@ -284,7 +275,7 @@ public class BrowserComp extends SimpleComp { var id = UUID.randomUUID().toString(); tab.setId(id); - SimpleChangeListener.apply(tabs.skinProperty(), newValue -> { + tabs.skinProperty().subscribe(newValue -> { if (newValue != null) { Platform.runLater(() -> { Label l = (Label) tabs.lookup("#" + id + " .tab-label"); @@ -303,7 +294,7 @@ public class BrowserComp extends SimpleComp { if (color != null) { c.getStyleClass().add(color.getId()); } - new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); + new TooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); c.addEventHandler( DragEvent.DRAG_ENTERED, mouseEvent -> Platform.runLater( diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java index 253f684ce..9fdb6cd7a 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java @@ -29,7 +29,7 @@ public class BrowserEntry { return null; } - for (var f : BrowserIconFileType.ALL) { + for (var f : BrowserIconFileType.getAll()) { if (f.matches(rawFileEntry)) { return f; } @@ -43,7 +43,7 @@ public class BrowserEntry { return null; } - for (var f : BrowserIconDirectoryType.ALL) { + for (var f : BrowserIconDirectoryType.getAll()) { if (f.matches(rawFileEntry)) { return f; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index f26090f42..0fea76579 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -9,7 +9,6 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; -import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.HumanReadableFormat; @@ -505,7 +504,7 @@ final class BrowserFileListComp extends SimpleComp { .get(); var quickAccess = new BrowserQuickAccessButtonComp( () -> getTableRow().getItem(), fileList.getFileSystemModel()) - .hide(BindingsHelper.persist(Bindings.createBooleanBinding( + .hide(Bindings.createBooleanBinding( () -> { var item = getTableRow().getItem(); var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY; @@ -514,7 +513,7 @@ final class BrowserFileListComp extends SimpleComp { .equals(fileList.getFileSystemModel().getCurrentParentDirectory()); return notDir || isParentLink; }, - itemProperty()))) + itemProperty()).not().not()) .createRegion(); editing.addListener((observable, oldValue, newValue) -> { @@ -558,7 +557,7 @@ final class BrowserFileListComp extends SimpleComp { // 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); - + // Visibility seems to be bugged, so use opacity setOpacity(0.0); } else { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java index ac21ec5d2..7e7ae8232 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileNames; @@ -33,7 +33,7 @@ public final class BrowserFileListModel { private final ObservableList previousSelection = FXCollections.observableArrayList(); private final ObservableList selection = FXCollections.observableArrayList(); private final ObservableList selectedRaw = - BindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry()); + ListBindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry()); private final Property draggedOverDirectory = new SimpleObjectProperty<>(); private final Property draggedOverEmpty = new SimpleBooleanProperty(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java index f1846e14e..839a393fa 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java @@ -3,9 +3,8 @@ package io.xpipe.app.browser; import atlantafx.base.theme.Styles; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TextFieldComp; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Pos; @@ -29,7 +28,7 @@ public class BrowserFilterComp extends Comp { var expanded = new SimpleBooleanProperty(); var text = new TextFieldComp(filterString, false).createRegion(); var button = new Button(); - new FancyTooltipAugment<>("app.search").augment(button); + new TooltipAugment<>("app.search").augment(button); text.focusedProperty().addListener((observable, oldValue, newValue) -> { if (!newValue && filterString.getValue() == null) { if (button.isFocused()) { @@ -47,7 +46,7 @@ public class BrowserFilterComp extends Comp { text.setMinWidth(0); Styles.toggleStyleClass(text, Styles.LEFT_PILL); - SimpleChangeListener.apply(filterString, val -> { + filterString.subscribe(val -> { if (val == null) { text.getStyleClass().remove(Styles.SUCCESS); } else { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java index 009382a98..3fe6ba9fe 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserGreetingComp.java @@ -2,8 +2,10 @@ package io.xpipe.app.browser; import atlantafx.base.theme.Styles; import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.scene.control.Label; import javafx.scene.layout.Region; @@ -15,7 +17,9 @@ public class BrowserGreetingComp extends SimpleComp { protected Region createSimple() { var r = new Label(getText()); AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> { - r.setText(getText()); + PlatformThread.runLaterIfNeeded(() -> { + r.setText(getText()); + }); }); AppFont.setSize(r, 7); r.getStyleClass().add(Styles.TEXT_BOLD); @@ -27,11 +31,11 @@ public class BrowserGreetingComp extends SimpleComp { var hour = ldt.getHour(); String text; if (hour > 18 || hour < 5) { - text = "Good evening"; + text = AppI18n.get("goodEvening"); } else if (hour < 12) { - text = "Good morning"; + text = AppI18n.get("goodMorning"); } else { - text = "Good afternoon"; + text = AppI18n.get("goodAfternoon"); } return text; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java index 82d47c21c..0d3737320 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -1,6 +1,9 @@ package io.xpipe.app.browser; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.browser.icon.BrowserIconDirectoryType; +import io.xpipe.app.browser.icon.BrowserIconFileType; +import io.xpipe.app.browser.icon.FileIconManager; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BooleanScope; @@ -47,7 +50,7 @@ public class BrowserModel { return; } - BindingsHelper.bindContent(selection, newValue.getFileList().getSelection()); + ListBindingsHelper.bindContent(selection, newValue.getFileList().getSelection()); }); } @@ -139,6 +142,13 @@ public class BrowserModel { return; } + // Only load icons when a file system is opened + ThreadHelper.runAsync(() -> { + BrowserIconFileType.loadDefinitions(); + BrowserIconDirectoryType.loadDefinitions(); + FileIconManager.loadIfNecessary(); + }); + ThreadHelper.runFailableAsync(() -> { OpenFileSystemModel model; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index 3fffcc61e..120de20bb 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -10,7 +10,6 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; @@ -42,7 +41,7 @@ public class BrowserNavBar extends SimpleComp { @Override protected Region createSimple() { var path = new SimpleStringProperty(model.getCurrentPath().get()); - SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> { + model.getCurrentPath().subscribe((newValue) -> { path.set(newValue); }); path.addListener((observable, oldValue, newValue) -> { @@ -58,7 +57,7 @@ public class BrowserNavBar extends SimpleComp { .styleClass(Styles.CENTER_PILL) .styleClass("path-text") .apply(struc -> { - SimpleChangeListener.apply(struc.get().focusedProperty(), val -> { + struc.get().focusedProperty().subscribe(val -> { struc.get() .pseudoClassStateChanged( INVISIBLE, @@ -71,7 +70,7 @@ public class BrowserNavBar extends SimpleComp { } }); - SimpleChangeListener.apply(model.getInOverview(), val -> { + model.getInOverview().subscribe(val -> { // Pseudo classes do not apply if set instantly before shown // If we start a new tab with a directory set, we have to set the pseudo class one pulse later Platform.runLater(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java index 4622876b7..a545860ce 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java @@ -4,7 +4,7 @@ import io.xpipe.app.comp.base.SimpleTitledPaneComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.ShellControl; @@ -66,7 +66,7 @@ public class BrowserOverviewComp extends SimpleComp { var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); - var recent = BindingsHelper.mappedContentBinding( + var recent = ListBindingsHelper.mappedContentBinding( model.getSavedState().getRecentDirectories(), s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())); var recentOverview = new BrowserFileOverviewComp(model, recent, true); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java index cee9a506b..e339be87a 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java @@ -52,9 +52,9 @@ public class BrowserSelectionListComp extends SimpleComp { protected Region createSimple() { var c = new ListBoxViewComp<>(list, list, entry -> { return Comp.of(() -> { - var wv = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 20) + var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24) .createRegion(); - var l = new Label(null, wv); + var l = new Label(null, image); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry))); return l; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java index 963334774..39e2b0e30 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -59,7 +59,7 @@ public class BrowserStatusBarComp extends SimpleComp { private Comp createClipboardStatus() { var cc = BrowserClipboard.currentCopyClipboard; - var ccCount = (BindingsHelper.persist(Bindings.createStringBinding( + var ccCount = Bindings.createStringBinding( () -> { if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) { return cc.getValue().getEntries().size() + " file" @@ -68,7 +68,7 @@ public class BrowserStatusBarComp extends SimpleComp { return null; } }, - cc))); + cc); return new LabelComp(ccCount); } @@ -86,7 +86,7 @@ public class BrowserStatusBarComp extends SimpleComp { .count(); }, model.getFileList().getAll()); - var selectedComp = new LabelComp(BindingsHelper.persist(Bindings.createStringBinding( + var selectedComp = new LabelComp(Bindings.createStringBinding( () -> { if (selectedCount.getValue().intValue() == 0) { return null; @@ -95,7 +95,7 @@ public class BrowserStatusBarComp extends SimpleComp { } }, selectedCount, - allCount))); + allCount)); return selectedComp; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index 29b9b2c37..5955f3bbe 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -6,7 +6,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.impl.*; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.storage.DataStorage; import io.xpipe.core.process.OsType; @@ -39,11 +39,11 @@ public class BrowserTransferComp extends SimpleComp { protected Region createSimple() { var background = new LabelComp(AppI18n.observable("transferDescription")) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) - .visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))); + .visible(Bindings.isEmpty(model.getItems())); var backgroundStack = new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); - var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry()); + var binding = ListBindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry()); var list = new BrowserSelectionListComp( binding, entry -> Bindings.createStringBinding( @@ -70,20 +70,20 @@ public class BrowserTransferComp extends SimpleComp { .flatMap(aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left"))) - .hide(PlatformThread.sync(BindingsHelper.persist(Bindings.isEmpty(model.getItems())))) + .hide(PlatformThread.sync(Bindings.isEmpty(model.getItems()))) .grow(true, false) .apply(struc -> struc.get().setPadding(new Insets(8))); var downloadButton = new IconButtonComp("mdi2d-download", () -> { model.download(); }) - .hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))) + .hide(Bindings.isEmpty(model.getItems())) .disable(PlatformThread.sync(model.getAllDownloaded())) - .apply(new FancyTooltipAugment<>("downloadStageDescription")); + .apply(new TooltipAugment<>("downloadStageDescription")); var clearButton = new IconButtonComp("mdi2c-close", () -> { model.clear(); }) - .hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))); + .hide(Bindings.isEmpty(model.getItems())); var clearPane = Comp.derive( new HorizontalComp(List.of(downloadButton, clearButton)) .apply(struc -> struc.get().setSpacing(10)), diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java index 3700cbf38..a652dda26 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.TileButtonComp; 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.impl.HorizontalComp; @@ -12,6 +13,7 @@ import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettySvgComp; import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; @@ -54,13 +56,14 @@ public class BrowserWelcomeComp extends SimpleComp { hbox.setSpacing(15); if (state == null) { - var header = new Label("Here you will be able to see where you left off last time."); + var header = new Label(); + header.textProperty().bind(AppI18n.observable("browserWelcomeEmpty")); vbox.getChildren().add(header); hbox.setPadding(new Insets(40, 40, 40, 50)); return new VBox(hbox); } - var list = BindingsHelper.filteredContentBinding(state.getEntries(), e -> { + var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> { var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); if (entry.isEmpty()) { return false; @@ -74,13 +77,14 @@ public class BrowserWelcomeComp extends SimpleComp { }); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); - var header = new LabelComp(Bindings.createStringBinding( - () -> { - return !empty.get() - ? "You were recently connected to the following systems:" - : "Here you will be able to see where you left off last time."; - }, - empty)) + var headerBinding = BindingsHelper.flatMap(empty,b -> { + if (b) { + return AppI18n.observable("browserWelcomeEmpty"); + } else { + return AppI18n.observable("browserWelcomeSystems"); + } + }); + var header = new LabelComp(headerBinding) .createRegion(); AppFont.setSize(header, 1); vbox.getChildren().add(header); diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java index b2b15698f..bf123a358 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemComp.java @@ -9,7 +9,6 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.Shortcuts; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -97,7 +96,7 @@ public class OpenFileSystemComp extends SimpleComp { home, model.getCurrentPath().isNull(), fileList, - BindingsHelper.persist(model.getCurrentPath().isNull().not()))); + model.getCurrentPath().isNull().not())); return stack.createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java index 4c7764890..a47044622 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -2,7 +2,7 @@ package io.xpipe.app.browser.action; import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.LicenseProvider; @@ -39,7 +39,7 @@ public interface LeafAction extends BrowserAction { if (getShortcut() != null) { Shortcuts.addShortcut(b, getShortcut()); } - new FancyTooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b); + new TooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b); var graphic = getIcon(model, selected); if (graphic != null) { b.setGraphic(graphic); diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java index 565b1baa5..1c87b296b 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java @@ -15,18 +15,18 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; -public interface BrowserIconDirectoryType { +public abstract class BrowserIconDirectoryType { - List ALL = new ArrayList<>(); + private static final List ALL = new ArrayList<>(); - static BrowserIconDirectoryType byId(String id) { + public synchronized static BrowserIconDirectoryType byId(String id) { return ALL.stream() .filter(fileType -> fileType.getId().equals(id)) .findAny() .orElseThrow(); } - static void loadDefinitions() { + public synchronized static void loadDefinitions() { ALL.add(new BrowserIconDirectoryType() { @Override @@ -74,13 +74,17 @@ public interface BrowserIconDirectoryType { }); } - String getId(); + public static synchronized List getAll() { + return ALL; + } - boolean matches(FileSystem.FileEntry entry); + public abstract String getId(); - String getIcon(FileSystem.FileEntry entry, boolean open); + public abstract boolean matches(FileSystem.FileEntry entry); - class Simple implements BrowserIconDirectoryType { + public abstract String getIcon(FileSystem.FileEntry entry, boolean open); + + public static class Simple extends BrowserIconDirectoryType { @Getter private final String id; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java index 171ead239..58be92326 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java @@ -12,18 +12,18 @@ import java.nio.file.Files; import java.util.*; import java.util.stream.Collectors; -public interface BrowserIconFileType { +public abstract class BrowserIconFileType { - List ALL = new ArrayList<>(); + private static final List ALL = new ArrayList<>(); - static BrowserIconFileType byId(String id) { + public synchronized static BrowserIconFileType byId(String id) { return ALL.stream() .filter(fileType -> fileType.getId().equals(id)) .findAny() .orElseThrow(); } - static void loadDefinitions() { + public synchronized static void loadDefinitions() { AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> { try (var reader = new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) { @@ -53,14 +53,18 @@ public interface BrowserIconFileType { }); } - String getId(); + public static synchronized List getAll() { + return ALL; + } - boolean matches(FileSystem.FileEntry entry); + public abstract String getId(); - String getIcon(); + public abstract boolean matches(FileSystem.FileEntry entry); + + public abstract String getIcon(); @Getter - class Simple implements BrowserIconFileType { + public static class Simple extends BrowserIconFileType { private final String id; private final IconVariant icon; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java index 56f03ea4c..32904253c 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java @@ -11,29 +11,27 @@ public class FileIconManager { public static synchronized void loadIfNecessary() { if (!loaded) { - AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons"); + AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons", true, false); loaded = true; } } - public static String getFileIcon(FileSystem.FileEntry entry, boolean open) { + public static synchronized String getFileIcon(FileSystem.FileEntry entry, boolean open) { if (entry == null) { return null; } - loadIfNecessary(); - var r = entry.resolved(); if (r.getKind() != FileKind.DIRECTORY) { - for (var f : BrowserIconFileType.ALL) { + for (var f : BrowserIconFileType.getAll()) { if (f.matches(r)) { - return getIconPath(f.getIcon()); + return f.getIcon(); } } } else { - for (var f : BrowserIconDirectoryType.ALL) { + for (var f : BrowserIconDirectoryType.getAll()) { if (f.matches(r)) { - return getIconPath(f.getIcon(r, open)); + return f.getIcon(r, open); } } } @@ -42,8 +40,4 @@ public class FileIconManager { ? (open ? "default_folder_opened.svg" : "default_folder.svg") : "default_file.svg"; } - - private static String getIconPath(String name) { - return name; - } } diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java index 53503c7d4..e7161550f 100644 --- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java @@ -7,7 +7,6 @@ import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import javafx.beans.binding.Bindings; @@ -25,14 +24,15 @@ public class AppLayoutComp extends Comp> { var multi = new MultiContentComp(model.getEntries().stream() .collect(Collectors.toMap( entry -> entry.comp(), - entry -> PlatformThread.sync(Bindings.createBooleanBinding( + entry -> Bindings.createBooleanBinding( () -> { return model.getSelected().getValue().equals(entry); }, - model.getSelected()))))); + model.getSelected()))) + ); var pane = new BorderPane(); - var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries()); + var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries()); pane.setCenter(multi.createRegion()); pane.setRight(sidebar.createRegion()); pane.getStyleClass().add("background"); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java index cd6136a8e..dbaa87a16 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -50,7 +49,7 @@ public class ButtonComp extends Comp> { var graphic = getGraphic(); if (graphic instanceof FontIcon f) { // f.iconColorProperty().bind(button.textFillProperty()); - SimpleChangeListener.apply(button.fontProperty(), c -> { + button.fontProperty().subscribe(c -> { f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); }); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java index 183088c1b..1834bf104 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java @@ -4,8 +4,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; 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.fxcomps.util.ListBindingsHelper; import javafx.css.Size; import javafx.css.SizeUnits; import javafx.scene.control.Button; @@ -38,12 +37,12 @@ public class DropdownComp extends Comp> { .createRegion(); button.visibleProperty() - .bind(BindingsHelper.anyMatch(cm.getItems().stream() + .bind(ListBindingsHelper.anyMatch(cm.getItems().stream() .map(menuItem -> menuItem.getGraphic().visibleProperty()) .toList())); var graphic = new FontIcon("mdi2c-chevron-double-down"); - SimpleChangeListener.apply(button.fontProperty(), c -> { + button.fontProperty().subscribe(c -> { graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java b/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java new file mode 100644 index 000000000..99742218b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java @@ -0,0 +1,48 @@ +package io.xpipe.app.comp.base; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.PlatformThread; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.layout.StackPane; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.kordamp.ikonli.javafx.FontIcon; + +@AllArgsConstructor +public class FontIconComp extends Comp { + + @Value + public static class Structure implements CompStructure { + + FontIcon icon; + StackPane pane; + + @Override + public StackPane get() { + return pane; + } + } + + private final ObservableValue icon; + + public FontIconComp(String icon) { + this.icon = new SimpleStringProperty(icon); + } + + @Override + public FontIconComp.Structure createBase() { + var fi = new FontIcon(); + var obs = PlatformThread.sync(icon); + icon.subscribe(val -> { + PlatformThread.runLaterIfNeeded(() -> { + fi.setIconLiteral(val); + }); + }); + + var pane = new StackPane(fi); + return new FontIconComp.Structure(fi, pane); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java index 6cd7c4178..9ca87049d 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; import javafx.scene.control.TextField; @@ -65,7 +64,7 @@ public class LazyTextFieldComp extends Comp { sp.prefHeightProperty().bind(r.prefHeightProperty()); r.setDisable(true); - SimpleChangeListener.apply(currentValue, n -> { + currentValue.subscribe(n -> { PlatformThread.runLaterIfNeeded(() -> { // Check if control value is the same. Then don't set it as that might cause bugs if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) { 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 5cefd9877..f5210c6a3 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 @@ -3,7 +3,7 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.application.Platform; import javafx.collections.ListChangeListener; @@ -88,7 +88,7 @@ public class ListBoxViewComp extends Comp> { } if (!listView.getChildren().equals(newShown)) { - BindingsHelper.setContent(listView.getChildren(), newShown); + ListBindingsHelper.setContent(listView.getChildren(), newShown); } }; diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index da75f6565..41dbf7835 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.Hyperlinks; @@ -59,7 +58,7 @@ public class MarkdownComp extends Comp> { var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow(); wv.getEngine().setUserStyleSheetLocation(url.toString()); - SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> { + PlatformThread.sync(markdown).subscribe(val -> { // Workaround for https://bugs.openjdk.org/browse/JDK-8199014 try { var file = Files.createTempFile(null, ".html"); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java index 852b08438..b2119a86c 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.value.ObservableValue; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -25,9 +24,11 @@ public class MultiContentComp extends SimpleComp { for (Map.Entry, ObservableValue> entry : content.entrySet()) { var region = entry.getKey().createRegion(); stack.getChildren().add(region); - SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> { - region.setManaged(val); - region.setVisible(val); + entry.getValue().subscribe(val -> { + PlatformThread.runLaterIfNeeded(() -> { + region.setManaged(val); + region.setVisible(val); + }); }); } return stack; diff --git a/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java b/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java index a6d27f68b..ae0cebbd1 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java @@ -37,7 +37,7 @@ public class OsLogoComp extends SimpleComp { @Override protected Region createSimple() { - var img = BindingsHelper.persist(Bindings.createObjectBinding( + var img = Bindings.createObjectBinding( () -> { if (state.getValue() != SystemStateComp.State.SUCCESS) { return null; @@ -51,10 +51,11 @@ public class OsLogoComp extends SimpleComp { return getImage(ons.getOsName()); }, wrapper.getPersistentState(), - state)); + state); var hide = BindingsHelper.map(img, s -> s != null); - return new StackComp( - List.of(new SystemStateComp(state).hide(hide), new PrettyImageComp(img, 24, 24).visible(hide))) + return new StackComp(List.of( + new SystemStateComp(state).hide(hide), + new PrettyImageComp(img, 24, 24).visible(hide))) .createRegion(); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java index 3dcea999e..0187a58e9 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java @@ -7,7 +7,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.Augment; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; @@ -73,7 +73,7 @@ public class SideMenuBarComp extends Comp> { var e = entries.get(i); var b = new IconButtonComp(e.icon(), () -> value.setValue(e)); b.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i])); - b.apply(new FancyTooltipAugment<>(e.name())); + b.apply(new TooltipAugment<>(e.name())); b.apply(struc -> { AppFont.setSize(struc.get(), 2); struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e)); @@ -133,7 +133,7 @@ public class SideMenuBarComp extends Comp> { UserReportComp.show(event.build()); }) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()])) - .apply(new FancyTooltipAugment<>("reportIssue")) + .apply(new TooltipAugment<>("reportIssue")) .apply(simpleBorders) .accessibleTextKey("reportIssue"); b.apply(struc -> { @@ -145,7 +145,7 @@ public class SideMenuBarComp extends Comp> { { var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB)) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1])) - .apply(new FancyTooltipAugment<>("visitGithubRepository")) + .apply(new TooltipAugment<>("visitGithubRepository")) .apply(simpleBorders) .accessibleTextKey("visitGithubRepository"); b.apply(struc -> { @@ -157,7 +157,7 @@ public class SideMenuBarComp extends Comp> { { var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD)) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2])) - .apply(new FancyTooltipAugment<>("discord")) + .apply(new TooltipAugment<>("discord")) .apply(simpleBorders) .accessibleTextKey("discord"); b.apply(struc -> { @@ -167,9 +167,20 @@ public class SideMenuBarComp extends Comp> { } { - var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) + var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE)) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3])) - .apply(new FancyTooltipAugment<>("updateAvailableTooltip")) + .apply(new TooltipAugment<>("translate")) + .apply(simpleBorders) + .accessibleTextKey("translate"); + b.apply(struc -> { + AppFont.setSize(struc.get(), 2); + }); + vbox.getChildren().add(b.createRegion()); + } + + { + var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) + .apply(new TooltipAugment<>("updateAvailableTooltip")) .accessibleTextKey("updateAvailableTooltip"); b.apply(struc -> { AppFont.setSize(struc.get(), 2); 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 1d2e52ead..9aa03175b 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 @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.store.StoreSection; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; @@ -32,13 +31,13 @@ public class StoreToggleComp extends SimpleComp { @Override protected Region createSimple() { var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE); - var visible = BindingsHelper.persist(Bindings.createBooleanBinding( + var visible = Bindings.createBooleanBinding( () -> { return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE && section.getShowDetails().get(); }, section.getWrapper().getValidity(), - section.getShowDetails())); + section.getShowDetails()); var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey)) .visible(visible) .disable(disable); diff --git a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java index 34669005e..5b095568d 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.core.process.ShellStoreState; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; @@ -35,7 +34,7 @@ public class SystemStateComp extends SimpleComp { state)); var fi = new FontIcon(); fi.getStyleClass().add("inner-icon"); - SimpleChangeListener.apply(icon, val -> fi.setIconLiteral(val)); + icon.subscribe(val -> fi.setIconLiteral(val)); var border = new FontIcon("mdi2c-circle-outline"); border.getStyleClass().add("outer-icon"); @@ -63,7 +62,7 @@ public class SystemStateComp extends SimpleComp { """; pane.getStylesheets().add(Styles.toDataURI(dataClass1)); - SimpleChangeListener.apply(PlatformThread.sync(state), val -> { + PlatformThread.sync(state).subscribe(val -> { pane.getStylesheets().removeAll(success, failure, other); pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other); }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java index 836e2d45c..8b7867566 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -13,7 +12,6 @@ import javafx.event.ActionEvent; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import lombok.AllArgsConstructor; import lombok.Builder; @@ -56,12 +54,8 @@ public class TileButtonComp extends Comp { var text = new VBox(header, desc); text.setSpacing(2); - var fi = new FontIcon(); - SimpleChangeListener.apply(PlatformThread.sync(icon), val -> { - fi.setIconLiteral(val); - }); - - var pane = new StackPane(fi); + var fi = new FontIconComp(icon).createStructure(); + var pane = fi.getPane(); var hbox = new HBox(pane, text); hbox.setSpacing(8); pane.prefWidthProperty() @@ -76,11 +70,11 @@ public class TileButtonComp extends Comp { desc.heightProperty())); pane.prefHeightProperty().addListener((c, o, n) -> { var size = Math.min(n.intValue(), 100); - fi.setIconSize((int) (size * 0.55)); + fi.getIcon().setIconSize((int) (size * 0.55)); }); bt.setGraphic(hbox); return Structure.builder() - .graphic(fi) + .graphic(fi.getIcon()) .button(bt) .content(hbox) .name(header) diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java index d09c643bc..9d48725a0 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java @@ -1,5 +1,6 @@ package io.xpipe.app.comp.store; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; @@ -80,6 +81,10 @@ public class StoreCategoryWrapper { private void setupListeners() { name.addListener((c, o, n) -> { + if (n.equals(translatedName(category.getName()))) { + return; + } + category.setName(n); }); @@ -91,6 +96,10 @@ public class StoreCategoryWrapper { update(); }); + AppPrefs.get().language().addListener((observable, oldValue, newValue) -> { + update(); + }); + sortMode.addListener((observable, oldValue, newValue) -> { category.setSortMode(newValue); }); @@ -112,8 +121,9 @@ public class StoreCategoryWrapper { public void update() { // Avoid reupdating name when changed from the name property! - if (!category.getName().equals(name.getValue())) { - name.setValue(category.getName()); + var catName = translatedName(category.getName()); + if (!catName.equals(name.getValue())) { + name.setValue(catName); } lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500))); @@ -140,18 +150,30 @@ public class StoreCategoryWrapper { }); } - public String getName() { - return name.getValue(); + private String translatedName(String original) { + if (original.equals("All connections")) { + return AppI18n.get("allConnections"); + } + if (original.equals("All scripts")) { + return AppI18n.get("allScripts"); + } + if (original.equals("Predefined")) { + return AppI18n.get("predefined"); + } + if (original.equals("Custom")) { + return AppI18n.get("custom"); + } + if (original.equals("Default")) { + return AppI18n.get("default"); + } + + return original; } public Property nameProperty() { return name; } - public Instant getLastAccess() { - return lastAccess.getValue(); - } - public Property lastAccessProperty() { return lastAccess; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java index 76dc1dfb5..9a32e93f4 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java @@ -12,7 +12,6 @@ import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ExceptionConverter; import io.xpipe.app.issue.TrackEvent; @@ -381,7 +380,7 @@ public class StoreCreationComp extends DialogComp { providerChoice.apply(GrowAugment.create(true, false)); providerChoice.onSceneAssign(struc -> struc.get().requestFocus()); - SimpleChangeListener.apply(provider, n -> { + provider.subscribe(n -> { if (n != null) { var d = n.guiDialog(existingEntry, store); var propVal = new SimpleValidator(); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java index da11e6bce..f89fd48dd 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java @@ -26,6 +26,9 @@ public class StoreCreationMenu { menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh")); + menu.getItems() + .add(category("addVisual", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.VISUAL, null)); + menu.getItems() .add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null)); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index 9f66920d2..e9348cd68 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -13,9 +13,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.*; -import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreColor; @@ -101,8 +99,7 @@ public abstract class StoreEntryComp extends SimpleComp { var loading = LoadingOverlayComp.noProgress( Comp.of(() -> button), - BindingsHelper.persist( - wrapper.getInRefresh().and(wrapper.getObserving().not()))); + wrapper.getInRefresh().and(wrapper.getObserving().not())); return loading.createRegion(); } @@ -138,7 +135,7 @@ public abstract class StoreEntryComp extends SimpleComp { } protected void applyState(Node node) { - SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> { + PlatformThread.sync(wrapper.getValidity()).subscribe(val -> { switch (val) { case LOAD_FAILED -> { node.pseudoClassStateChanged(FAILED, true); @@ -174,7 +171,7 @@ public abstract class StoreEntryComp extends SimpleComp { var imageComp = PrettyImageHelper.ofFixedSize(img, w, h); var storeIcon = imageComp.createRegion(); if (wrapper.getValidity().getValue().isUsable()) { - new FancyTooltipAugment<>(new SimpleStringProperty( + new TooltipAugment<>(new SimpleStringProperty( wrapper.getEntry().getProvider().getDisplayName())) .augment(storeIcon); } @@ -212,7 +209,7 @@ public abstract class StoreEntryComp extends SimpleComp { }); button.accessibleText( actionProvider.getName(wrapper.getEntry().ref()).getValue()); - button.apply(new FancyTooltipAugment<>( + button.apply(new TooltipAugment<>( actionProvider.getName(wrapper.getEntry().ref()))); if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { button.hide(Bindings.not(p.getValue())); @@ -248,7 +245,7 @@ public abstract class StoreEntryComp extends SimpleComp { settingsButton.accessibleText("More"); settingsButton.apply(new ContextMenuAugment<>( event -> event.getButton() == MouseButton.PRIMARY, null, () -> StoreEntryComp.this.createContextMenu())); - settingsButton.apply(new FancyTooltipAugment<>("more")); + settingsButton.apply(new TooltipAugment<>("more")); return settingsButton; } @@ -371,7 +368,8 @@ public abstract class StoreEntryComp extends SimpleComp { StoreViewState.get() .getSortedCategories(wrapper.getCategory().getValue().getRoot()) .forEach(storeCategoryWrapper -> { - MenuItem m = new MenuItem(storeCategoryWrapper.getName()); + MenuItem m = new MenuItem(); + m.textProperty().bind(storeCategoryWrapper.nameProperty()); m.setOnAction(event -> { wrapper.moveTo(storeCategoryWrapper.getCategory()); event.consume(); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java index 2973a396a..979e8903f 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.HorizontalComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; @@ -50,16 +49,16 @@ public class StoreEntryListComp extends SimpleComp { var map = new LinkedHashMap, ObservableValue>(); map.put( createList(), - BindingsHelper.persist(Bindings.not(Bindings.isEmpty( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren())))); + Bindings.not(Bindings.isEmpty( + StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))); map.put(new StoreIntroComp(), showIntro); map.put( new StoreNotFoundComp(), - BindingsHelper.persist(Bindings.and( + Bindings.and( Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())), Bindings.isEmpty( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren())))); + StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))); return new MultiContentComp(map).createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java index 5eb91dee0..d2412f1bd 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java @@ -5,11 +5,11 @@ 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.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; import javafx.beans.binding.Bindings; @@ -36,7 +36,7 @@ public class StoreEntryListStatusComp extends SimpleComp { public StoreEntryListStatusComp() { this.sortMode = new SimpleObjectProperty<>(); - SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { + StoreViewState.get().getActiveCategory().subscribe(val -> { sortMode.setValue(val.getSortMode().getValue()); }); sortMode.addListener((observable, oldValue, newValue) -> { @@ -51,21 +51,12 @@ public class StoreEntryListStatusComp extends SimpleComp { private Region createGroupListHeader() { var label = new Label(); - label.textProperty() - .bind(Bindings.createStringBinding( - () -> { - return StoreViewState.get() - .getActiveCategory() - .getValue() - .getRoot() - .equals(StoreViewState.get().getAllConnectionsCategory()) - ? "Connections" - : "Scripts"; - }, - StoreViewState.get().getActiveCategory())); + var name = BindingsHelper.flatMap(StoreViewState.get().getActiveCategory(), + categoryWrapper -> AppI18n.observable(categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory()) ? "connections" : "scripts")); + label.textProperty().bind(name); label.getStyleClass().add("name"); - var all = BindingsHelper.filteredContentBinding( + var all = ListBindingsHelper.filteredContentBinding( StoreViewState.get().getAllEntries(), storeEntryWrapper -> { var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot(); @@ -76,7 +67,7 @@ public class StoreEntryListStatusComp extends SimpleComp { .equals(storeRoot); }, StoreViewState.get().getActiveCategory()); - var shownList = BindingsHelper.filteredContentBinding( + var shownList = ListBindingsHelper.filteredContentBinding( all, storeEntryWrapper -> { return storeEntryWrapper.shouldShow( @@ -135,7 +126,8 @@ public class StoreEntryListStatusComp extends SimpleComp { } private Region createButtons() { - var menu = new MenuButton(AppI18n.get("addConnections"), new FontIcon("mdi2p-plus-thick")); + var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick")); + menu.textProperty().bind(AppI18n.observable("addConnections")); menu.setAlignment(Pos.CENTER); menu.setTextAlignment(TextAlignment.CENTER); AppFont.medium(menu); @@ -188,7 +180,7 @@ public class StoreEntryListStatusComp extends SimpleComp { sortMode)); }); alphabetical.accessibleTextKey("sortAlphabetical"); - alphabetical.apply(new FancyTooltipAugment<>("sortAlphabetical")); + alphabetical.apply(new TooltipAugment<>("sortAlphabetical")); return alphabetical; } @@ -227,7 +219,7 @@ public class StoreEntryListStatusComp extends SimpleComp { sortMode)); }); date.accessibleTextKey("sortLastUsed"); - date.apply(new FancyTooltipAugment<>("sortLastUsed")); + date.apply(new TooltipAugment<>("sortLastUsed")); return date; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java index 3780cc91c..c2939b9ac 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java @@ -2,6 +2,7 @@ package io.xpipe.app.comp.store; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; @@ -64,10 +65,10 @@ public class StoreSection { var c = Comparator.comparingInt( value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); - var mappedSortMode = BindingsHelper.mappedBinding( + var mappedSortMode = BindingsHelper.flatMap( category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); - return BindingsHelper.orderedContentBinding( + return ListBindingsHelper.orderedContentBinding( list, (o1, o2) -> { var current = mappedSortMode.getValue(); @@ -86,16 +87,16 @@ public class StoreSection { Predicate entryFilter, ObservableStringValue filterString, ObservableValue category) { - var topLevel = BindingsHelper.filteredContentBinding( + var topLevel = ListBindingsHelper.filteredContentBinding( all, section -> { return DataStorage.get().isRootEntry(section.getEntry()); }, category); - var cached = BindingsHelper.cachedMappedContentBinding( - topLevel, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); + var cached = ListBindingsHelper.cachedMappedContentBinding( + topLevel, topLevel, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); - var shown = BindingsHelper.filteredContentBinding( + var shown = ListBindingsHelper.filteredContentBinding( ordered, section -> { var showFilter = filterString == null || section.shouldShow(filterString.get()); @@ -121,7 +122,7 @@ public class StoreSection { return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth); } - var allChildren = BindingsHelper.filteredContentBinding(all, other -> { + var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> { // Legacy implementation that does not use children caches. Use for testing // if (true) return DataStorage.get() // .getDisplayParent(other.getEntry()) @@ -131,10 +132,10 @@ public class StoreSection { // This check is fast as the children are cached in the storage return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); }); - var cached = BindingsHelper.cachedMappedContentBinding( - allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); + var cached = ListBindingsHelper.cachedMappedContentBinding( + allChildren, allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); - var filtered = BindingsHelper.filteredContentBinding( + var filtered = ListBindingsHelper.filteredContentBinding( ordered, section -> { var showFilter = filterString == null || section.shouldShow(filterString.get()); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java index 96d111eda..8c6a9b764 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java @@ -7,8 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; @@ -40,11 +39,11 @@ public class StoreSectionComp extends Comp> { } private Comp> createQuickAccessButton() { - var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding( + var quickAccessDisabled = Bindings.createBooleanBinding( () -> { return section.getShownChildren().isEmpty(); }, - section.getShownChildren())); + section.getShownChildren()); Consumer quickAccessAction = w -> { ThreadHelper.runFailableAsync(() -> { w.executeDefaultAction(); @@ -91,8 +90,7 @@ public class StoreSectionComp extends Comp> { return "Expand " + section.getWrapper().getName().getValue(); }, section.getWrapper().getName())) - .disable(BindingsHelper.persist( - Bindings.size(section.getShownChildren()).isEqualTo(0))) + .disable(Bindings.size(section.getShownChildren()).isEqualTo(0)) .styleClass("expand-button") .maxHeight(100) .vgrow(); @@ -131,7 +129,7 @@ public class StoreSectionComp extends Comp> { // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // section is actually expanded - var listSections = BindingsHelper.filteredContentBinding( + var listSections = ListBindingsHelper.filteredContentBinding( section.getShownChildren(), storeSection -> section.getAllChildren().size() <= 20 || section.getWrapper().getExpanded().get(), @@ -143,26 +141,26 @@ public class StoreSectionComp extends Comp> { .minHeight(0) .hgrow(); - var expanded = BindingsHelper.persist(Bindings.createBooleanBinding( + var expanded = Bindings.createBooleanBinding( () -> { return section.getWrapper().getExpanded().get() && section.getShownChildren().size() > 0; }, section.getWrapper().getExpanded(), - section.getShownChildren())); + section.getShownChildren()); var full = new VerticalComp(List.of( topEntryList, - Comp.separator().hide(BindingsHelper.persist(expanded.not())), + Comp.separator().hide(expanded.not()), new HorizontalComp(List.of(content)) .styleClass("content") .apply(struc -> struc.get().setFillHeight(true)) - .hide(BindingsHelper.persist(Bindings.or( + .hide(Bindings.or( Bindings.not(section.getWrapper().getExpanded()), - Bindings.size(section.getShownChildren()).isEqualTo(0)))))); + Bindings.size(section.getShownChildren()).isEqualTo(0))))); return full.styleClass("store-entry-section-comp") .apply(struc -> { struc.get().setFillWidth(true); - SimpleChangeListener.apply(expanded, val -> { + expanded.subscribe(val -> { struc.get().pseudoClassStateChanged(EXPANDED, val); }); struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); @@ -170,7 +168,7 @@ public class StoreSectionComp extends Comp> { struc.get().pseudoClassStateChanged(ROOT, topLevel); struc.get().pseudoClassStateChanged(SUB, !topLevel); - SimpleChangeListener.apply(section.getWrapper().getColor(), val -> { + section.getWrapper().getColor().subscribe(val -> { if (!topLevel) { return; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java index f8f756fae..dc22b86f8 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java @@ -8,8 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStoreColor; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; @@ -101,16 +100,15 @@ public class StoreSectionMiniComp extends Comp> { + section.getWrapper().getName().getValue(); }, section.getWrapper().getName())) - .disable(BindingsHelper.persist( - Bindings.size(section.getAllChildren()).isEqualTo(0))) + .disable(Bindings.size(section.getAllChildren()).isEqualTo(0)) .grow(false, true) .styleClass("expand-button"); - var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding( + var quickAccessDisabled = Bindings.createBooleanBinding( () -> { return section.getShownChildren().isEmpty(); }, - section.getShownChildren())); + section.getShownChildren()); Consumer quickAccessAction = w -> { action.accept(w); }; @@ -134,7 +132,7 @@ public class StoreSectionMiniComp extends Comp> { // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // section is actually expanded var listSections = section.getWrapper() != null - ? BindingsHelper.filteredContentBinding( + ? ListBindingsHelper.filteredContentBinding( section.getShownChildren(), storeSection -> section.getAllChildren().size() <= 20 || expanded.get(), expanded, @@ -149,9 +147,9 @@ public class StoreSectionMiniComp extends Comp> { list.add(new HorizontalComp(List.of(content)) .styleClass("content") .apply(struc -> struc.get().setFillHeight(true)) - .hide(BindingsHelper.persist(Bindings.or( + .hide(Bindings.or( Bindings.not(expanded), - Bindings.size(section.getAllChildren()).isEqualTo(0))))); + Bindings.size(section.getAllChildren()).isEqualTo(0)))); var vert = new VerticalComp(list); if (condensedStyle) { @@ -160,7 +158,7 @@ public class StoreSectionMiniComp extends Comp> { return vert.styleClass("store-section-mini-comp") .apply(struc -> { struc.get().setFillWidth(true); - SimpleChangeListener.apply(expanded, val -> { + expanded.subscribe(val -> { struc.get().pseudoClassStateChanged(EXPANDED, val); }); struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); @@ -171,7 +169,7 @@ public class StoreSectionMiniComp extends Comp> { }) .apply(struc -> { if (section.getWrapper() != null) { - SimpleChangeListener.apply(section.getWrapper().getColor(), val -> { + section.getWrapper().getColor().subscribe(val -> { if (section.getDepth() != 1) { return; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java index 8b6d019b0..f08b2e4ad 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java @@ -1,7 +1,7 @@ package io.xpipe.app.comp.store; import io.xpipe.app.core.AppCache; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; @@ -270,10 +270,10 @@ public class StoreViewState { return parent; } - return o1.getName().compareToIgnoreCase(o2.getName()); + return o1.nameProperty().getValue().compareToIgnoreCase(o2.nameProperty().getValue()); } }; - return BindingsHelper.filteredContentBinding( + return ListBindingsHelper.filteredContentBinding( categories, cat -> root == null || cat.getRoot().equals(root)) .sorted(comparator); } diff --git a/app/src/main/java/io/xpipe/app/core/AppGreetings.java b/app/src/main/java/io/xpipe/app/core/AppGreetings.java index 08be1218e..9f2f92486 100644 --- a/app/src/main/java/io/xpipe/app/core/AppGreetings.java +++ b/app/src/main/java/io/xpipe/app/core/AppGreetings.java @@ -3,7 +3,6 @@ package io.xpipe.app.core; import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.util.BindingsHelper; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -98,7 +97,7 @@ public class AppGreetings { alert.getButtonTypes().add(buttonType); Button button = (Button) alert.getDialogPane().lookupButton(buttonType); - button.disableProperty().bind(BindingsHelper.persist(accepted.not())); + button.disableProperty().bind(accepted.not()); } alert.getButtonTypes().add(ButtonType.CANCEL); diff --git a/app/src/main/java/io/xpipe/app/core/AppI18n.java b/app/src/main/java/io/xpipe/app/core/AppI18n.java index 327042b30..7b57fd37b 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -2,7 +2,7 @@ package io.xpipe.app.core; import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.ext.PrefsChoiceValue; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; @@ -10,101 +10,70 @@ import io.xpipe.app.prefs.SupportedLocale; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.Translatable; import io.xpipe.core.util.ModuleHelper; +import io.xpipe.core.util.XPipeInstallation; import javafx.beans.binding.Bindings; -import javafx.beans.binding.StringBinding; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import lombok.SneakyThrows; +import lombok.Value; import org.apache.commons.io.FilenameUtils; import org.ocpsoft.prettytime.PrettyTime; import java.io.IOException; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.time.Duration; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.UnaryOperator; import java.util.regex.Pattern; public class AppI18n { - private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); - private static final AppI18n INSTANCE = new AppI18n(); - private Map translations; - private Map markdownDocumentations; - private PrettyTime prettyTime; + @Value + static class LoadedTranslations { - public static void init() { - var i = INSTANCE; - if (i.translations != null) { - return; + Map translations; + Map markdownDocumentations; + PrettyTime prettyTime; + } + + private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); + private static AppI18n INSTANCE; + private LoadedTranslations english; + private final Property currentLanguage = new SimpleObjectProperty<>(); + + public static void init() throws Exception { + INSTANCE = new AppI18n(); + INSTANCE.load(); + } + + private void load() throws Exception { + if (english == null) { + english = load(Locale.ENGLISH); } - i.load(); - if (AppPrefs.get() != null) { - AppPrefs.get().language().addListener((c, o, n) -> { - i.clear(); - i.load(); + AppPrefs.get().language().subscribe(n -> { + try { + currentLanguage.setValue(n != null ? load(n.getLocale()) : null); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).handle(); + } }); } } - public static AppI18n getInstance() { + public static AppI18n get() { return INSTANCE; } - public static StringBinding readableInstant(String s, ObservableValue instant) { - return readableInstant(instant, rs -> getValue(getInstance().getLocalised(s), rs)); - } - - public static StringBinding readableInstant(ObservableValue instant, UnaryOperator op) { - return Bindings.createStringBinding( - () -> { - if (instant.getValue() == null) { - return "null"; - } - - return op.apply( - getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1)))); - }, - instant); - } - - public static StringBinding readableInstant(ObservableValue instant) { - return Bindings.createStringBinding( - () -> { - if (instant.getValue() == null) { - return "null"; - } - - return getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1))); - }, - instant); - } - - public static StringBinding readableDuration(ObservableValue duration) { - return Bindings.createStringBinding( - () -> { - if (duration.getValue() == null) { - return "null"; - } - - return getInstance() - .prettyTime - .formatDuration(getInstance() - .prettyTime - .approximateDuration(Instant.now().plus(duration.getValue()))); - }, - duration); + private LoadedTranslations getLoaded() { + return currentLanguage.getValue() != null ? currentLanguage.getValue() : english; } public static ObservableValue observable(String s, Object... vars) { @@ -115,7 +84,7 @@ public class AppI18n { var key = INSTANCE.getKey(s); return Bindings.createStringBinding(() -> { return get(key, vars); - }); + }, INSTANCE.currentLanguage); } public static String get(String s, Object... vars) { @@ -147,7 +116,7 @@ public class AppI18n { || caller.equals(ModuleHelper.class) || caller.equals(ModalOverlayComp.class) || caller.equals(AppI18n.class) - || caller.equals(FancyTooltipAugment.class) + || caller.equals(TooltipAugment.class) || caller.equals(PrefsChoiceValue.class) || caller.equals(Translatable.class) || caller.equals(AppWindowHelper.class) @@ -160,11 +129,6 @@ public class AppI18n { return ""; } - private void clear() { - translations.clear(); - prettyTime = null; - } - public String getKey(String s) { var key = s; if (!s.contains(".")) { @@ -173,62 +137,62 @@ public class AppI18n { return key; } - public boolean containsKey(String s) { - var key = getKey(s); - if (translations == null) { - return false; - } - - return translations.containsKey(key); - } - public String getLocalised(String s, Object... vars) { var key = getKey(s); - if (translations == null) { + if (english == null) { TrackEvent.warn("Translations not initialized for " + key); return s; } - if (!translations.containsKey(key)) { - TrackEvent.warn("Translation key not found for " + key); - return key; + if (currentLanguage.getValue() != null && currentLanguage.getValue().getTranslations().containsKey(key)) { + var localisedString = currentLanguage.getValue().getTranslations().get(key); + return getValue(localisedString, vars); } - var localisedString = translations.get(key); - return getValue(localisedString, vars); + if (english.getTranslations().containsKey(key)) { + var localisedString = english.getTranslations().get(key); + return getValue(localisedString, vars); + } + + TrackEvent.warn("Translation key not found for " + key); + return key; } - public boolean isLoaded() { - return translations != null; - } - - private boolean matchesLocale(Path f) { - var l = AppPrefs.get() != null - ? AppPrefs.get().language().getValue().getLocale() - : SupportedLocale.ENGLISH.getLocale(); + private boolean matchesLocale(Path f, Locale l) { var name = FilenameUtils.getBaseName(f.getFileName().toString()); var ending = "_" + l.toLanguageTag(); return name.endsWith(ending); } public String getMarkdownDocumentation(String name) { - if (!markdownDocumentations.containsKey(name)) { - TrackEvent.withWarn("Markdown documentation for key " + name + " not found") - .handle(); + if (currentLanguage.getValue() != null && currentLanguage.getValue().getMarkdownDocumentations().containsKey(name)) { + var localisedString = currentLanguage.getValue().getMarkdownDocumentations().get(name); + return localisedString; } - return markdownDocumentations.getOrDefault(name, ""); + if (english.getMarkdownDocumentations().containsKey(name)) { + var localisedString = english.getMarkdownDocumentations().get(name); + return localisedString; + } + + TrackEvent.withWarn("Markdown documentation for key " + name + " not found") + .handle(); + return ""; } - private void load() { + private Path getModuleLangPath(String module) { + return XPipeInstallation.getLangPath().resolve(module); + } + + private LoadedTranslations load(Locale l) throws Exception { TrackEvent.info("Loading translations ..."); - translations = new HashMap<>(); + var translations = new HashMap(); for (var module : AppExtensionManager.getInstance().getContentModules()) { - AppResources.with(module.getName(), "lang", basePath -> { + var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName())).resolve("strings"); if (!Files.exists(basePath)) { - return; + continue; } AtomicInteger fileCounter = new AtomicInteger(); @@ -238,7 +202,7 @@ public class AppI18n { Files.walkFileTree(basePath, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (!matchesLocale(file)) { + if (!matchesLocale(file, l)) { return FileVisitResult.CONTINUE; } @@ -249,7 +213,7 @@ public class AppI18n { fileCounter.incrementAndGet(); try (var in = Files.newInputStream(file)) { var props = new Properties(); - props.load(in); + props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); props.forEach((key, value) -> { var hasPrefix = key.toString().contains("."); var usedPrefix = hasPrefix ? "" : defaultPrefix; @@ -267,21 +231,20 @@ public class AppI18n { .tag("fileCount", fileCounter.get()) .tag("lineCount", lineCounter.get()) .handle(); - }); } - markdownDocumentations = new HashMap<>(); + var markdownDocumentations = new HashMap(); for (var module : AppExtensionManager.getInstance().getContentModules()) { - AppResources.with(module.getName(), "lang", basePath -> { + var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName())).resolve("texts"); if (!Files.exists(basePath)) { - return; + continue; } var moduleName = FilenameUtils.getExtension(module.getName()); Files.walkFileTree(basePath, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (!matchesLocale(file)) { + if (!matchesLocale(file, l)) { return FileVisitResult.CONTINUE; } @@ -302,13 +265,14 @@ public class AppI18n { return FileVisitResult.CONTINUE; } }); - }); } - this.prettyTime = new PrettyTime( + var prettyTime = new PrettyTime( AppPrefs.get() != null ? AppPrefs.get().language().getValue().getLocale() : SupportedLocale.ENGLISH.getLocale()); + + return new LoadedTranslations(translations,markdownDocumentations, prettyTime); } @SuppressWarnings("removal") diff --git a/app/src/main/java/io/xpipe/app/core/AppImages.java b/app/src/main/java/io/xpipe/app/core/AppImages.java index 07fcdc688..7a4dc4c6d 100644 --- a/app/src/main/java/io/xpipe/app/core/AppImages.java +++ b/app/src/main/java/io/xpipe/app/core/AppImages.java @@ -30,11 +30,11 @@ public class AppImages { TrackEvent.info("Loading images ..."); for (var module : AppExtensionManager.getInstance().getContentModules()) { - loadDirectory(module.getName(), "img"); + loadDirectory(module.getName(), "img", true, true); } } - public static void loadDirectory(String module, String dir) { + public static void loadDirectory(String module, String dir, boolean loadImages, boolean loadSvgs) { AppResources.with(module, dir, basePath -> { if (!Files.exists(basePath)) { return; @@ -48,10 +48,10 @@ public class AppImages { var relativeFileName = FilenameUtils.separatorsToUnix( basePath.relativize(file).toString()); try { - if (FilenameUtils.getExtension(file.toString()).equals("svg")) { + if (FilenameUtils.getExtension(file.toString()).equals("svg") && loadSvgs) { var s = Files.readString(file); svgImages.put(defaultPrefix + relativeFileName, s); - } else { + } else if (loadImages) { images.put(defaultPrefix + relativeFileName, loadImage(file)); } } catch (IOException ex) { diff --git a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java index 8860ef334..b15f8dda2 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java +++ b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java @@ -2,10 +2,8 @@ package io.xpipe.app.core; import io.xpipe.app.browser.BrowserComp; import io.xpipe.app.browser.BrowserModel; -import io.xpipe.app.comp.DeveloperTabComp; import io.xpipe.app.comp.store.StoreLayoutComp; import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.prefs.AppPrefsComp; import io.xpipe.app.util.LicenseProvider; import javafx.beans.property.Property; @@ -30,13 +28,11 @@ public class AppLayoutModel { private final List entries; private final Property selected; - private final ObservableValue selectedWrapper; public AppLayoutModel(SavedState savedState) { this.savedState = savedState; this.entries = createEntryList(); this.selected = new SimpleObjectProperty<>(entries.get(1)); - this.selectedWrapper = PlatformThread.sync(selected); } public static AppLayoutModel get() { @@ -53,14 +49,10 @@ public class AppLayoutModel { INSTANCE = null; } - public Property getSelectedInternal() { + public Property getSelected() { return selected; } - public ObservableValue getSelected() { - return selectedWrapper; - } - public void selectBrowser() { selected.setValue(entries.getFirst()); } @@ -81,19 +73,11 @@ public class AppLayoutModel { var l = new ArrayList<>(List.of( new Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)), new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()), - new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()))); - // new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new - // StorageLayoutComp()), - // new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()) - if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) { - l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp())); - } - - l.add(new Entry( - AppI18n.observable("explorePlans"), - "mdi2p-professional-hexagon", - LicenseProvider.get().overviewPage())); - + new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()), + new Entry( + AppI18n.observable("explorePlans"), + "mdi2p-professional-hexagon", + LicenseProvider.get().overviewPage()))); return l; } diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java index ff8625600..a0f4570bc 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -3,7 +3,6 @@ package io.xpipe.app.core; import atlantafx.base.theme.*; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; @@ -44,7 +43,7 @@ public class AppTheme { return; } - SimpleChangeListener.apply(AppPrefs.get().theme, t -> { + AppPrefs.get().theme.subscribe(t -> { Theme.ALL.forEach( theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId())); if (t == null) { @@ -56,7 +55,7 @@ public class AppTheme { stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark()); }); - SimpleChangeListener.apply(AppPrefs.get().performanceMode(), val -> { + AppPrefs.get().performanceMode().subscribe(val -> { stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val); stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val); }); diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index 450af36e5..4fb42aa71 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -47,6 +47,7 @@ public class BaseMode extends OperationMode { AppI18n.init(); LicenseProvider.get().init(); AppPrefs.initLocal(); + AppI18n.init(); AppCertutilCheck.check(); AppAvCheck.check(); AppSid.init(); diff --git a/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java index a61c3311c..3ecbce070 100644 --- a/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java @@ -2,7 +2,6 @@ package io.xpipe.app.exchange; import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.exchange.LaunchExchange; -import io.xpipe.core.process.TerminalInitScriptConfig; import io.xpipe.core.store.LaunchableStore; import java.util.Arrays; @@ -16,9 +15,9 @@ public class LaunchExchangeImpl extends LaunchExchange public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { var store = getStoreEntryById(msg.getId(), false); if (store.getStore() instanceof LaunchableStore s) { - var command = s.prepareLaunchCommand() - .prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null); - return Response.builder().command(split(command)).build(); +// var command = s.prepareLaunchCommand() +// .prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null); +// return Response.builder().command(split(command)).build(); } throw new IllegalArgumentException(store.getName() + " is not launchable"); diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/InstanceExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/InstanceExchangeImpl.java deleted file mode 100644 index 972559515..000000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/InstanceExchangeImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.update.XPipeInstanceHelper; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.InstanceExchange; -import io.xpipe.core.store.LocalStore; - -public class InstanceExchangeImpl extends InstanceExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - return Response.builder() - .instance(XPipeInstanceHelper.getInstance(new LocalStore()).orElseThrow()) - .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 23d94a170..e287dd887 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -201,6 +201,7 @@ public interface DataStoreProvider { COMMAND, TUNNEL, SCRIPT, - CLUSTER + CLUSTER, + VISUAL; } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/Comp.java b/app/src/main/java/io/xpipe/app/fxcomps/Comp.java index 1b8d6483c..dce02a41a 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/Comp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/Comp.java @@ -4,9 +4,9 @@ import atlantafx.base.controls.Spacer; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.GrowAugment; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; +import io.xpipe.app.fxcomps.impl.TooltipAugment; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.Shortcuts; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; @@ -144,7 +144,8 @@ public abstract class Comp> { public Comp hide(ObservableValue o) { return apply(struc -> { var region = struc.get(); - SimpleChangeListener.apply(o, n -> { + BindingsHelper.preserve(region, o); + o.subscribe(n -> { if (!n) { region.setVisible(true); region.setManaged(true); @@ -189,11 +190,11 @@ public abstract class Comp> { } public Comp tooltip(ObservableValue text) { - return apply(new FancyTooltipAugment<>(text)); + return apply(new TooltipAugment<>(text)); } public Comp tooltipKey(String key) { - return apply(new FancyTooltipAugment<>(key)); + return apply(new TooltipAugment<>(key)); } public Region createRegion() { @@ -202,6 +203,8 @@ public abstract class Comp> { public S createStructure() { S struc = createBase(); + // Make comp last at least as long as region + BindingsHelper.preserve(struc.get(), this); if (augments != null) { for (var a : augments) { a.augment(struc); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java index 779c5732f..2a56182ce 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java @@ -4,9 +4,8 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.util.Translatable; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; @@ -72,19 +71,19 @@ public class ChoiceComp extends Comp>> { throw new UnsupportedOperationException(); } }); - SimpleChangeListener.apply(range, c -> { + range.subscribe(c -> { var list = FXCollections.observableArrayList(c.keySet()); if (!list.contains(null) && includeNone) { list.add(null); } - BindingsHelper.setContent(cb.getItems(), list); + ListBindingsHelper.setContent(cb.getItems(), list); }); cb.valueProperty().addListener((observable, oldValue, newValue) -> { value.setValue(newValue); }); - SimpleChangeListener.apply(value, val -> { + value.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val)); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java index 2bc5f1d51..48ad40924 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -58,7 +57,7 @@ public class ChoicePaneComp extends Comp> { var vbox = new VBox(transformer.apply(cb)); vbox.setFillWidth(true); cb.prefWidthProperty().bind(vbox.widthProperty()); - SimpleChangeListener.apply(cb.valueProperty(), n -> { + cb.valueProperty().subscribe(n -> { if (n == null) { if (vbox.getChildren().size() > 1) { vbox.getChildren().remove(1); @@ -82,7 +81,7 @@ public class ChoicePaneComp extends Comp> { cb.valueProperty().addListener((observable, oldValue, newValue) -> { selected.setValue(newValue); }); - SimpleChangeListener.apply(selected, val -> { + selected.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val)); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java index 8652a6660..02d93a4b8 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java @@ -4,10 +4,9 @@ import atlantafx.base.theme.Styles; import io.xpipe.app.browser.StandaloneFileBrowser; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.ContextualFileReference; @@ -39,7 +38,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { public ContextualFileReferenceChoiceComp( ObservableValue> fileSystem, Property filePath) { this.fileSystem = new SimpleObjectProperty<>(); - SimpleChangeListener.apply(fileSystem, val -> { + fileSystem.subscribe(val -> { this.fileSystem.setValue(val); }); this.filePath = filePath; @@ -66,7 +65,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { .styleClass(Styles.CENTER_PILL) .grow(false, true); - var canGitShare = BindingsHelper.persist(Bindings.createBooleanBinding( + var canGitShare = Bindings.createBooleanBinding( () -> { if (!AppPrefs.get().enableGitStorage().get() || filePath.getValue() == null @@ -77,8 +76,18 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { return true; }, filePath, - AppPrefs.get().enableGitStorage())); + AppPrefs.get().enableGitStorage()); var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> { + if (!AppPrefs.get().enableGitStorage().get()) { + AppLayoutModel.get().selectSettings(); + AppPrefs.get().selectCategory(3); + return; + } + + if (filePath.getValue() == null || ContextualFileReference.of(filePath.getValue()).isInDataDirectory()) { + return; + } + if (filePath.getValue() == null || filePath.getValue().isBlank() || !canGitShare.get()) { return; } @@ -108,7 +117,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { ErrorEvent.fromThrowable(e).handle(); } }); - gitShareButton.apply(new FancyTooltipAugment<>("gitShareFileTooltip")); + gitShareButton.apply(new TooltipAugment<>("gitShareFileTooltip")); gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true); var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton)) diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreFlowChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreFlowChoiceComp.java index 6568a563c..8f98b608d 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreFlowChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreFlowChoiceComp.java @@ -27,11 +27,11 @@ public class DataStoreFlowChoiceComp extends SimpleComp { map.put(DataFlow.INPUT_OUTPUT, AppI18n.observable("app.inout")); return new ToggleGroupComp<>(selected, new SimpleObjectProperty<>(map)) .apply(struc -> { - new FancyTooltipAugment<>("app.inputDescription") + new TooltipAugment<>("app.inputDescription") .augment(struc.get().getChildren().get(0)); - new FancyTooltipAugment<>("app.outputDescription") + new TooltipAugment<>("app.outputDescription") .augment(struc.get().getChildren().get(1)); - new FancyTooltipAugment<>("app.inoutDescription") + new TooltipAugment<>("app.inoutDescription") .augment(struc.get().getChildren().get(2)); }) .createRegion(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java index 205708977..f148fc6b9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java @@ -1,10 +1,10 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.core.AppActionLinkDetector; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.scene.Node; @@ -28,12 +28,13 @@ public class FilterComp extends Comp { @Override public Structure createBase() { var fi = new FontIcon("mdi2m-magnify"); - var bgLabel = new Label("Search", fi); + var bgLabel = new Label(null, fi); + bgLabel.textProperty().bind(AppI18n.observable("searchFilter")); bgLabel.getStyleClass().add("filter-background"); var filter = new TextField(); filter.setAccessibleText("Filter"); - SimpleChangeListener.apply(filterText, val -> { + filterText.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> { if (!Objects.equals(filter.getText(), val)) { filter.setText(val); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java index da1a559ad..3556f7712 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java @@ -3,6 +3,7 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; 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 83433c5f2..cb4794b69 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 @@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.core.AppImages; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.core.store.FileNames; @@ -106,7 +105,7 @@ public class PrettyImageComp extends SimpleComp { } }; - SimpleChangeListener.apply(PlatformThread.sync(value), val -> update.accept(val)); + PlatformThread.sync(value).subscribe(val -> update.accept(val)); AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> { update.accept(value.getValue()); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java index 2519304c8..461baa10d 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.core.AppImages; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.core.store.FileNames; import javafx.beans.binding.Bindings; @@ -92,7 +91,7 @@ public class PrettySvgComp extends SimpleComp { image.set(fixed); }; - SimpleChangeListener.apply(syncValue, val -> update.accept(val)); + syncValue.subscribe(val -> update.accept(val)); AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> { update.accept(syncValue.getValue()); }); 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 c4eaf095a..6620ec31c 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 @@ -11,8 +11,7 @@ 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.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.util.ContextMenuHelper; @@ -77,7 +76,7 @@ public class StoreCategoryComp extends SimpleComp { showing.bind(cm.showingProperty()); return cm; })); - var shownList = BindingsHelper.filteredContentBinding( + var shownList = ListBindingsHelper.filteredContentBinding( category.getContainedEntries(), storeEntryWrapper -> { return storeEntryWrapper.shouldShow( @@ -92,9 +91,8 @@ public class StoreCategoryComp extends SimpleComp { Comp.hspacer(4), Comp.of(() -> name), Comp.hspacer(), - count.hide(BindingsHelper.persist(hover.or(showing).or(focus))), - settings.hide( - BindingsHelper.persist(hover.not().and(showing.not()).and(focus.not()))))); + count.hide(hover.or(showing).or(focus)), + settings.hide(hover.not().and(showing.not()).and(focus.not())))); h.padding(new Insets(0, 10, 0, (category.getDepth() * 10))); var categoryButton = new ButtonComp(null, h.createRegion(), category::select) @@ -108,14 +106,14 @@ public class StoreCategoryComp extends SimpleComp { var l = category.getChildren() .sorted(Comparator.comparing( - storeCategoryWrapper -> storeCategoryWrapper.getName().toLowerCase(Locale.ROOT))); + storeCategoryWrapper -> storeCategoryWrapper.nameProperty().getValue().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(categoryButton, children.hide(emptyBinding))); v.styleClass("category"); v.apply(struc -> { - SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { + StoreViewState.get().getActiveCategory().subscribe(val -> { struc.get().pseudoClassStateChanged(SELECTED, val.equals(category)); }); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java index f5960527d..8e745e31d 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java @@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.core.AppProperties; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.value.ObservableValue; @@ -37,7 +36,7 @@ public class SvgView { public static SvgView create(ObservableValue content) { var widthProperty = new SimpleIntegerProperty(); var heightProperty = new SimpleIntegerProperty(); - SimpleChangeListener.apply(content, val -> { + content.subscribe(val -> { if (val == null || val.isBlank()) { return; } @@ -69,7 +68,7 @@ public class SvgView { wv.setDisable(true); wv.getEngine().loadContent(svgContent.getValue() != null ? getHtml(svgContent.getValue()) : null); - SimpleChangeListener.apply(svgContent, n -> { + svgContent.subscribe( n -> { if (n == null) { wv.setOpacity(0.0); return; diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java index 9f0ffe0c1..f31825dae 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; @@ -28,7 +27,7 @@ public class TextAreaComp extends Comp { this.lastAppliedValue = value; this.currentValue = new SimpleStringProperty(value.getValue()); this.lazy = lazy; - SimpleChangeListener.apply(value, val -> { + value.subscribe(val -> { this.currentValue.setValue(val); }); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TextFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TextFieldComp.java index 4ce92c33c..66183eef2 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TextFieldComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; import javafx.scene.control.TextField; @@ -27,7 +26,7 @@ public class TextFieldComp extends Comp> { this.currentValue = new SimpleStringProperty(value.getValue()); this.lazy = lazy; if (!lazy) { - SimpleChangeListener.apply(currentValue, val -> { + currentValue.subscribe(val -> { value.setValue(val); }); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java index 55cd6f56f..9f590e33c 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.scene.control.ToggleButton; @@ -29,7 +28,7 @@ public class ToggleGroupComp extends Comp> { var box = new HBox(); box.getStyleClass().add("toggle-group-comp"); ToggleGroup group = new ToggleGroup(); - SimpleChangeListener.apply(PlatformThread.sync(range), val -> { + PlatformThread.sync(range).subscribe(val -> { if (!val.containsKey(value.getValue())) { this.value.setValue(null); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java similarity index 59% rename from app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java rename to app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java index 7af8b6d0d..4fcd32a75 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java @@ -5,18 +5,19 @@ import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.Shortcuts; +import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; import javafx.scene.control.Tooltip; -public class FancyTooltipAugment> implements Augment { +public class TooltipAugment> implements Augment { private final ObservableValue text; - public FancyTooltipAugment(ObservableValue text) { + public TooltipAugment(ObservableValue text) { this.text = PlatformThread.sync(text); } - public FancyTooltipAugment(String key) { + public TooltipAugment(String key) { this.text = AppI18n.observable(key); } @@ -24,11 +25,15 @@ public class FancyTooltipAugment> implements Augment< public void augment(S struc) { var region = struc.get(); var tt = new Tooltip(); - var toDisplay = text.getValue(); if (Shortcuts.getDisplayShortcut(region) != null) { - toDisplay = toDisplay + "\n\nShortcut: " + Shortcuts.getDisplayShortcut(region).getDisplayText(); + var s = AppI18n.observable("shortcut"); + var binding = Bindings.createStringBinding(() -> { + return text.getValue() + "\n\n" + s.getValue() + ": " + Shortcuts.getDisplayShortcut(region).getDisplayText(); + }, text, s); + tt.textProperty().bind(binding); + } else { + tt.textProperty().bind(text); } - tt.textProperty().setValue(toDisplay); tt.setStyle("-fx-font-size: 11pt;"); tt.setWrapText(true); tt.setMaxWidth(400); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java b/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java index b557a61d4..169f4ee65 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java @@ -1,98 +1,50 @@ package io.xpipe.app.fxcomps.util; import io.xpipe.app.util.ThreadHelper; -import javafx.beans.Observable; -import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; -import javafx.beans.binding.ListBinding; -import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; import lombok.Value; import java.lang.ref.WeakReference; -import java.util.*; +import java.util.Collections; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import java.util.function.Predicate; @SuppressWarnings("InfiniteLoopStatement") public class BindingsHelper { private static final Set REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>()); - /* - TODO: Proper cleanup. Maybe with a separate thread? - */ - private static final Map, Set> BINDINGS = new ConcurrentHashMap<>(); static { ThreadHelper.createPlatformThread("referenceGC", true, () -> { while (true) { for (ReferenceEntry reference : REFERENCES) { if (reference.canGc()) { - /* - TODO: Figure out why some bindings are garbage collected, even if they shouldn't - */ - // REFERENCES.remove(reference); + REFERENCES.remove(reference); } } ThreadHelper.sleep(1000); + + // Use for testing + // System.gc(); } }) .start(); } - public static void bindExclusive( - Property selected, Map> map, Property toBind) { - selected.addListener((c, o, n) -> { - toBind.unbind(); - toBind.bind(map.get(n)); - }); - - toBind.bind(map.get(selected.getValue())); - } - - public static void linkPersistently(Object source, Object target) { + public static void preserve(Object source, Object target) { REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target)); } - public static > T persist(T binding) { - var dependencies = new HashSet(); - while (dependencies.addAll(binding.getDependencies().stream() - .map(o -> (javafx.beans.Observable) o) - .toList())) {} - dependencies.add(binding); - BINDINGS.put(new WeakReference<>(binding), dependencies); - return binding; - } - - public static > T persist(T binding) { - var dependencies = new HashSet(); - while (dependencies.addAll(binding.getDependencies().stream() - .map(o -> (javafx.beans.Observable) o) - .toList())) {} - dependencies.add(binding); - BINDINGS.put(new WeakReference<>(binding), dependencies); - return binding; - } - - public static void bindContent(ObservableList l1, ObservableList l2) { - setContent(l1, l2); - l2.addListener((ListChangeListener) c -> { - setContent(l1, l2); - }); - } - public static ObservableValue map( ObservableValue observableValue, Function mapper) { - return persist(Bindings.createObjectBinding( + return Bindings.createObjectBinding( () -> { return mapper.apply(observableValue.getValue()); }, - observableValue)); + observableValue); } public static ObservableValue flatMap( @@ -105,229 +57,10 @@ public class BindingsHelper { observableValue.addListener((observable, oldValue, newValue) -> { runnable.run(); }); - linkPersistently(observableValue, prop); + preserve(prop, observableValue); return prop; } - public static ObservableValue anyMatch(List> l) { - return BindingsHelper.persist(Bindings.createBooleanBinding( - () -> { - return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); - }, - l.toArray(ObservableValue[]::new))); - } - - public static void bindMappedContent(ObservableList l1, ObservableList l2, Function map) { - Runnable runnable = () -> { - setContent(l1, l2.stream().map(map).toList()); - }; - runnable.run(); - l2.addListener((ListChangeListener) c -> { - runnable.run(); - }); - } - - public static ObservableList mappedContentBinding(ObservableList l2, Function map) { - ObservableList l1 = FXCollections.observableList(new ArrayList<>()); - Runnable runnable = () -> { - setContent(l1, l2.stream().map(map).toList()); - }; - runnable.run(); - l2.addListener((ListChangeListener) c -> { - runnable.run(); - }); - linkPersistently(l2, l1); - return l1; - } - - public static ObservableList cachedMappedContentBinding(ObservableList l2, Function map) { - var cache = new HashMap(); - - ObservableList l1 = FXCollections.observableList(new ArrayList<>()); - Runnable runnable = () -> { - cache.keySet().removeIf(t -> !l2.contains(t)); - setContent( - l1, - l2.stream() - .map(v -> { - if (!cache.containsKey(v)) { - cache.put(v, map.apply(v)); - } - - return cache.get(v); - }) - .toList()); - }; - runnable.run(); - l2.addListener((ListChangeListener) c -> { - runnable.run(); - }); - linkPersistently(l2, l1); - return l1; - } - - public static ObservableList cachedMappedContentBinding( - ObservableList all, ObservableList shown, Function map) { - var cache = new HashMap(); - - ObservableList l1 = FXCollections.observableList(new ArrayList<>()); - Runnable runnable = () -> { - cache.keySet().removeIf(t -> !all.contains(t)); - setContent( - l1, - shown.stream() - .map(v -> { - if (!cache.containsKey(v)) { - cache.put(v, map.apply(v)); - } - - return cache.get(v); - }) - .toList()); - }; - runnable.run(); - shown.addListener((ListChangeListener) c -> { - runnable.run(); - }); - linkPersistently(all, l1); - linkPersistently(shown, l1); - return l1; - } - - public static ObservableValue mappedBinding( - ObservableValue observableValue, Function> mapper) { - var binding = (Binding) observableValue.flatMap(mapper); - return persist(binding); - } - - public static ObservableList orderedContentBinding( - ObservableList l2, Comparator comp, Observable... observables) { - return orderedContentBinding( - l2, - Bindings.createObjectBinding( - () -> { - return new Comparator<>() { - @Override - public int compare(V o1, V o2) { - return comp.compare(o1, o2); - } - }; - }, - observables)); - } - - // public static ObservableValue mappedBinding(ObservableValue observableValue, Function> mapper) { - // var v = new SimpleObjectProperty(); - // SimpleChangeListener.apply(observableValue, val -> { - // v.unbind(); - // v.bind(mapper.apply(val)); - // }); - // return v; - // } - - public static ObservableList orderedContentBinding( - ObservableList l2, ObservableValue> comp) { - ObservableList l1 = FXCollections.observableList(new ArrayList<>()); - Runnable runnable = () -> { - setContent(l1, l2.stream().sorted(comp.getValue()).toList()); - }; - runnable.run(); - l2.addListener((ListChangeListener) c -> { - runnable.run(); - }); - comp.addListener((observable, oldValue, newValue) -> { - runnable.run(); - }); - linkPersistently(l2, l1); - return l1; - } - - public static ObservableList filteredContentBinding(ObservableList l2, Predicate predicate) { - return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate)); - } - - public static ObservableList filteredContentBinding( - ObservableList l2, Predicate predicate, Observable... observables) { - return filteredContentBinding( - l2, - Bindings.createObjectBinding( - () -> { - return new Predicate<>() { - @Override - public boolean test(V v) { - return predicate.test(v); - } - }; - }, - Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new))); - } - - public static ObservableList filteredContentBinding( - ObservableList l2, ObservableValue> predicate) { - ObservableList l1 = FXCollections.observableList(new ArrayList<>()); - Runnable runnable = () -> { - setContent( - l1, - predicate.getValue() != null - ? l2.stream().filter(predicate.getValue()).toList() - : l2); - }; - runnable.run(); - l2.addListener((ListChangeListener) c -> { - runnable.run(); - }); - predicate.addListener((c, o, n) -> { - runnable.run(); - }); - linkPersistently(l2, l1); - return l1; - } - - public static void setContent(ObservableList target, List newList) { - if (target.equals(newList)) { - return; - } - - if (target.size() == 0) { - target.setAll(newList); - return; - } - - if (newList.size() == 0) { - target.clear(); - return; - } - - var targetSet = new HashSet<>(target); - var newSet = new HashSet<>(newList); - - // Only add missing element - if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) { - var l = new HashSet<>(newSet); - l.removeAll(targetSet); - if (l.size() > 0) { - var found = l.iterator().next(); - var index = newList.indexOf(found); - target.add(index, found); - return; - } - } - - // Only remove not needed element - if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) { - var l = new HashSet<>(targetSet); - l.removeAll(newSet); - if (l.size() > 0) { - target.remove(l.iterator().next()); - return; - } - } - - // Other cases are more difficult - target.setAll(newList); - } - @Value private static class ReferenceEntry { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java b/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java new file mode 100644 index 000000000..9d0e95d94 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java @@ -0,0 +1,190 @@ +package io.xpipe.app.fxcomps.util; + +import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; + +public class ListBindingsHelper { + + public static void bindContent(ObservableList l1, ObservableList l2) { + setContent(l1, l2); + l2.addListener((ListChangeListener) c -> { + setContent(l1, l2); + }); + } + + public static ObservableValue anyMatch(List> l) { + return Bindings.createBooleanBinding( + () -> { + return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); + }, + l.toArray(ObservableValue[]::new)); + } + + public static ObservableList mappedContentBinding(ObservableList l2, Function map) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().map(map).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, l2); + return l1; + } + + public static ObservableList cachedMappedContentBinding( + ObservableList all, ObservableList shown, Function map) { + var cache = new HashMap(); + + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + cache.keySet().removeIf(t -> !all.contains(t)); + setContent( + l1, + shown.stream() + .map(v -> { + if (!cache.containsKey(v)) { + cache.put(v, map.apply(v)); + } + + return cache.get(v); + }) + .toList()); + }; + runnable.run(); + shown.addListener((ListChangeListener) c -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, all); + BindingsHelper.preserve(l1, shown); + return l1; + } + + public static ObservableList orderedContentBinding( + ObservableList l2, Comparator comp, Observable... observables) { + return orderedContentBinding( + l2, + Bindings.createObjectBinding( + () -> { + return new Comparator<>() { + @Override + public int compare(V o1, V o2) { + return comp.compare(o1, o2); + } + }; + }, + observables)); + } + + public static ObservableList orderedContentBinding( + ObservableList l2, ObservableValue> comp) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().sorted(comp.getValue()).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + comp.addListener((observable, oldValue, newValue) -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, l2); + return l1; + } + + public static ObservableList filteredContentBinding(ObservableList l2, Predicate predicate) { + return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate)); + } + + public static ObservableList filteredContentBinding( + ObservableList l2, Predicate predicate, Observable... observables) { + return filteredContentBinding( + l2, + Bindings.createObjectBinding( + () -> { + return new Predicate<>() { + @Override + public boolean test(V v) { + return predicate.test(v); + } + }; + }, + Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new))); + } + + public static ObservableList filteredContentBinding( + ObservableList l2, ObservableValue> predicate) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent( + l1, + predicate.getValue() != null + ? l2.stream().filter(predicate.getValue()).toList() + : l2); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + predicate.addListener((c, o, n) -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, l2); + return l1; + } + + public static void setContent(ObservableList target, List newList) { + if (target.equals(newList)) { + return; + } + + if (target.size() == 0) { + target.setAll(newList); + return; + } + + if (newList.size() == 0) { + target.clear(); + return; + } + + var targetSet = new HashSet<>(target); + var newSet = new HashSet<>(newList); + + // Only add missing element + if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) { + var l = new HashSet<>(newSet); + l.removeAll(targetSet); + if (l.size() > 0) { + var found = l.iterator().next(); + var index = newList.indexOf(found); + target.add(index, found); + return; + } + } + + // Only remove not needed element + if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) { + var l = new HashSet<>(targetSet); + l.removeAll(newSet); + if (l.size() > 0) { + target.remove(l.iterator().next()); + return; + } + } + + // Other cases are more difficult + target.setAll(newList); + } +} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java b/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java index 09c560f2a..43513134e 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java @@ -85,7 +85,6 @@ public class PlatformThread { ov.removeListener(invListenerMap.getOrDefault(listener, listener)); } }; - BindingsHelper.linkPersistently(obs, ov); return obs; } @@ -272,7 +271,6 @@ public class PlatformThread { ol.removeListener(invListenerMap.getOrDefault(listener, listener)); } }; - BindingsHelper.linkPersistently(obs, ol); return obs; } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java b/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java index 08a17de86..0863bafe8 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/Shortcuts.java @@ -41,7 +41,7 @@ public class Shortcuts { DISPLAY_SHORTCUTS.put(region, comb); AtomicReference scene = new AtomicReference<>(); - SimpleChangeListener.apply(region.sceneProperty(), s -> { + region.sceneProperty().subscribe(s -> { if (Objects.equals(s, scene.get())) { return; } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/SimpleChangeListener.java b/app/src/main/java/io/xpipe/app/fxcomps/util/SimpleChangeListener.java deleted file mode 100644 index 48720c16a..000000000 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/SimpleChangeListener.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.xpipe.app.fxcomps.util; - -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; - -@FunctionalInterface -public interface SimpleChangeListener { - - static void apply(ObservableValue obs, SimpleChangeListener cl) { - obs.addListener(cl.wrapped()); - cl.onChange(obs.getValue()); - } - - void onChange(T val); - - default ChangeListener wrapped() { - return (observable, oldValue, newValue) -> this.onChange(newValue); - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index b16362539..f319a258b 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -49,11 +49,13 @@ public class AppPrefs { map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class); final ObjectProperty terminalType = map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class); + final ObjectProperty rdpClientType = + map(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClientType.class); final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class); + final StringProperty customRdpClientCommand = + map(new SimpleStringProperty(null), "customRdpClientCommand", String.class); final StringProperty customTerminalCommand = - map(new SimpleStringProperty(""), "customTerminalCommand", String.class); - final BooleanProperty preferTerminalTabs = - map(new SimpleBooleanProperty(true), "preferTerminalTabs", Boolean.class); + map(new SimpleStringProperty(null), "customTerminalCommand", String.class); final BooleanProperty clearTerminalOnInit = map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class); public final BooleanProperty disableCertutilUse = @@ -104,7 +106,7 @@ public class AppPrefs { map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = bindDeveloperTrue(developerDisableGuiRestrictions); - private final ObjectProperty language = + final ObjectProperty language = map(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class); @Getter @@ -139,6 +141,7 @@ public class AppPrefs { new AppearanceCategory(), new TerminalCategory(), new EditorCategory(), + new RdpCategory(), new SyncCategory(), new VaultCategory(), new LocalShellCategory(), @@ -357,10 +360,18 @@ public class AppPrefs { return terminalType; } + public ObservableValue rdpClientType() { + return rdpClientType; + } + public ObservableValue customTerminalCommand() { return customTerminalCommand; } + public ObservableValue customRdpClientCommand() { + return customRdpClientCommand; + } + public ObservableValue storageDirectory() { return storageDirectory; } @@ -411,7 +422,12 @@ public class AppPrefs { if (externalEditor.get() == null) { ExternalEditorType.detectDefault(); } + terminalType.set(ExternalTerminalType.determineDefault(terminalType.get())); + + if (rdpClientType.get() == null) { + rdpClientType.setValue(ExternalRdpClientType.determineDefault()); + } } public Comp getCustomComp(String id) { diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java index 38caefcd3..f5cbc92cc 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.prefs; import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.ScrollPane; @@ -28,7 +27,7 @@ public class AppPrefsComp extends SimpleComp { .createRegion(); })); var pfxSp = new ScrollPane(); - SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> { + AppPrefs.get().getSelectedCategory().subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> { pfxSp.setContent(map.get(val)); }); diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java index 43de8d9aa..f8b0ad50e 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java @@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.css.PseudoClass; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -27,7 +26,7 @@ public class AppPrefsSidebarComp extends SimpleComp { .apply(struc -> { struc.get().setTextAlignment(TextAlignment.LEFT); struc.get().setAlignment(Pos.CENTER_LEFT); - SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> { + AppPrefs.get().getSelectedCategory().subscribe(val -> { struc.get().pseudoClassStateChanged(SELECTED, appPrefsCategory.equals(val)); }); }) @@ -36,13 +35,15 @@ public class AppPrefsSidebarComp extends SimpleComp { .toList(); var vbox = new VerticalComp(buttons).styleClass("sidebar"); vbox.apply(struc -> { - SimpleChangeListener.apply(PlatformThread.sync(AppPrefs.get().getSelectedCategory()), val -> { - var index = val != null ? AppPrefs.get().getCategories().indexOf(val) : 0; - if (index >= struc.get().getChildren().size()) { - return; - } + AppPrefs.get().getSelectedCategory().subscribe(val -> { + PlatformThread.runLaterIfNeeded(() -> { + var index = val != null ? AppPrefs.get().getCategories().indexOf(val) : 0; + if (index >= struc.get().getChildren().size()) { + return; + } - ((Button) struc.get().getChildren().get(index)).fire(); + ((Button) struc.get().getChildren().get(index)).fire(); + }); }); }); return vbox.createRegion(); diff --git a/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java b/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java index 75d7fb000..6cc2880a8 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java @@ -2,12 +2,21 @@ package io.xpipe.app.prefs; import atlantafx.base.controls.ProgressSliderSkin; import atlantafx.base.theme.Styles; +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppTheme; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IntFieldComp; +import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.OptionsBuilder; +import javafx.geometry.Pos; import javafx.scene.control.Slider; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.Arrays; +import java.util.List; public class AppearanceCategory extends AppPrefsCategory { @@ -16,12 +25,28 @@ public class AppearanceCategory extends AppPrefsCategory { return "appearance"; } + private Comp languageChoice() { + var prefs = AppPrefs.get(); + var c = ChoiceComp.ofTranslatable(prefs.language, Arrays.asList(SupportedLocale.values()), false); + var visit = new ButtonComp(AppI18n.observable("translate"), new FontIcon("mdi2w-web"), () -> { + Hyperlinks.open(Hyperlinks.TRANSLATE); + }); + return new HorizontalComp(List.of(c, visit)).apply(struc -> { + struc.get().setAlignment(Pos.CENTER_LEFT); + struc.get().setSpacing(10); + }); + } + @Override protected Comp create() { var prefs = AppPrefs.get(); return new OptionsBuilder() .addTitle("uiOptions") .sub(new OptionsBuilder() + .nameAndDescription("language") + .addComp( + languageChoice(), + prefs.language) .nameAndDescription("theme") .addComp( ChoiceComp.ofTranslatable(prefs.theme, AppTheme.Theme.ALL, false) diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java new file mode 100644 index 000000000..68f88a7a6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java @@ -0,0 +1,163 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.*; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import lombok.Value; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Supplier; + +public interface ExternalRdpClientType extends PrefsChoiceValue { + + @Value + class LaunchConfiguration { + String title; + RdpConfig config; + UUID storeId; + SecretRetrievalStrategy password; + } + + abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType { + + public PathCheckType(String id, String executable, boolean explicityAsync) { + super(id, executable, explicityAsync); + } + } + + abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalRdpClientType { + + public MacOsType(String id, String applicationName) { + super(id, applicationName); + } + } + + void launch(LaunchConfiguration configuration) throws Exception; + + default Path writeConfig(RdpConfig input) throws Exception { + var file = LocalShell.getShell().getSystemTemporaryDirectory().join("exec-" + ScriptHelper.getScriptId() + ".rdp"); + var string = input.toString(); + Files.writeString(file.toLocalPath(), string); + return file.toLocalPath(); + } + + ExternalRdpClientType MSTSC = new PathCheckType("app.mstsc", "mstsc.exe", true) { + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var adaptedRdpConfig = getAdaptedConfig(configuration); + var file = writeConfig(adaptedRdpConfig); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(executable).addFile(file.toString())); + } + + private RdpConfig getAdaptedConfig(LaunchConfiguration configuration) throws Exception { + var input = configuration.getConfig(); + if (input.get("password 51").isPresent()) { + return input; + } + + var address = input.get("full address") + .map(typedValue -> typedValue.getValue()) + .orElse("?"); + var pass = SecretManager.retrieve( + configuration.getPassword(), "Password for " + address, configuration.getStoreId(), 0); + if (pass == null) { + return input; + } + + var adapted = input.overlay(Map.of( + "password 51", + new RdpConfig.TypedValue("b", encrypt(pass.getSecretValue())), + "prompt for credentials", + new RdpConfig.TypedValue("i", "0"))); + return adapted; + } + + private String encrypt(String password) throws Exception { + var ps = LocalShell.getLocalPowershell(); + var cmd = ps.command( + "(\"" + password + "\" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString;"); + cmd.setSensitive(); + return cmd.readStdoutOrThrow(); + } + }; + + ExternalRdpClientType REMMINA = new PathCheckType("app.remmina", "remmina", true) { + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var file = writeConfig(configuration.getConfig()); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(executable).add("-c").addFile(file.toString())); + } + }; + + ExternalRdpClientType MICROSOFT_REMOTE_DESKTOP_MACOS_APP = new MacOsType("app.microsoftRemoteDesktopApp", "Microsoft Remote Desktop.app") { + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var file = writeConfig(configuration.getConfig()); + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Microsoft Remote Desktop.app") + .addFile(file.toString())); + } + }; + + class CustomType extends ExternalApplicationType implements ExternalRdpClientType { + + public CustomType() { + super("app.custom"); + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var customCommand = AppPrefs.get().customRdpClientCommand().getValue(); + if (customCommand == null || customCommand.isBlank()) { + throw ErrorEvent.expected(new IllegalStateException("No custom RDP command specified")); + } + + var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; + ExternalApplicationHelper.startAsync(CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", writeConfig(configuration.getConfig()).toString()))); + } + + @Override + public boolean isAvailable() { + return true; + } + } + + ExternalRdpClientType CUSTOM = new CustomType(); + + List WINDOWS_CLIENTS = List.of(MSTSC); + List LINUX_CLIENTS = List.of(REMMINA); + List MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP); + + @SuppressWarnings("TrivialFunctionalExpressionUsage") + List ALL = ((Supplier>) () -> { + var all = new ArrayList(); + if (OsType.getLocal().equals(OsType.WINDOWS)) { + all.addAll(WINDOWS_CLIENTS); + } + if (OsType.getLocal().equals(OsType.LINUX)) { + all.addAll(LINUX_CLIENTS); + } + if (OsType.getLocal().equals(OsType.MACOS)) { + all.addAll(MACOS_CLIENTS); + } + all.add(CUSTOM); + return all; + }).get(); + + static ExternalRdpClientType determineDefault() { + return ALL.stream() + .filter(t -> !t.equals(CUSTOM)) + .filter(t -> t.isAvailable()) + .findFirst() + .orElse(null); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java b/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java new file mode 100644 index 000000000..68ab53bd6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java @@ -0,0 +1,32 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.TextFieldComp; +import io.xpipe.app.util.OptionsBuilder; + +public class RdpCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "rdp"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + return new OptionsBuilder() + .addTitle("rdpConfiguration") + .sub(new OptionsBuilder() + .nameAndDescription("rdpClient") + .addComp(ChoiceComp.ofTranslatable( + prefs.rdpClientType, PrefsChoiceValue.getSupported(ExternalRdpClientType.class), false)) + .nameAndDescription("customRdpClientCommand") + .addComp(new TextFieldComp(prefs.customRdpClientCommand, true) + .apply(struc -> struc.get().setPromptText("myrdpclient -c $FILE")) + .hide(prefs.rdpClientType.isNotEqualTo(ExternalRdpClientType.CUSTOM))) + ) + .buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java b/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java index a3a3cde31..314c9859b 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java +++ b/app/src/main/java/io/xpipe/app/prefs/SupportedLocale.java @@ -11,8 +11,8 @@ import java.util.Locale; @AllArgsConstructor @Getter public enum SupportedLocale implements PrefsChoiceValue { - ENGLISH(Locale.ENGLISH, "english"); - // GERMAN(Locale.GERMAN, "german"); + ENGLISH(Locale.ENGLISH, "english"), + GERMAN(Locale.GERMAN, "german"); private final Locale locale; private final String id; diff --git a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java index af2dbde99..1e369e327 100644 --- a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java @@ -8,7 +8,6 @@ import io.xpipe.app.fxcomps.impl.ChoiceComp; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.OptionsBuilder; @@ -66,14 +65,14 @@ public class TerminalCategory extends AppPrefsCategory { Hyperlinks.open(t.getWebsite()); }); - var visitVisible = BindingsHelper.persist(Bindings.createBooleanBinding(() -> { + var visitVisible = Bindings.createBooleanBinding(() -> { var t = prefs.terminalType().getValue(); if (t == null || t.getWebsite() == null) { return false; } return true; - }, prefs.terminalType())); + }, prefs.terminalType()); visit.visible(visitVisible); return new HorizontalComp(List.of(c, visit)).apply(struc -> { @@ -108,22 +107,6 @@ public class TerminalCategory extends AppPrefsCategory { .apply(struc -> struc.get().setPromptText("myterminal -e $CMD")) .hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM))) .addComp(terminalTest) - .name("preferTerminalTabs") - .description(Bindings.createStringBinding( - () -> { - var disabled = prefs.terminalType().getValue() != null - && !prefs.terminalType.get().supportsTabs(); - return !disabled - ? AppI18n.get("preferTerminalTabs") - : AppI18n.get( - "preferTerminalTabsDisabled", - prefs.terminalType() - .getValue() - .toTranslatedString() - .getValue()); - }, - prefs.terminalType())) - .addToggle(prefs.preferTerminalTabs) .disable(Bindings.createBooleanBinding( () -> { return prefs.terminalType().getValue() != null diff --git a/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java b/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java index 5d6241b20..339a77839 100644 --- a/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java @@ -27,7 +27,7 @@ public class UpdateCheckComp extends SimpleComp { XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate())); } - private void restart() { + private void performUpdateAndRestart() { XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(); UpdateAvailableAlert.showIfNeeded(); } @@ -82,7 +82,7 @@ public class UpdateCheckComp extends SimpleComp { return new TileButtonComp(name, description, graphic, actionEvent -> { actionEvent.consume(); if (updateReady.getValue()) { - restart(); + performUpdateAndRestart(); return; } diff --git a/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java b/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java index dcc83c3e6..a2fb3f221 100644 --- a/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java +++ b/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java @@ -18,6 +18,12 @@ public class UpdateAvailableAlert { return; } + // Check whether we still have the latest version prepared + uh.refreshUpdateCheckSilent(); + if (uh.getPreparedUpdate().getValue() == null) { + return; + } + var u = uh.getPreparedUpdate().getValue(); var update = AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("updateReadyAlertTitle")); diff --git a/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java b/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java deleted file mode 100644 index 0c42c420b..000000000 --- a/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.xpipe.app.update; - -import io.xpipe.app.core.AppProperties; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.XPipeInstance; -import io.xpipe.core.store.ShellStore; - -import java.io.IOException; -import java.nio.file.Files; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -public class XPipeInstanceHelper { - - public static UUID getInstanceId() { - var file = AppProperties.get().getDataDir().resolve("instance"); - if (!Files.exists(file)) { - var id = UUID.randomUUID(); - try { - Files.writeString(file, id.toString()); - } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); - } - return id; - } - - try { - return UUID.fromString(Files.readString(file)); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).handle(); - return UUID.randomUUID(); - } - } - - public static boolean isSupported(ShellStore host) { - try (var pc = host.control().start(); - var cmd = pc.command("xpipe")) { - cmd.discardOrThrow(); - return true; - } catch (Exception e) { - return false; - } - } - - public static Optional getInstance(ShellStore store) { - if (!isSupported(store)) { - return Optional.empty(); - } - - // try (BeaconClient beaconClient = ProcessBeaconClient.create(store)) { - // beaconClient.sendRequest(InstanceExchange.Request.builder().build()); - // InstanceExchange.Response response = beaconClient.receiveResponse(); - // return Optional.of(response.getInstance()); - // } catch (Exception e) { - // return Optional.empty(); - // } - return Optional.empty(); - } - - public static XPipeInstance refresh() { - Map> map = DataStorage.get().getStoreEntries().stream() - .filter(entry -> entry.getStore() instanceof ShellStore) - .collect(Collectors.toMap( - entry -> entry.getStore().asNeeded(), - entry -> getInstance(entry.getStore().asNeeded()))); - var adjacent = map.entrySet().stream() - .filter(shellStoreOptionalEntry -> - shellStoreOptionalEntry.getValue().isPresent()) - .collect(Collectors.toMap( - entry -> entry.getKey(), entry -> entry.getValue().get())); - var reachable = adjacent.values().stream() - .map(XPipeInstance::getReachable) - .flatMap(Collection::stream) - .toList(); - - var id = getInstanceId(); - var name = "test"; - return new XPipeInstance(id, name, adjacent, reachable); - } -} 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 f274250d7..65410fa29 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.comp.store.StoreCategoryWrapper; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.geometry.Insets; import javafx.scene.control.ComboBox; @@ -28,7 +27,7 @@ public class DataStoreCategoryChoiceComp extends SimpleComp { @Override protected Region createSimple() { - SimpleChangeListener.apply(external, newValue -> { + external.subscribe(newValue -> { if (newValue == null) { value.setValue(root); } else if (root == null) { diff --git a/app/src/main/java/io/xpipe/app/util/Hyperlinks.java b/app/src/main/java/io/xpipe/app/util/Hyperlinks.java index 37b66fcb1..cc9f384d1 100644 --- a/app/src/main/java/io/xpipe/app/util/Hyperlinks.java +++ b/app/src/main/java/io/xpipe/app/util/Hyperlinks.java @@ -9,6 +9,7 @@ public class Hyperlinks { public static final String PRIVACY = "https://docs.xpipe.io/privacy-policy"; public static final String EULA = "https://docs.xpipe.io/end-user-license-agreement"; public static final String SECURITY = "https://docs.xpipe.io/security"; + public static final String TRANSLATE = "https://github.com/xpipe-io/xpipe/lang"; public static final String DISCORD = "https://discord.gg/8y89vS8cRb"; public static final String SLACK = "https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg"; diff --git a/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java b/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java index 0b7f50c0a..0c713373c 100644 --- a/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java +++ b/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java @@ -297,7 +297,7 @@ public class OptionsBuilder { public OptionsBuilder longDescription(String descriptionKey) { finishCurrent(); - longDescription = AppI18n.getInstance().getMarkdownDocumentation(descriptionKey); + longDescription = AppI18n.get().getMarkdownDocumentation(descriptionKey); return this; } diff --git a/app/src/main/java/io/xpipe/app/util/RdpConfig.java b/app/src/main/java/io/xpipe/app/util/RdpConfig.java new file mode 100644 index 000000000..6e7174d9d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/RdpConfig.java @@ -0,0 +1,64 @@ +package io.xpipe.app.util; + +import lombok.Value; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Value +public class RdpConfig { + + public static RdpConfig parseFile(String file) throws IOException { + var content = Files.readString(Path.of(file)); + return parseContent(content); + } + + public static RdpConfig parseContent(String content) { + var map = new LinkedHashMap(); + content.lines().forEach(s -> { + var split = s.split(":"); + if (split.length < 2) { + return; + } + + if (split.length == 2) { + map.put(split[0].trim(), new RdpConfig.TypedValue("s", split[1].trim())); + } + + + if (split.length == 3) { + map.put(split[0].trim(), new RdpConfig.TypedValue(split[1].trim(), split[2].trim())); + } + }); + return new RdpConfig(map); + } + + @Value + public static class TypedValue { + String type; + String value; + } + + Map content; + + public RdpConfig overlay(Map override) { + var newMap = new LinkedHashMap<>(content); + newMap.putAll(override); + return new RdpConfig(newMap); + } + + public String toString() { + return content.entrySet().stream().map(e -> { + return e.getKey() + ":" + e.getValue().getType() + ":" + e.getValue().getValue(); + }).collect(Collectors.joining("\n")); + } + + public Optional get(String key) { + return Optional.ofNullable(content.get(key)); + } +} 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 d6804e8d1..4f4e53cd0 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -7,7 +7,6 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; @@ -168,7 +167,7 @@ public class ScanAlert { }) .padding(new Insets(20)); - SimpleChangeListener.apply(entry, newValue -> { + entry.subscribe(newValue -> { selected.clear(); stackPane.getChildren().clear(); diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 4bad25c43..ffff35305 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -77,6 +77,7 @@ open module io.xpipe.app { requires jdk.management; requires jdk.management.agent; requires net.steppschuh.markdowngenerator; + requires com.shinyhut.vernacular; // Required by extensions requires java.security.jgss; @@ -150,6 +151,5 @@ open module io.xpipe.app { TerminalWaitExchangeImpl, TerminalLaunchExchangeImpl, QueryStoreExchangeImpl, - InstanceExchangeImpl, VersionExchangeImpl; } 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 47e2669e5..e89e46444 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 @@ -78,6 +78,7 @@ browseInternalStorage=Browse internal storage addTunnel=Tunnel ... addScript=Script ... addHost=Remote Host ... +addVisual=Visual ... addShell=Shell Environment ... addCommand=Custom Command ... addAutomatically=Search Automatically ... @@ -111,8 +112,6 @@ newLine=Newline crlf=CRLF (Windows) lf=LF (Linux) none=None -expand=Expand -accessSubConnections=Access sub connections common=Common key=Key color=Color @@ -140,7 +139,7 @@ test=Test lockCreationAlertTitle=Set passphrase lockCreationAlertHeader=Set your new master passphrase finish=Finish -error=An error occurred +error=Error downloadStageDescription=Downloads files to your local machine, so you can drag and drop them into your native desktop environment. ok=Ok search=Search diff --git a/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css b/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css index 796d4b334..a40eb5d41 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css @@ -43,11 +43,6 @@ -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(12, 11, 11) 40%, rgb(32, 32, 40) 50%, rgb(35, 29, 29) 100%); } -.store-header-bar .menu-button:hover, .root:key-navigation .store-header-bar .menu-button:focused { - -fx-background-color: -color-bg-default; - -fx-border-color: -color-fg-default; -} - .store-header-bar .menu-button > * { -fx-text-fill: -color-bg-default; } @@ -61,15 +56,20 @@ -fx-border-width: 4; } -.store-header-bar .menu-button:hover > *, .root:key-navigation .store-header-bar .menu-button:focused > * { +.root .store-header-bar .menu-button:hover, .root:key-navigation .store-header-bar .menu-button:focused { + -fx-background-color: -color-bg-default; + -fx-border-color: -color-fg-default; +} + +.root .store-header-bar .menu-button:hover > *, .root:key-navigation .store-header-bar .menu-button:focused > * { -fx-text-fill: -color-fg-default; } -.store-header-bar .menu-button:hover > * > .ikonli-font-icon, .root:key-navigation .store-header-bar .menu-button:focused > * > .ikonli-font-icon { +.root .store-header-bar .menu-button:hover > * > .ikonli-font-icon, .root:key-navigation .store-header-bar .menu-button:focused > * > .ikonli-font-icon { -fx-icon-color: -color-fg-default; } -.store-header-bar .menu-button:hover .arrow, .root:key-navigation .store-header-bar .menu-button:focused .arrow { +.root .store-header-bar .menu-button:hover .arrow, .root:key-navigation .store-header-bar .menu-button:focused .arrow { -fx-border-color: -color-fg-default; -fx-border-width: 4; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css index 9961de269..c170ce6a9 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/style.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css @@ -2,19 +2,11 @@ -fx-background-color: transparent; } -.root:pretty:dark.background { - -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, derive(-color-bg-default, 5%) 40%, derive(-color-bg-default, 2%) 50%, derive(-color-bg-default, 5%) 100%); -} - -.root:performance:dark.background { +.root:dark.background { -fx-background-color: derive(-color-bg-default, 5%); } -.root:pretty:light.background { - -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, derive(-color-bg-default, -9%) 40%, derive(-color-bg-default, 1%) 50%, derive(-color-bg-default, -9%) 100%); -} - -.root:performance:light.background { +.root:light.background { -fx-background-color: derive(-color-bg-default, -9%); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index bcef6b4a1..724f92bf6 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -11,10 +11,8 @@ import com.fasterxml.jackson.databind.node.TextNode; import io.xpipe.beacon.exchange.MessageExchanges; import io.xpipe.beacon.exchange.data.ClientErrorMessage; import io.xpipe.beacon.exchange.data.ServerErrorMessage; -import io.xpipe.core.store.ShellStore; import io.xpipe.core.util.Deobfuscator; import io.xpipe.core.util.JacksonMapper; -import io.xpipe.core.util.ProxyManagerProvider; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -62,51 +60,6 @@ public class BeaconClient implements AutoCloseable { return client; } - public static BeaconClient connectProxy(ShellStore proxy) throws Exception { - var control = proxy.control().start(); - if (!ProxyManagerProvider.get().setup(control)) { - throw new IOException("XPipe connector required to perform operation"); - } - var command = control.command("xpipe beacon --raw").start(); - command.discardErr(); - return new BeaconClient(command, command.getStdout(), command.getStdin()) { - - // { - // new Thread(() -> { - // while (true) { - // if (!control.isRunning()) { - // close(); - // } - // } - // }) - // } - - @Override - public void close() throws ConnectorException { - try { - getRawInputStream().readAllBytes(); - } catch (IOException ex) { - throw new ConnectorException(ex); - } - - super.close(); - } - - @Override - public T receiveResponse() - throws ConnectorException, ClientException, ServerException { - try { - sendEOF(); - getRawOutputStream().close(); - } catch (IOException ex) { - throw new ConnectorException(ex); - } - - return super.receiveResponse(); - } - }; - } - public static Optional tryEstablishConnection(ClientInformation information) { try { return Optional.of(establishConnection(information)); diff --git a/beacon/src/main/java/io/xpipe/beacon/XPipeInstance.java b/beacon/src/main/java/io/xpipe/beacon/XPipeInstance.java deleted file mode 100644 index 272086c04..000000000 --- a/beacon/src/main/java/io/xpipe/beacon/XPipeInstance.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.xpipe.beacon; - -import io.xpipe.core.store.ShellStore; -import lombok.Value; - -import java.util.List; -import java.util.Map; -import java.util.UUID; - -@Value -public class XPipeInstance { - - UUID uuid; - String name; - Map adjacent; - List reachable; -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/InstanceExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/InstanceExchange.java deleted file mode 100644 index 9f080bb88..000000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/InstanceExchange.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.XPipeInstance; -import io.xpipe.beacon.exchange.MessageExchange; -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class InstanceExchange implements MessageExchange { - - @Override - public String getId() { - return "instance"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - @NonNull - XPipeInstance instance; - } -} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 53b0602d1..56dc388d9 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -27,7 +27,6 @@ open module io.xpipe.beacon { SinkExchange, DrainExchange, LaunchExchange, - InstanceExchange, EditStoreExchange, StoreProviderListExchange, ModeExchange, diff --git a/build.gradle b/build.gradle index 277f6755f..16eb3ff15 100644 --- a/build.gradle +++ b/build.gradle @@ -104,6 +104,7 @@ project.ext { if (signingPassword == null) { signingPassword = '' } + deeplApiKey = findProperty('DEEPL_API_KEY') } if (org.gradle.internal.os.OperatingSystem.current() == org.gradle.internal.os.OperatingSystem.LINUX) { diff --git a/core/src/main/java/io/xpipe/core/store/FilePath.java b/core/src/main/java/io/xpipe/core/store/FilePath.java index 61228982b..d8da0b7e0 100644 --- a/core/src/main/java/io/xpipe/core/store/FilePath.java +++ b/core/src/main/java/io/xpipe/core/store/FilePath.java @@ -3,6 +3,7 @@ package io.xpipe.core.store; import lombok.EqualsAndHashCode; import lombok.NonNull; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -23,6 +24,10 @@ public final class FilePath { } } + public Path toLocalPath() { + return Path.of(value); + } + public String toString() { return value; } diff --git a/core/src/main/java/io/xpipe/core/store/LaunchableStore.java b/core/src/main/java/io/xpipe/core/store/LaunchableStore.java index 234429453..b0a5b3e77 100644 --- a/core/src/main/java/io/xpipe/core/store/LaunchableStore.java +++ b/core/src/main/java/io/xpipe/core/store/LaunchableStore.java @@ -1,12 +1,6 @@ package io.xpipe.core.store; -import io.xpipe.core.process.ProcessControl; - public interface LaunchableStore extends DataStore { - default boolean canLaunch() { - return true; - } - - ProcessControl prepareLaunchCommand() throws Exception; + default void launch() throws Exception {} } diff --git a/core/src/main/java/io/xpipe/core/store/ShellStore.java b/core/src/main/java/io/xpipe/core/store/ShellStore.java index 51cdc2ce4..3d249b9ed 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/ShellStore.java @@ -14,7 +14,6 @@ public interface ShellStore extends DataStore, LaunchableStore, FileSystemStore, return new ConnectionFileSystem(control(), this); } - @Override default ProcessControl prepareLaunchCommand() { return control(); } diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java index 8aee357ec..c5a4083b1 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -295,6 +295,22 @@ public class XPipeInstallation { return path; } + public static Path getLangPath() { + if (!ModuleHelper.isImage()) { + return getCurrentInstallationBasePath().resolve("lang"); + } + + var install = getCurrentInstallationBasePath(); + var type = OsType.getLocal(); + if (type.equals(OsType.WINDOWS)) { + return install.resolve("app").resolve("lang"); + } else if (type.equals(OsType.LINUX)) { + return install.resolve("app").resolve("lang"); + } else { + return install.resolve("Contents").resolve("Resources").resolve("lang"); + } + } + public static Path getBundledFontsPath() { if (!ModuleHelper.isImage()) { return Path.of("dist", "fonts"); diff --git a/dist/build.gradle b/dist/build.gradle index 22f199b12..dbea9df0a 100644 --- a/dist/build.gradle +++ b/dist/build.gradle @@ -91,6 +91,7 @@ if (rootProject.fullVersion) { apply from: 'choco.gradle' apply from: 'winget.gradle' apply from: 'install.gradle' + apply from: 'i18n.gradle' signing { useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java index 874e2f9cb..b881d75e9 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java @@ -40,7 +40,7 @@ public class LaunchAction implements ActionProvider { @Override public boolean isApplicable(DataStoreEntryRef o) { - return o.get().getValidity().isUsable() && o.getStore().canLaunch(); + return o.get().getValidity().isUsable(); } @Override @@ -71,7 +71,7 @@ public class LaunchAction implements ActionProvider { @Override public boolean isApplicable(DataStoreEntryRef o) { - return o.get().getValidity().isUsable() && o.getStore().canLaunch(); + return o.get().getValidity().isUsable(); } }; } @@ -95,12 +95,7 @@ public class LaunchAction implements ActionProvider { } if (entry.getStore() instanceof LaunchableStore s) { - var command = s.prepareLaunchCommand(); - if (command == null) { - return; - } - - TerminalLauncher.open(entry, storeName, null, command); + s.launch(); } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java deleted file mode 100644 index a0a7a27ba..000000000 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.xpipe.ext.base.action; - -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.ActionProvider; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.DesktopShortcuts; -import io.xpipe.core.store.LaunchableStore; -import javafx.beans.value.ObservableValue; -import lombok.Value; - -public class LaunchShortcutAction implements ActionProvider { - - @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite() { - - @Override - public Action createAction(DataStoreEntryRef store) { - return new Action(store.get()); - } - - @Override - public Class getApplicableClass() { - return LaunchableStore.class; - } - - @Override - public ObservableValue getName(DataStoreEntryRef store) { - return AppI18n.observable("createShortcut"); - } - - @Override - public String getIcon(DataStoreEntryRef store) { - return "mdi2c-code-greater-than"; - } - }; - } - - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry entry; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - public void execute() throws Exception { - DesktopShortcuts.create("xpipe://launch/" + entry.getUuid().toString(), entry.getName()); - } - } -} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java index c32c2eb99..f38d12fe8 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java @@ -5,9 +5,7 @@ import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.LaunchableStore; import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.JacksonMapper; import lombok.Value; @@ -43,7 +41,7 @@ public class XPipeUrlAction implements ActionProvider { if (!entry.getValidity().isUsable()) { return null; } - return new LaunchAction(entry); + return new LaunchAction.Action(entry); } case "action" -> { var id = args.get(1); @@ -87,30 +85,6 @@ public class XPipeUrlAction implements ActionProvider { } } - @Value - static class LaunchAction implements ActionProvider.Action { - - DataStoreEntry entry; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - public void execute() throws Exception { - var storeName = entry.getName(); - if (entry.getStore() instanceof LaunchableStore s) { - var command = s.prepareLaunchCommand(); - if (command == null) { - return; - } - - TerminalLauncher.open(storeName, command); - } - } - } - @Value static class AddStoreAction implements ActionProvider.Action { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java index 1705ae095..3037aa465 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java @@ -5,6 +5,7 @@ import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.icon.BrowserIconFileType; import io.xpipe.app.browser.icon.BrowserIcons; +import io.xpipe.app.browser.icon.BrowserIconFileType; import javafx.scene.Node; import java.util.List; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/LaunchableTerminalStore.java b/ext/base/src/main/java/io/xpipe/ext/base/store/LaunchableTerminalStore.java new file mode 100644 index 000000000..8feb2ad24 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/LaunchableTerminalStore.java @@ -0,0 +1,16 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.process.ProcessControl; +import io.xpipe.core.store.LaunchableStore; +import io.xpipe.ext.base.SelfReferentialStore; + +public interface LaunchableTerminalStore extends SelfReferentialStore, LaunchableStore { + + @Override + default void launch() throws Exception { + TerminalLauncher.open(getSelfEntry(), getSelfEntry().getName(), null, prepareLaunchCommand()); + } + + ProcessControl prepareLaunchCommand() throws Exception; +} diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_de.properties b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_de.properties deleted file mode 100644 index d86dcac53..000000000 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_de.properties +++ /dev/null @@ -1,3 +0,0 @@ -displayName=Mein Dateiformat -description=Meine Dateiformat-Beschreibung -fileName=Mein Dateiformat Datei \ No newline at end of file diff --git a/gradle/gradle_scripts/vernacular-1.16.jar b/gradle/gradle_scripts/vernacular-1.16.jar new file mode 100644 index 0000000000000000000000000000000000000000..64b3ff9806657953f9296606ddf311a6894f13f8 GIT binary patch literal 94482 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ĸ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 +finishStep=Fertigstellen +edit=Bearbeiten +browseInternal=Intern durchsuchen +checkOutUpdate=Update auschecken +open=Öffnen +quit=Beenden +noTerminalSet=Es wurde keine Terminalanwendung automatisch eingestellt. Du kannst dies manuell im Einstellungsmenü tun. +connections=Verbindungen +settings=Einstellungen +explorePlans=Lizenz +help=Hilfe +#custom +about=Informationen +developer=Entwickler +browseFileTitle=Datei durchsuchen +browse=Durchsuchen +browser=Browser +selectFileFromComputer=Eine Datei von diesem Computer auswählen +links=Nützliche Links +website=Website +documentation=Dokumentation +discordDescription=Dem Discord-Server beitreten +security=Sicherheit +securityPolicy=Sicherheitsinformationen +securityPolicyDescription=Lies die detaillierte Sicherheitsrichtlinie +privacy=Datenschutzrichtlinie +privacyDescription=Lies die Datenschutzbestimmungen für die XPipe-Anwendung +slackDescription=Dem Slack-Arbeitsbereich beitreten +support=Unterstützung +githubDescription=Schau dir das GitHub-Repository an +openSourceNotices=Open-Source-Hinweise +xPipeClient=XPipe Desktop +checkForUpdates=Nach Updates suchen +checkForUpdatesDescription=Ein Update herunterladen, wenn es eins gibt +lastChecked=Zuletzt geprüft +version=Version +#custom +build=Build +runtimeVersion=Laufzeitversion +virtualMachine=Virtuelle Maschine +updateReady=Update installieren +updateReadyPortable=Update auschecken +updateReadyDescription=Ein Update wurde heruntergeladen und ist bereit zur Installation +updateReadyDescriptionPortable=Ein Update ist zum Download verfügbar +updateRestart=Neustart zur Aktualisierung +never=Niemals +updateAvailableTooltip=Update verfügbar +visitGithubRepository=GitHub Repository besuchen +updateAvailable=Update verfügbar: $VERSION$ +downloadUpdate=Update herunterladen +legalAccept=Ich akzeptiere die Endbenutzer-Lizenzvereinbarung +confirm=Bestätige +print=Drucken +whatsNew=Was ist neu in der Version $VERSION$ ($DATE$) +antivirusNoticeTitle=Ein Hinweis auf Antivirenprogramme +updateChangelogAlertTitle=Changelog +greetingsAlertTitle=Willkommen bei XPipe +gotIt=Verstanden +eula=Endbenutzer-Lizenzvertrag +news=Nachrichten +introduction=Einführung +privacyPolicy=Datenschutzrichtlinie +agree=Zustimmen +disagree=Widerspreche +directories=Verzeichnisse +logFile=Log-Datei +logFiles=Log-Dateien +logFilesAttachment=Log-Dateien +issueReporter=Ausgabe Reporter +openCurrentLogFile=Log-Dateien +openCurrentLogFileDescription=Die Protokolldatei der aktuellen Sitzung öffnen +openLogsDirectory=Logs-Verzeichnis öffnen +installationFiles=Installationsdateien +openInstallationDirectory=Installationsdateien +openInstallationDirectoryDescription=XPipe-Installationsverzeichnis öffnen +launchDebugMode=Debug-Modus +launchDebugModeDescription=XPipe im Debug-Modus neu starten +extensionInstallTitle=Herunterladen +extensionInstallDescription=Diese Aktion erfordert zusätzliche Bibliotheken von Drittanbietern, die nicht von XPipe vertrieben werden. Du kannst sie hier automatisch installieren. Die Komponenten werden dann von der Website des Anbieters heruntergeladen: +extensionInstallLicenseNote=Durch das Herunterladen und die automatische Installation erklärst du dich mit den Bedingungen der Lizenzen von Drittanbietern einverstanden: +license=Lizenz +installRequired=Installation erforderlich +restore=Wiederherstellen +restoreAllSessions=Alle Sitzungen wiederherstellen +connectionTimeout=Zeitüberschreitung beim Verbindungsstart +connectionTimeoutDescription=Die Zeit in Sekunden, die auf eine Antwort gewartet wird, bevor eine Verbindung als beendet gilt. Wenn einige deiner Fernsysteme lange brauchen, um sich zu verbinden, kannst du versuchen, diesen Wert zu erhöhen. +useBundledTools=Gebündelte OpenSSH-Tools verwenden +useBundledToolsDescription=Ziehe die gebündelte Version des openssh-Clients deiner lokal installierten Version vor.\n\nDiese Version ist in der Regel aktueller als die auf deinem System mitgelieferte und unterstützt möglicherweise zusätzliche Funktionen. Damit entfällt auch die Notwendigkeit, diese Tools überhaupt zu installieren.\n\nZur Anwendung ist ein Neustart erforderlich. +appearance=Erscheinungsbild +integrations=Integrationen +uiOptions=UI Optionen +theme=Thema +localShell=Lokale Shell +themeDescription=Dein bevorzugtes Anzeigethema +dontAutomaticallyStartVmSshServer=SSH-Server für VMs bei Bedarf nicht automatisch starten +dontAutomaticallyStartVmSshServerDescription=Jede Shell-Verbindung zu einer VM, die in einem Hypervisor läuft, wird über SSH hergestellt. XPipe kann bei Bedarf automatisch den installierten SSH-Server starten. Wenn du das aus Sicherheitsgründen nicht möchtest, kannst du dieses Verhalten mit dieser Option einfach deaktivieren. +confirmGitShareTitle=Bestätige die Git-Freigabe +confirmGitShareHeader=Dadurch wird die Datei in deinen Git-Datenspeicher kopiert und deine Änderungen werden übertragen. Willst du fortfahren? +gitShareFileTooltip=Datei zum Git Vault-Datenverzeichnis hinzufügen, damit sie automatisch synchronisiert wird.\n\n Diese Aktion kann nur verwendet werden, wenn der Git Vault in den Einstellungen aktiviert ist. +performanceMode=Leistungsmodus +performanceModeDescription=Deaktiviert alle visuellen Effekte, die nicht zur Verbesserung der Anwendungsleistung erforderlich sind. +dontAcceptNewHostKeys=Neue SSH-Hostschlüssel nicht automatisch akzeptieren +dontAcceptNewHostKeysDescription=XPipe akzeptiert standardmäßig automatisch Hostschlüssel von Systemen, auf denen dein SSH-Client keinen bekannten Hostschlüssel gespeichert hat. Wenn sich jedoch ein bekannter Host-Schlüssel geändert hat, wird die Verbindung verweigert, bis du den neuen Schlüssel akzeptierst.\n\nWenn du dieses Verhalten deaktivierst, kannst du alle Host-Schlüssel überprüfen, auch wenn es zunächst keinen Konflikt gibt. +uiScale=UI-Skala +uiScaleDescription=Ein benutzerdefinierter Skalierungswert, der unabhängig von der systemweiten Anzeigeskala eingestellt werden kann. Die Werte sind in Prozent angegeben, d.h. ein Wert von 150 führt zu einer Skalierung der Benutzeroberfläche von 150%.\n\nDie Anwendung erfordert einen Neustart. +editorProgram=Editor Programm +editorProgramDescription=Der Standard-Texteditor, der beim Bearbeiten von Textdaten aller Art verwendet wird. +windowOpacity=Fenster-Opazität +windowOpacityDescription=Ändert die Deckkraft des Fensters, um zu verfolgen, was im Hintergrund passiert. +useSystemFont=Systemschriftart verwenden +openDataDir=Verzeichnis der Tresordaten +openDataDirButton=Datenverzeichnis öffnen +openDataDirDescription=Wenn du zusätzliche Dateien, wie z.B. SSH-Schlüssel, systemübergreifend mit deinem Git-Repository synchronisieren möchtest, kannst du sie in das Verzeichnis Speicherdaten legen. Bei allen Dateien, die dort referenziert werden, werden die Dateipfade auf allen synchronisierten Systemen automatisch angepasst. +#custom +updates=Updates +passwordKey=Passwortschlüssel +selectAll=Alles auswählen +command=Befehl +advanced=Fortgeschrittene +thirdParty=Open-Source-Hinweise +eulaDescription=Lies die Endbenutzer-Lizenzvereinbarung für die XPipe-Anwendung +thirdPartyDescription=Die Open-Source-Lizenzen von Bibliotheken Dritter anzeigen +workspaceLock=Master-Passphrase +enableGitStorage=Git-Synchronisation einschalten +sharing=Teilen +sync=Synchronisation +enableGitStorageDescription=Wenn diese Funktion aktiviert ist, initialisiert XPipe ein Git-Repository für die Speicherung der Verbindungsdaten und überträgt alle Änderungen in dieses Repository. Beachte, dass dafür git installiert sein muss und dass dies die Lade- und Speichervorgänge verlangsamen kann.\n\nAlle Kategorien, die synchronisiert werden sollen, müssen explizit als gemeinsam genutzt gekennzeichnet werden.\n\nErfordert einen Neustart, um angewendet zu werden. +storageGitRemote=Git Remote URL +storageGitRemoteDescription=Wenn diese Option gesetzt ist, zieht XPipe beim Laden automatisch alle Änderungen und überträgt sie beim Speichern an das entfernte Repository. So kannst du deine Konfigurationsdaten zwischen mehreren XPipe-Installationen austauschen. Es werden sowohl HTTP- als auch SSH-URLs unterstützt. Beachte, dass dies die Lade- und Speichervorgänge verlangsamen kann.\n\nErfordert einen Neustart zur Anwendung. +vault=Tresor +workspaceLockDescription=Legt ein benutzerdefiniertes Passwort fest, um alle in XPipe gespeicherten sensiblen Daten zu verschlüsseln. Dies erhöht die Sicherheit, da es eine zusätzliche Verschlüsselungsebene für deine gespeicherten sensiblen Daten bietet. Du wirst dann beim Start von XPipe aufgefordert, das Passwort einzugeben. +useSystemFontDescription=Legt fest, ob deine Systemschriftart oder die von XPipe verwendete Standardschriftart (Roboto) verwendet werden soll. +tooltipDelay=Tooltip-Verzögerung +tooltipDelayDescription=Die Anzahl der Millisekunden, die gewartet wird, bis ein Tooltip angezeigt wird. +fontSize=Schriftgröße +windowOptions=Fensteroptionen +saveWindowLocation=Speicherort des Fensters +saveWindowLocationDescription=Legt fest, ob die Fensterkoordinaten gespeichert und bei Neustarts wiederhergestellt werden sollen. +startupShutdown=Starten / Herunterfahren +showChildCategoriesInParentCategory=Unterkategorien in der übergeordneten Kategorie anzeigen +showChildCategoriesInParentCategoryDescription=Ob alle Verbindungen, die sich in Unterkategorien befinden, einbezogen werden sollen, wenn eine bestimmte übergeordnete Kategorie ausgewählt wird.\n\nWenn dies deaktiviert ist, verhalten sich die Kategorien eher wie klassische Ordner, die nur ihren direkten Inhalt zeigen, ohne Unterordner einzubeziehen. +condenseConnectionDisplay=Verbindungsanzeige verdichten +condenseConnectionDisplayDescription=Nimm für jede Verbindung der obersten Ebene weniger Platz in der Vertikalen ein, um die Verbindungsliste zu komprimieren. +enforceWindowModality=Fenstermodalität erzwingen +enforceWindowModalityDescription=Bewirkt, dass sekundäre Fenster, wie z. B. das Dialogfeld zum Herstellen einer Verbindung, alle Eingaben für das Hauptfenster blockieren, solange sie geöffnet sind. Das ist nützlich, wenn du manchmal falsch klickst. +openConnectionSearchWindowOnConnectionCreation=Fenster für die Verbindungssuche bei der Verbindungserstellung öffnen +openConnectionSearchWindowOnConnectionCreationDescription=Ob beim Hinzufügen einer neuen Shell-Verbindung automatisch das Fenster zur Suche nach verfügbaren Unterverbindungen geöffnet werden soll oder nicht. +workflow=Workflow +system=System +application=Anwendung +storage=Speicherung +runOnStartup=Beim Starten ausführen +closeBehaviour=Verhalten schließen +closeBehaviourDescription=Legt fest, wie XPipe beim Schließen des Hauptfensters vorgehen soll. +language=Sprache +languageDescription=Die zu verwendende Anzeigesprache.\n\nBeachte, dass diese automatische Übersetzungen als Basis verwenden und über Beiträge angepasst werden. +lightTheme=Licht-Thema +darkTheme=Dunkles Thema +exit=XPipe beenden +continueInBackground=Weiter im Hintergrund +minimizeToTray=In die Taskleiste minimieren +closeBehaviourAlertTitle=Schließverhalten einstellen +closeBehaviourAlertTitleHeader=Wähle aus, was beim Schließen des Fensters passieren soll. Alle aktiven Verbindungen werden geschlossen, wenn die Anwendung heruntergefahren wird. +startupBehaviour=Startverhalten +startupBehaviourDescription=Steuert das Standardverhalten der Desktop-Anwendung, wenn XPipe gestartet wird. +clearCachesAlertTitle=Cache säubern +clearCachesAlertTitleHeader=Willst du alle XPipe-Caches löschen? +clearCachesAlertTitleContent=Beachte, dass dadurch alle Daten gelöscht werden, die zur Verbesserung des Nutzererlebnisses gespeichert wurden. +startGui=GUI starten +startInTray=Start im Tray +startInBackground=Start im Hintergrund +clearCaches=Caches löschen ... +clearCachesDescription=Alle Cache-Daten löschen +apply=Anwenden +cancel=Abbrechen +notAnAbsolutePath=Kein absoluter Pfad +notADirectory=Nicht ein Verzeichnis +notAnEmptyDirectory=Kein leeres Verzeichnis +automaticallyUpdate=Nach Updates suchen +automaticallyUpdateDescription=Wenn diese Funktion aktiviert ist, werden Informationen über neue Versionen automatisch abgerufen, während XPipe läuft. Es wird kein Updater im Hintergrund ausgeführt, und du musst die Installation von Updates immer noch explizit bestätigen. +sendAnonymousErrorReports=Anonyme Fehlerberichte senden +sendUsageStatistics=Anonyme Nutzungsstatistiken senden +storageDirectory=Speicherverzeichnis +storageDirectoryDescription=Der Ort, an dem XPipe alle Verbindungsinformationen speichern soll. Diese Einstellung wird erst beim nächsten Neustart übernommen. Wenn du diese Einstellung änderst, werden die Daten aus dem alten Verzeichnis nicht in das neue kopiert. +logLevel=Log-Level +appBehaviour=Verhalten der Anwendung +logLevelDescription=Die Protokollstufe, die beim Schreiben von Protokolldateien verwendet werden sollte. +developerMode=Entwickler-Modus +developerModeDescription=Wenn diese Option aktiviert ist, hast du Zugriff auf eine Reihe von zusätzlichen Optionen, die für die Entwicklung nützlich sind. Nur aktiv nach einem Neustart. +editor=Editor +custom=Benutzerdefiniert +passwordManagerCommand=Passwortmanager-Befehl +passwordManagerCommandDescription=Der Befehl, der ausgeführt werden soll, um Passwörter abzurufen. Der Platzhalterstring $KEY wird beim Aufruf durch den zitierten Passwortschlüssel ersetzt. Dies sollte deinen Passwortmanager CLI aufrufen, um das Passwort auf stdout auszugeben, z.B. mypassmgr get $KEY.\n\nDann kannst du den Schlüssel so einstellen, dass er immer dann abgerufen wird, wenn du eine Verbindung aufbaust, die ein Passwort erfordert. +passwordManagerCommandTest=Passwort-Manager testen +passwordManagerCommandTestDescription=Du kannst hier testen, ob die Ausgabe korrekt aussieht, wenn du einen Passwortmanager-Befehl eingerichtet hast. Der Befehl sollte nur das Passwort selbst auf stdout ausgeben, keine andere Formatierung sollte in der Ausgabe enthalten sein. +preferEditorTabs=Lieber neue Tabs öffnen +preferEditorTabsDescription=Legt fest, ob XPipe versuchen soll, neue Tabs in dem von dir gewählten Editor zu öffnen, anstatt neue Fenster zu öffnen.\n\nBeachte, dass nicht jeder Editor dies unterstützt. +customEditorCommand=Benutzerdefinierter Editor-Befehl +customEditorCommandDescription=Der Befehl, der ausgeführt werden soll, um den benutzerdefinierten Editor zu starten.\n\nDie Platzhalterzeichenfolge $FILE wird beim Aufruf durch den absoluten Dateinamen in Anführungszeichen ersetzt. Denke daran, den ausführbaren Pfad deines Editors in Anführungszeichen zu setzen, wenn er Leerzeichen enthält. +editorReloadTimeout=Zeitüberschreitung beim Neuladen des Editors +editorReloadTimeoutDescription=Die Anzahl der Millisekunden, die gewartet wird, bevor eine Datei nach einer Aktualisierung gelesen wird. Dadurch werden Probleme vermieden, wenn dein Editor beim Schreiben oder Freigeben von Dateisperren langsam ist. +encryptAllVaultData=Alle Tresordaten verschlüsseln +encryptAllVaultDataDescription=Wenn diese Option aktiviert ist, wird jeder Teil der Verbindungsdaten im Tresor verschlüsselt und nicht nur die Geheimnisse innerhalb der Daten. Dadurch wird eine weitere Sicherheitsebene für andere Parameter wie Benutzernamen, Hostnamen usw. geschaffen, die im Tresor standardmäßig nicht verschlüsselt sind.\n\nDiese Option macht den Verlauf und die Diffs deines Git-Tresors unbrauchbar, da du die ursprünglichen Änderungen nicht mehr sehen kannst, sondern nur noch die binären Änderungen. +vaultSecurity=Tresor-Sicherheit +developerDisableUpdateVersionCheck=Update-Versionsprüfung deaktivieren +developerDisableUpdateVersionCheckDescription=Legt fest, ob der Update-Checker die Versionsnummer bei der Suche nach einem Update ignorieren soll. +developerDisableGuiRestrictions=GUI-Einschränkungen deaktivieren +developerDisableGuiRestrictionsDescription=Steuert, ob bestimmte deaktivierte Aktionen noch über die Benutzeroberfläche ausgeführt werden können. +developerShowHiddenEntries=Versteckte Einträge anzeigen +developerShowHiddenEntriesDescription=Wenn aktiviert, werden versteckte und interne Datenquellen angezeigt. +developerShowHiddenProviders=Versteckte Anbieter anzeigen +developerShowHiddenProvidersDescription=Legt fest, ob versteckte und interne Verbindungs- und Datenquellenanbieter im Erstellungsdialog angezeigt werden sollen. +developerDisableConnectorInstallationVersionCheck=Connector-Versionsprüfung deaktivieren +developerDisableConnectorInstallationVersionCheckDescription=Legt fest, ob der Update-Checker die Versionsnummer ignoriert, wenn er die Version eines XPipe-Anschlusses prüft, der auf einem entfernten Computer installiert ist. +shellCommandTest=Shell-Befehlstest +shellCommandTestDescription=Führe einen Befehl in der Shell-Sitzung aus, die intern von XPipe verwendet wird. +terminal=Terminal +terminalEmulator=Terminal-Emulator +terminalConfiguration=Terminal-Konfiguration +editorConfiguration=Editor-Konfiguration +defaultApplication=Standardanwendung +terminalEmulatorDescription=Das Standardterminal, das beim Öffnen einer beliebigen Shell-Verbindung verwendet wird. Diese Anwendung wird nur zu Anzeigezwecken verwendet, das gestartete Shell-Programm hängt von der Shell-Verbindung selbst ab. +program=Programm +customTerminalCommand=Benutzerdefinierter Terminalbefehl +customTerminalCommandDescription=Der auszuführende Befehl, um das benutzerdefinierte Terminal mit einem bestimmten Befehl zu öffnen.\n\nXPipe erstellt ein temporäres Launcher-Shell-Skript für dein Terminal, das ausgeführt wird. Die Platzhalterzeichenfolge $CMD in dem von dir angegebenen Befehl wird beim Aufruf durch das eigentliche Launcher-Skript ersetzt. Denke daran, den ausführbaren Pfad deines Terminals in Anführungszeichen zu setzen, wenn er Leerzeichen enthält. +preferTerminalTabs=Lieber neue Tabs öffnen +preferTerminalTabsDisabled=Ziehe es vor, neue Tabs zu öffnen.\n\n Das aktuell ausgewählte Terminal $TERM$ unterstützt das Öffnen von Tabs über das CLI nicht. +preferTerminalTabsDescription=Legt fest, ob XPipe versucht, neue Tabs in dem von dir gewählten Terminal zu öffnen, anstatt neue Fenster zu öffnen.\n\nBeachte, dass nicht jedes Terminal dies unterstützt. +clearTerminalOnInit=Terminal bei Init löschen +clearTerminalOnInitDescription=Wenn diese Funktion aktiviert ist, führt XPipe einen Löschbefehl aus, wenn eine neue Terminalsitzung gestartet wird, um unnötige Ausgaben zu entfernen. +enableFastTerminalStartup=Schnelles Starten des Terminals aktivieren +enableFastTerminalStartupDescription=Wenn diese Option aktiviert ist, werden Terminalsitzungen nach Möglichkeit schneller gestartet.\n\n Dadurch werden einige Startprüfungen übersprungen und die angezeigten Systeminformationen nicht aktualisiert. Eventuelle Verbindungsfehler werden nur im Terminal angezeigt. +dontCachePasswords=Aufgeforderte Passwörter nicht zwischenspeichern +dontCachePasswordsDescription=Legt fest, ob abgefragte Passwörter von XPipe intern zwischengespeichert werden sollen, damit du sie in der aktuellen Sitzung nicht erneut eingeben musst.\n\nIst dieses Verhalten deaktiviert, musst du die abgefragten Anmeldedaten jedes Mal neu eingeben, wenn sie vom System verlangt werden. +denyTempScriptCreation=Temporäre Skripterstellung verweigern +denyTempScriptCreationDescription=Um einige seiner Funktionen zu realisieren, erstellt XPipe manchmal temporäre Shell-Skripte auf einem Zielsystem, um die einfache Ausführung einfacher Befehle zu ermöglichen. Diese enthalten keine sensiblen Informationen und werden nur zu Implementierungszwecken erstellt.\n\nWenn dieses Verhalten deaktiviert ist, erstellt XPipe keine temporären Dateien auf einem entfernten System. Diese Option ist in hochsicheren Kontexten nützlich, in denen jede Dateisystemänderung überwacht wird. Wenn diese Option deaktiviert ist, funktionieren einige Funktionen, z. B. Shell-Umgebungen und Skripte, nicht wie vorgesehen. +disableCertutilUse=Die Verwendung von certutil unter Windows deaktivieren +useLocalFallbackShell=Lokale Fallback-Shell verwenden +useLocalFallbackShellDescription=Wechsle zu einer anderen lokalen Shell, um lokale Vorgänge zu bearbeiten. Unter Windows ist dies die PowerShell, auf anderen Systemen die Bourne Shell.\n\nDiese Option kann verwendet werden, wenn die normale lokale Standard-Shell deaktiviert oder bis zu einem gewissen Grad beschädigt ist. Einige Funktionen funktionieren möglicherweise nicht wie erwartet, wenn diese Option aktiviert ist.\n\nDie Anwendung erfordert einen Neustart. +disableCertutilUseDescription=Aufgrund verschiedener Unzulänglichkeiten und Bugs in cmd.exe werden temporäre Shell-Skripte mit certutil erstellt, indem es zur Dekodierung von base64-Eingaben verwendet wird, da cmd.exe bei Nicht-ASCII-Eingaben versagt. XPipe kann dafür auch die PowerShell verwenden, aber das ist langsamer.\n\nDamit wird die Verwendung von certutil auf Windows-Systemen deaktiviert, um einige Funktionen zu realisieren und stattdessen auf die PowerShell zurückzugreifen. Das könnte einige AVs freuen, da einige von ihnen die Verwendung von certutil blockieren. +disableTerminalRemotePasswordPreparation=Terminal-Fernpasswortvorbereitung deaktivieren +disableTerminalRemotePasswordPreparationDescription=In Situationen, in denen eine Remote-Shell-Verbindung, die über mehrere Zwischensysteme geht, im Terminal aufgebaut werden soll, kann es erforderlich sein, alle erforderlichen Kennwörter auf einem der Zwischensysteme vorzubereiten, damit alle Abfragen automatisch ausgefüllt werden können.\n\nWenn du nicht möchtest, dass die Kennwörter jemals an ein Zwischensystem übertragen werden, kannst du dieses Verhalten deaktivieren. Jedes erforderliche Zwischenpasswort wird dann beim Öffnen im Terminal selbst abgefragt. +more=Mehr diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties b/lang/app/strings/translations_en.properties similarity index 56% rename from app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties rename to lang/app/strings/translations_en.properties index 202d35c5c..98c3ae992 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties +++ b/lang/app/strings/translations_en.properties @@ -1,21 +1,281 @@ +delete=Delete +rename=Rename +properties=Properties +usedDate=Used $DATE$ +openDir=Open Directory +sortLastUsed=Sort by last used date +sortAlphabetical=Sort alphabetical by name +restart=Restart XPipe +restartDescription=A restart can often be a quick fix +reportIssue=Report Issue +reportIssueDescription=Open the integrated issue reporter +usefulActions=Useful actions +stored=Saved +troubleshootingOptions=Troubleshooting tools +troubleshoot=Troubleshoot +remote=Remote File +addShellStore=Add Shell ... +addShellTitle=Add Shell Connection +savedConnections=Saved Connections +save=Save +#context: verb +clean=Clean +refresh=Refresh +moveTo=Move to ... +addDatabase=Database ... +browseInternalStorage=Browse internal storage +addTunnel=Tunnel ... +addScript=Script ... +addHost=Remote Host ... +addShell=Shell Environment ... +addCommand=Custom Command ... +addAutomatically=Search Automatically ... +addOther=Add Other ... +addConnection=Add Connection +skip=Skip +addConnections=New +selectType=Select Type +selectTypeDescription=Select connection type +selectShellType=Shell Type +selectShellTypeDescription=Select the Type of the Shell Connection +name=Name +storeIntroTitle=Connection Hub +storeIntroDescription=Here you can manage all your local and remote shell connections in one place. To start off, you can quickly detect available connections automatically and choose which ones to add. +detectConnections=Search for connections +configuration=Configuration +dragAndDropFilesHere=Or just drag and drop a file here +confirmDsCreationAbortTitle=Confirm abort +confirmDsCreationAbortHeader=Do you want to abort the data source creation? +confirmDsCreationAbortContent=Any data source creation progress will be lost. +confirmInvalidStoreTitle=Failed connection +confirmInvalidStoreHeader=Do you want to skip connection validation? +confirmInvalidStoreContent=You can add this connection even if it could not be validated and fix the connection problems later on. +none=None +expand=Expand +accessSubConnections=Access sub connections +#context: noun, not rare +common=Common +color=Color +alwaysConfirmElevation=Always confirm elevation +alwaysConfirmElevationDescription=Controls how to handle cases when elevated access is required to run a command on a system, e.g. with sudo.\n\nBy default, any sudo credentials are cached during a session and automatically provided when needed. If this option is enabled, it will ask you to confirm the elevation access every time. +allow=Allow +ask=Ask +deny=Deny +share=Add to git repository +unshare=Remove from git repository +remove=Remove +newCategory=New subcategory +passwordManager=Password manager +prompt=Prompt +customCommand=Custom command +other=Other +setLock=Set lock +selectConnection=Select connection +changeLock=Change passphrase +test=Test +lockCreationAlertTitle=Set passphrase +lockCreationAlertHeader=Set your new master passphrase +#context: verb, exit +finish=Finish +error=An error occurred +downloadStageDescription=Downloads files to your local machine, so you can drag and drop them into your native desktop environment. +ok=Ok +search=Search +newFile=New file +newDirectory=New directory +passphrase=Passphrase +repeatPassphrase=Repeat passphrase +password=Password +unlockAlertTitle=Unlock workspace +unlockAlertHeader=Enter your vault passphrase to continue +enterLockPassword=Enter lock password +repeatPassword=Repeat password +askpassAlertTitle=Askpass +unsupportedOperation=Unsupported operation: $MSG$ +fileConflictAlertTitle=Resolve conflict +fileConflictAlertHeader=A conflict was encountered. How would you like to proceed? +fileConflictAlertContent=The file $FILE$ does already exist on the target system. +fileConflictAlertContentMultiple=The file $FILE$ already exists. There might be more conflicts that you can automatically resolve by choosing an option that applies to all. +moveAlertTitle=Confirm move +moveAlertHeader=Do you want to move the ($COUNT$) selected elements into $TARGET$? +deleteAlertTitle=Confirm deletion +deleteAlertHeader=Do you want to delete the ($COUNT$) selected elements? +selectedElements=Selected elements: +mustNotBeEmpty=$VALUE$ must not be empty +valueMustNotBeEmpty=Value must not be empty +transferDescription=Drop files to transfer +dragFiles=Drag files within browser +dragLocalFiles=Drag local files from here +null=$VALUE$ must be not null +roots=Roots +scripts=Scripts +searchFilter=Search ... +#context: last used +recent=Recent +shortcut=Shortcut +browserWelcomeEmpty=Here you will be able to see where you left off last time. +browserWelcomeSystems=You were recently connected to the following systems: +hostFeatureUnsupported=$FEATURE$ is not installed on the host +missingStore=$NAME$ does not exist +connectionName=Connection name +connectionNameDescription=Give this connection a custom name +openFileTitle=Open file +unknown=Unknown +scanAlertTitle=Add connections +scanAlertChoiceHeader=Target +scanAlertChoiceHeaderDescription=Choose where to search for connections. This will look for all available connections first. +scanAlertHeader=Connection types +scanAlertHeaderDescription=Select types of connections you want to automatically add for the system. +noInformationAvailable=No information available +localMachine=Local Machine +yes=Yes +no=No +errorOccured=An error occured +terminalErrorOccured=A terminal error occured +errorTypeOccured=An exception of type $TYPE$ was thrown +permissionsAlertTitle=Permissions required +permissionsAlertHeader=Additional permissions are required to perform this operation. +permissionsAlertContent=Please follow the pop-up to give XPipe the required permissions in the settings menu. +errorDetails=Show details +updateReadyAlertTitle=Update Ready +updateReadyAlertHeader=An update to version $VERSION$ is ready to be installed +updateReadyAlertContent=This will install the new version and restart XPipe once the installation finished. +errorNoDetail=No error details are available +updateAvailableTitle=Update Available +updateAvailableHeader=An XPipe update to version $VERSION$ is available to install +updateAvailableContent=Even though XPipe could not be started, you can attempt to install the update to potentially fix the issue. +clipboardActionDetectedTitle=Clipboard Action detected +clipboardActionDetectedHeader=Do you want to import your clipboard content? +clipboardActionDetectedContent=XPipe detected content in your clipboard that can be opened. Do you want to open it now? +install=Install ... +ignore=Ignore +possibleActions=Possible actions +reportError=Report error +reportOnGithub=Report on GitHub +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=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 +sendReport=Send report +errorHandler=Error handler +events=Events +method=Method +validate=Validate +stackTrace=Stack trace +previousStep=< Previous +nextStep=Next > +finishStep=Finish +edit=Edit +browseInternal=Browse Internal +checkOutUpdate=Check out update +open=Open +quit=Quit +noTerminalSet=No terminal application has been set automatically. You can do so manually in the settings menu. +connections=Connections +settings=Settings +explorePlans=License +help=Help +about=About +developer=Developer +browseFileTitle=Browse file +browse=Browse +browser=Browser +selectFileFromComputer=Select a file from this computer +links=Useful links +website=Website +documentation=Documentation +discordDescription=Join the Discord server +security=Security +securityPolicy=Security information +securityPolicyDescription=Read the detailed security policy +privacy=Privacy Policy +privacyDescription=Read the privacy policy for the XPipe application +slackDescription=Join the Slack workspace +support=Support +githubDescription=Check out the GitHub repository +openSourceNotices=Open Source Notices +xPipeClient=XPipe Desktop +checkForUpdates=Check for updates +checkForUpdatesDescription=Download an update if there is one +lastChecked=Last checked +version=Version +build=Build version +runtimeVersion=Runtime version +virtualMachine=Virtual machine +updateReady=Install update +updateReadyPortable=Check out update +updateReadyDescription=An update was downloaded and is ready to be installed +updateReadyDescriptionPortable=An update is available to download +updateRestart=Restart to update +never=Never +updateAvailableTooltip=Update available +visitGithubRepository=Visit GitHub repository +updateAvailable=Update available: $VERSION$ +downloadUpdate=Download update +legalAccept=I accept the End User License Agreement +#context: verb +confirm=Confirm +#context: verb +print=Print +whatsNew=What's new in version $VERSION$ ($DATE$) +antivirusNoticeTitle=A note on Antivirus programs +updateChangelogAlertTitle=Changelog +greetingsAlertTitle=Welcome to XPipe +#context: understood +gotIt=Got It +eula=End User License Agreement +news=News +introduction=Introduction +privacyPolicy=Privacy Policy +agree=Agree +disagree=Disagree +directories=Directories +logFile=Log File +logFiles=Log Files +logFilesAttachment=Log Files +issueReporter=Issue Reporter +openCurrentLogFile=Log files +openCurrentLogFileDescription=Open the log file of the current session +openLogsDirectory=Open logs directory +installationFiles=Installation Files +openInstallationDirectory=Installation files +openInstallationDirectoryDescription=Open XPipe installation directory +launchDebugMode=Debug mode +launchDebugModeDescription=Restart XPipe in debug mode +extensionInstallTitle=Download +extensionInstallDescription=This action requires additional third party libraries that are not distributed by XPipe. You can automatically install them here. The components are then downloaded from the vendor website: +extensionInstallLicenseNote=By performing the download and automatic installation you agree to the terms of the third party licenses: +license=License +installRequired=Installation Required +restore=Restore +restoreAllSessions=Restore all sessions connectionTimeout=Connection start timeout connectionTimeoutDescription=The time in seconds to wait for a response before considering a connection to be timed out. If some of your remote systems take long to connect, you can try to increase this value. useBundledTools=Use bundled OpenSSH tools useBundledToolsDescription=Prefer to use bundled version of the openssh client instead of your locally installed one.\n\nThis version is usually more up-to-date than the ones shipped on your system and might support additional features. This also removes the requirement to have these tools installed in the first place.\n\nRequires restart to apply. -connections=Connections appearance=Appearance integrations=Integrations uiOptions=UI Options +#context: display theme theme=Theme +rdp=Remote desktop +rdpConfiguration=Remote desktop configuration +rdpClient=RDP client +rdpClientDescription=The RDP client program to call when launching RDP connections.\n\nNote that various clients have different degrees of abilities and integrations. Some clients don't support passing passwords automatically, so you still have to fill them in on launch. localShell=Local shell -themeDescription=You preferred theme +#context: display theme +themeDescription=Your preferred display theme dontAutomaticallyStartVmSshServer=Don't automatically start SSH server for VMs when needed dontAutomaticallyStartVmSshServerDescription=Any shell connection to a VM running in a hypervisor is made through SSH. XPipe can automatically start the installed SSH server when needed. If you don't want this for security reasons, then you can just disable this behavior with this option. confirmGitShareTitle=Confirm git sharing confirmGitShareHeader=This will copy the file into your git vault and commit your changes. Do you want to continue? gitShareFileTooltip=Add file to the git vault data directory so that it is automatically synced.\n\nThis action can only be used when the git vault is enabled in the settings. performanceMode=Performance mode -performanceModeDescription=Disables all visual effects that are not required to improve the application performance. +performanceModeDescription=Disables all visual effects that are not required in order to improve the application performance. dontAcceptNewHostKeys=Don't accept new SSH host keys automatically dontAcceptNewHostKeysDescription=XPipe will automatically accept host keys by default from systems where your SSH client has no known host key already saved. If any known host key has changed however, it will refuse to connect unless you accept the new one.\n\nDisabling this behavior allows you to check all host keys, even if there is no conflict initially. uiScale=UI Scale @@ -23,8 +283,6 @@ uiScaleDescription=A custom scaling value that can be set independently of your editorProgram=Editor Program editorProgramDescription=The default text editor to use when editing any kind of text data. windowOpacity=Window opacity -customTerminalPlaceholder=myterminal -e $CMD -customEditorPlaceholder=myeditor $FILE windowOpacityDescription=Changes the window opacity to keep track of what is happening in the background. useSystemFont=Use system font openDataDir=Vault data directory @@ -66,14 +324,16 @@ openConnectionSearchWindowOnConnectionCreationDescription=Whether or not to auto workflow=Workflow system=System application=Application -updateToPrereleases=Include prereleases -updateToPrereleasesDescription=When enabled, the update check will also look for available prereleases in addition to full releases. storage=Storage runOnStartup=Run on startup +#context: setting closeBehaviour=Close behaviour closeBehaviourDescription=Controls how XPipe should proceed upon closing its main window. language=Language +languageDescription=The display language to use.\n\nNote that these use automatic translations as a base and are manually fixed and improved by contributors. You can also help the translation effort by submitting translation fixes on GitHub. +#context: display theme lightTheme=Light Theme +#context: display theme darkTheme=Dark Theme exit=Quit XPipe continueInBackground=Continue in background @@ -90,7 +350,6 @@ startInTray=Start in tray startInBackground=Start in background clearCaches=Clear caches ... clearCachesDescription=Delete all cache data -ok=OK apply=Apply cancel=Cancel notAnAbsolutePath=Not an absolute path @@ -115,6 +374,8 @@ passwordManagerCommandTest=Test password manager passwordManagerCommandTestDescription=You can test here whether the output looks correct if you have set up a password manager command. The command should only output the password itself to stdout, no other formatting should be included in the output. preferEditorTabs=Prefer to open new tabs preferEditorTabsDescription=Controls whether XPipe will try to open new tabs in your chosen editor instead of new windows.\n\nNote that not every editor supports this. +customRdpClientCommand=Custom command +customRdpClientCommandDescription=The command to execute to start the custom RDP client.\n\nThe placeholder string $FILE will be replaced by the quoted absolute .rdp file name when called. Remember to quote your executable path if it contains spaces. customEditorCommand=Custom editor command customEditorCommandDescription=The command to execute to start the custom editor.\n\nThe placeholder string $FILE will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces. editorReloadTimeout=Editor reload timeout @@ -122,13 +383,6 @@ editorReloadTimeoutDescription=The amount of milliseconds to wait before reading encryptAllVaultData=Encrypt all vault data encryptAllVaultDataDescription=When enabled, every part of the vault connection data will be encrypted as opposed to only secrets within in that data. This adds another layer of security for other parameters like usernames, hostnames, etc., that are not encrypted by default in the vault.\n\nThis option will render your git vault history and diffs useless as you can't see the original changes anymore, only binary changes. vaultSecurity=Vault security -securityPolicy=Security policy -notepad++=Notepad++ -notepad++Windows=Notepad++ -notepad++Linux=Notepad++ -notepad=Notepad -security=Security -developer=Developer developerDisableUpdateVersionCheck=Disable Update Version Check developerDisableUpdateVersionCheckDescription=Controls whether the update checker will ignore the version number when looking for an update. developerDisableGuiRestrictions=Disable GUI restrictions @@ -141,35 +395,15 @@ developerDisableConnectorInstallationVersionCheck=Disable Connector Version Chec developerDisableConnectorInstallationVersionCheckDescription=Controls whether the update checker will ignore the version number when inspecting the version of an XPipe connector installed on a remote machine. shellCommandTest=Shell Command Test shellCommandTestDescription=Run a command in the shell session used internally by XPipe. -konsole=Konsole -xfce=Xfce 4 -elementaryTerminal=Elementary Terminal -macosTerminal=Terminal.app -iterm2=iTerm2 -warp=Warp -tabby=Tabby -alacritty=Alacritty -alacrittyMacOs=Alacritty -kittyMacOs=Kitty -bbedit=BBEdit -fleet=Fleet -intellij=IntelliJ IDEA -pycharm=PyCharm -webstorm=WebStorm -clion=CLion -tabbyMacOs=Tabby terminal=Terminal terminalEmulator=Terminal emulator terminalConfiguration=Terminal configuration editorConfiguration=Editor configuration defaultApplication=Default application -terminalEmulatorDescription=The default terminal to use when opening any kind of shell connection. This application is only used for display purposes, the started shell program depends on the shell connection itself. +terminalEmulatorDescription=The default terminal to use when opening any kind of shell connection. This application is only used for display purposes, the started shell program depends on the shell connection itself.\n\nThe level of feature support varies by terminal, that is why each one is marked as either recommended or not recommended. All non-recommended terminals work with XPipe but might lack features like tabs, title colors, shell support, and more. Your user experience will be best when using a recommended terminal. program=Program customTerminalCommand=Custom terminal command customTerminalCommandDescription=The command to execute to open the custom terminal with a given command.\n\nXPipe will create a temporary launcher shell script for your terminal to execute. The placeholder string $CMD in the command you supply will be replaced by the actual launcher script when called. Remember to quote your terminal executable path if it contains spaces. -preferTerminalTabs=Prefer to open new tabs -preferTerminalTabsDisabled=Prefer to open new tabs.\n\nThe currently selected terminal $TERM$ does not support opening tabs from the CLI. -preferTerminalTabsDescription=Controls whether XPipe will try to open new tabs in your chosen terminal instead of new windows.\n\nNote that not every terminal supports this. clearTerminalOnInit=Clear terminal on init clearTerminalOnInitDescription=When enabled, XPipe will run a clear command when a new terminal session is launched to remove any unnecessary output. enableFastTerminalStartup=Enable fast terminal startup @@ -184,12 +418,13 @@ useLocalFallbackShellDescription=Switch to using another local shell to handle l disableCertutilUseDescription=Due to several shortcomings and bugs in cmd.exe, temporary shell scripts are created with certutil by using it to decode base64 input as cmd.exe breaks on non-ASCII input. XPipe can also use PowerShell for that but this will be slower.\n\nThis disables any use of certutil on Windows systems to realize some functionality and fall back to PowerShell instead. This might please some AVs as some of them block certutil usage. disableTerminalRemotePasswordPreparation=Disable terminal remote password preparation disableTerminalRemotePasswordPreparationDescription=In situations where a remote shell connection that goes through multiple intermediate systems should be established in the terminal, there might be a requirement to prepare any required passwords on one of the intermediate systems to allow for an automatic filling of any prompts.\n\nIf you don't want the passwords to ever be transferred to any intermediate system, you can disable this behavior. Any required intermediate password will then be queried in the terminal itself when opened. -cmd=cmd.exe -powershell=Powershell -pwsh=Powershell Core -windowsTerminal=Windows Terminal -windowsTerminalPreview=Windows Terminal Preview -gnomeTerminal=Gnome Terminal -createLock=Create lock -tilix=Tilix -wezterm=WezTerm +more=More +translate=Translate +allConnections=All connections +allScripts=All scripts +predefined=Predefined +default=Default +goodMorning=Good morning +goodAfternoon=Good afternoon +goodNight=Good night +addVisual=Visual ... diff --git a/lang/base/strings/translations_de.properties b/lang/base/strings/translations_de.properties new file mode 100644 index 000000000..f8bbfce0a --- /dev/null +++ b/lang/base/strings/translations_de.properties @@ -0,0 +1,76 @@ +localMachine=Lokale Maschine +destination=Ziel +configuration=Konfiguration +launch=Starten +start=Start +stop=Stopp +pause=Pause +refresh=Aktualisieren +options=Optionen +newFile=Neue Datei +newLink=Neuer Link +linkName=Link-Name +scanConnections=Verfügbare Verbindungen finden ... +observe=Beobachten beginnen +stopObserve=Beobachten stoppen +createShortcut=Desktop-Verknüpfung erstellen +browseFiles=Dateien durchsuchen +clone=Klonen +targetPath=Zielpfad +newDirectory=Neues Verzeichnis +copyShareLink=Link kopieren +selectStore=Laden auswählen +saveSource=Für später speichern +execute=Führen Sie aus +deleteChildren=Alle Kinder entfernen +descriptionDescription=Gib dieser Gruppe eine optionale Beschreibung +selectSource=Quelle auswählen +commandLineRead=Aktualisieren +commandLineWrite=Schreibe +wslHost=WSL-Host +timeout=Timeout +additionalOptions=Zusätzliche Optionen +type=Typ +input=Eingabe +machine=Maschine +container=Container +host=Host +port=Port +user=Benutzer +password=Passwort +method=Methode +uri=URL +distribution=Vertrieb +username=Benutzername +shellType=Shell-Typ +command=Befehl +browseFile=Datei durchsuchen +openShell=Shell öffnen +editFile=Datei bearbeiten +usage=Verwendung +description=Beschreibung +open=Öffnen +edit=Bearbeiten +scriptContents=Skript-Inhalte +scriptContentsDescription=Die auszuführenden Skriptbefehle +snippets=Skript-Abhängigkeiten +snippetsDescription=Andere Skripte, die zuerst ausgeführt werden sollen +snippetsDependenciesDescription=Alle möglichen Skripte, die ggf. ausgeführt werden sollten +isDefault=Wird in allen kompatiblen Shells auf init ausgeführt +bringToShells=An alle kompatiblen Shells bringen +isDefaultGroup=Alle Gruppenskripte auf der Shell init ausführen +executionType=Ausführungsart +executionTypeDescription=Wann dieses Snippet ausgeführt werden soll +minimumShellDialect=Shell-Typ +minimumShellDialectDescription=Der erforderliche Shell-Typ für dieses Skript +dumbOnly=Nur stumm +terminalOnly=Nur Terminal +both=Beide +shouldElevate=Sollte erheben +shouldElevateDescription=Ob dieses Skript mit erhöhten Rechten ausgeführt werden soll +script.displayName=Skript +script.displayDescription=Ein wiederverwendbares Skript erstellen +scriptGroup.displayName=Skript-Gruppe +scriptGroup.displayDescription=Eine Gruppe für Skripte erstellen +scriptGroup=Gruppe +scriptGroupDescription=Die Gruppe, der dieses Skript zugewiesen werden soll diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties b/lang/base/strings/translations_en.properties similarity index 74% rename from ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties rename to lang/base/strings/translations_en.properties index acac0433c..4dcd57085 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties +++ b/lang/base/strings/translations_en.properties @@ -1,13 +1,6 @@ -anyBinaryFile=Any binary file -dataFile=Data file -binaryFile=Binary file -commandLine=Command Line -java=Java -fileOutput=File Output localMachine=Local Machine destination=Destination configuration=Configuration -selectOutput=Select Output launch=Launch start=Start stop=Stop @@ -37,12 +30,9 @@ commandLineWrite=Write wslHost=WSL Host timeout=Timeout additionalOptions=Additional Options -rawFileOutput=Raw File Output -dataSourceOutput=Data Source Output type=Type input=Input machine=Machine -bytes=$N$ bytes container=Container host=Host port=Port @@ -52,24 +42,14 @@ method=Method uri=URL distribution=Distribution username=Username -shellType=Shell Type +shellType=Shell type command=Command -target=Target -writeMode=Write Mode -exportStream=Export Stream -browseFile=Browse File -openShell=Open Shell -editFile=Edit File +browseFile=Browse file +openShell=Open shell +editFile=Edit file usage=Usage description=Description -unconnected=Unconnected -waitingForConsumer=Waiting for Consumer -waitingForProducer=Waiting for Producer open=Open -closed=Closed -internalStream.displayName=Internal Stream -local.displayName=Local machine -local.displayDescription= edit=Edit scriptContents=Script contents scriptContentsDescription=The script commands to execute diff --git a/lang/base/texts/elevation_de.md b/lang/base/texts/elevation_de.md new file mode 100644 index 000000000..e71427d4a --- /dev/null +++ b/lang/base/texts/elevation_de.md @@ -0,0 +1,14 @@ +## Elevation + +Der Prozess der Elevation ist betriebssystemspezifisch. + +### Linux & macOS + +Jeder erhobene Befehl wird mit `sudo` ausgeführt. Das optionale `sudo` Passwort wird bei Bedarf über XPipe abgefragt. +Du kannst in den Einstellungen festlegen, ob du dein Passwort jedes Mal eingeben willst, wenn es gebraucht wird, oder ob du es für die aktuelle Sitzung zwischenspeichern willst. + +### Windows + +Unter Windows ist es nicht möglich, einen untergeordneten Prozess zu aktivieren, wenn der übergeordnete Prozess nicht auch aktiviert ist. +Wenn XPipe also nicht als Administrator ausgeführt wird, kannst du lokal keine Berechtigungserweiterung nutzen. +Bei Fernverbindungen muss das verbundene Benutzerkonto über Administratorrechte verfügen. \ No newline at end of file diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/elevation_en.md b/lang/base/texts/elevation_en.md similarity index 100% rename from ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/elevation_en.md rename to lang/base/texts/elevation_en.md diff --git a/lang/base/texts/executionType_de.md b/lang/base/texts/executionType_de.md new file mode 100644 index 000000000..45b36bcfc --- /dev/null +++ b/lang/base/texts/executionType_de.md @@ -0,0 +1,15 @@ +## Ausführungsarten + +Es gibt zwei verschiedene Ausführungsarten, wenn XPipe eine Verbindung zu einem System herstellt. + +### Im Hintergrund + +Die erste Verbindung zu einem System wird im Hintergrund in einer stummen Terminalsitzung hergestellt. + +Blockierende Befehle, die Benutzereingaben erfordern, können den Shell-Prozess einfrieren, wenn XPipe ihn intern zuerst im Hintergrund startet. Um dies zu vermeiden, solltest du diese blockierenden Befehle nur im Terminalmodus aufrufen. + +Der Dateibrowser z. B. verwendet für seine Operationen ausschließlich den dummen Hintergrundmodus. Wenn du also möchtest, dass deine Skriptumgebung für die Dateibrowser-Sitzung gilt, sollte sie im dummen Modus ausgeführt werden. + +### In den Terminals + +Nachdem die anfängliche Dumb-Terminal-Verbindung erfolgreich war, öffnet XPipe eine separate Verbindung im eigentlichen Terminal. Wenn du möchtest, dass das Skript ausgeführt wird, wenn du die Verbindung in einem Terminal öffnest, dann wähle den Terminalmodus. diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md b/lang/base/texts/executionType_en.md similarity index 56% rename from ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md rename to lang/base/texts/executionType_en.md index 185b08e88..68e77d4b7 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md +++ b/lang/base/texts/executionType_en.md @@ -2,16 +2,14 @@ There are two distinct execution types when XPipe connects to a system. -### Dumb terminals +### In the background -The first connection to a system is made in the background in a dumb terminal. +The first connection to a system is made in the background in a dumb terminal session. -Blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background. -To avoid this, you should only call these blocking commands in the terminal mode. +Blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background. To avoid this, you should only call these blocking commands in the terminal mode. The file browser for example entirely uses the dumb background mode to handle its operations, so if you want your script environment to apply to the file browser session, it should run in the dumb mode. -### Proper terminals +### In the terminals -After a dumb terminal connection has succeeded, XPipe will open a separate connection in the actual terminal. -If you want the script to be run when you open the connection in a terminal, then choose the terminal mode. +After the initial dumb terminal connection has succeeded, XPipe will open a separate connection in the actual terminal. If you want the script to be run when you open the connection in a terminal, then choose the terminal mode. diff --git a/lang/base/texts/scriptCompatibility_de.md b/lang/base/texts/scriptCompatibility_de.md new file mode 100644 index 000000000..bd8bc5707 --- /dev/null +++ b/lang/base/texts/scriptCompatibility_de.md @@ -0,0 +1,13 @@ +## Skript-Kompatibilität + +Der Shell-Typ bestimmt, wo das Skript ausgeführt werden kann. +Abgesehen von einer exakten Übereinstimmung, d.h. der Ausführung eines `zsh`-Skripts in `zsh`, führt XPipe auch eine breitere Kompatibilitätsprüfung durch. + +### Posix-Shells + +Jedes Skript, das als `sh`-Skript deklariert ist, kann in jeder Posix-Shell-Umgebung wie `bash` oder `zsh` ausgeführt werden. +Wenn du ein grundlegendes Skript auf vielen verschiedenen Systemen ausführen willst, ist die Verwendung von Skripten mit `sh`-Syntax die beste Lösung dafür. + +### PowerShell + +Skripte, die als normale `powershell`-Skripte deklariert sind, können auch in `pwsh`-Umgebungen ausgeführt werden. diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptCompatibility_en.md b/lang/base/texts/scriptCompatibility_en.md similarity index 82% rename from ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptCompatibility_en.md rename to lang/base/texts/scriptCompatibility_en.md index 80bcdc2d7..9509de303 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptCompatibility_en.md +++ b/lang/base/texts/scriptCompatibility_en.md @@ -10,4 +10,4 @@ If you intend to run a basic script on many different systems, then using only ` ### PowerShell -Scripts declared as normal PowerShell scripts are also able to run in PowerShell Core environments. +Scripts declared as normal `powershell` scripts are also able to run in `pwsh` environments. diff --git a/lang/base/texts/scriptDependencies_de.md b/lang/base/texts/scriptDependencies_de.md new file mode 100644 index 000000000..1f3c8bf97 --- /dev/null +++ b/lang/base/texts/scriptDependencies_de.md @@ -0,0 +1,5 @@ +## Skriptabhängigkeiten + +Die Skripte und Skriptgruppen, die zuerst ausgeführt werden sollen. Wenn eine ganze Gruppe zu einer Abhängigkeit gemacht wird, werden alle Skripte in dieser Gruppe als Abhängigkeiten betrachtet. + +Der aufgelöste Abhängigkeitsgraph von Skripten wird abgeflacht, gefiltert und eindeutig gemacht. D.h. es werden nur kompatible Skripte ausgeführt und wenn ein Skript mehrmals ausgeführt werden würde, wird es nur beim ersten Mal ausgeführt. diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md b/lang/base/texts/scriptDependencies_en.md similarity index 100% rename from ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md rename to lang/base/texts/scriptDependencies_en.md diff --git a/lang/base/texts/script_de.md b/lang/base/texts/script_de.md new file mode 100644 index 000000000..7793b812d --- /dev/null +++ b/lang/base/texts/script_de.md @@ -0,0 +1,5 @@ +## Skriptinhalt + +Der Inhalt des Skripts, das ausgeführt werden soll. Du kannst ihn entweder direkt bearbeiten oder die Schaltfläche "Externe Bearbeitung" in der oberen rechten Ecke verwenden, um einen externen Texteditor zu starten. + +Bei Shells, die dies unterstützen, musst du keine Shebang-Zeile angeben, sie wird automatisch mit dem entsprechenden Shell-Typ hinzugefügt. diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/script_en.md b/lang/base/texts/script_en.md similarity index 100% rename from ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/script_en.md rename to lang/base/texts/script_en.md diff --git a/lang/jdbc/strings/translations_de.properties b/lang/jdbc/strings/translations_de.properties new file mode 100644 index 000000000..0b6a263b4 --- /dev/null +++ b/lang/jdbc/strings/translations_de.properties @@ -0,0 +1,20 @@ +postgres.displayName=PostgreSQL +postgres.displayDescription=Eine psql-Shell für einen PostgreSQL-Server öffnen +query=Abfrage +proxy=Proxy +peerAuth=Peer-Authentifizierung +port=Port +url=URL +instance=Instanz +username=Benutzername +usernameDescription=Der Benutzer, als der man sich anmeldet +password=Passwort +passwordDescription=Das Passwort zur Authentifizierung +authentication=Authentifizierung +authenticationType=Methode +connection=Verbindung +connectionUrl=Verbindungs-URL +connectionString=Verbindungsstring +passwordAuth=Passwort-Authentifizierung +windowsAuth=Windows-Authentifizierung +psqlShell=PSQL-Shell im Terminal öffnen diff --git a/lang/jdbc/strings/translations_en.properties b/lang/jdbc/strings/translations_en.properties new file mode 100644 index 000000000..9998d944b --- /dev/null +++ b/lang/jdbc/strings/translations_en.properties @@ -0,0 +1,20 @@ +postgres.displayName=PostgreSQL +postgres.displayDescription=Open a psql shell to a PostgreSQL Server +query=Query +proxy=Proxy +peerAuth=Peer Authentication +port=Port +url=URL +instance=Instance +username=Username +usernameDescription=The user to log in as +password=Password +passwordDescription=The password to authenticate +authentication=Authentication +authenticationType=Method +connection=Connection +connectionUrl=Connection URL +connectionString=Connection String +passwordAuth=Password Authentication +windowsAuth=Windows Authentication +psqlShell=Open PSQL Shell in Terminal \ No newline at end of file diff --git a/lang/proc/strings/translations_de.properties b/lang/proc/strings/translations_de.properties new file mode 100644 index 000000000..f36e533c3 --- /dev/null +++ b/lang/proc/strings/translations_de.properties @@ -0,0 +1,273 @@ +showInternalPods=Interne Pods anzeigen +showAllNamespaces=Alle Namespaces anzeigen +showInternalContainers=Interne Container anzeigen +refresh=Aktualisieren +vmwareGui=GUI starten +monitorVm=VM überwachen +addCluster=Cluster hinzufügen ... +showNonRunningInstances=Nicht laufende Instanzen anzeigen +vmwareGuiDescription=Ob eine virtuelle Maschine im Hintergrund oder in einem Fenster gestartet werden soll. +vmwareEncryptionPassword=Verschlüsselungspasswort +vmwareEncryptionPasswordDescription=Das optionale Passwort, das zur Verschlüsselung der VM verwendet wird. +vmwarePasswordDescription=Das erforderliche Passwort für den Gastbenutzer. +vmwarePassword=Benutzer-Passwort +vmwareUser=Gast-Benutzer +runTempContainer=Temporärer Container ausführen +vmwareUserDescription=Der Benutzername deines primären Gastbenutzers +dockerTempRunAlertTitle=Temporärer Container ausführen +dockerTempRunAlertHeader=Damit wird ein Shell-Prozess in einem temporären Container ausgeführt, der automatisch entfernt wird, sobald er gestoppt wird. +imageName=Bildname +imageNameDescription=Die zu verwendende Kennung des Containerbildes +containerName=Container-Name +containerNameDescription=Der optionale benutzerdefinierte Containername +vm=Virtuelle Maschine +yubikeyPiv=Yubikey PIV (Pro) +vmDescription=Die zugehörige Konfigurationsdatei. +vmwareScan=VMware Desktop-Hypervisoren +library=Bibliothek +customPkcs11Library=Benutzerdefinierte PKCS#11-Bibliothek (Pro) +vmwareMachine.displayName=VMware Virtuelle Maschine +vmwareMachine.displayDescription=Verbindung zu einer virtuellen Maschine über SSH +vmwareInstallation.displayName=VMware Desktop Hypervisor Installation +vmwareInstallation.displayDescription=Interaktion mit den installierten VMs über deren CLI +start=Start +stop=Stopp +pause=Pause +requiredSshServerAlertTitle=SSH-Server einrichten +requiredSshServerAlertHeader=Es kann kein installierter SSH-Server in der VM gefunden werden. +requiredSshServerAlertContent=Um sich mit der VM zu verbinden, sucht XPipe nach einem laufenden SSH-Server, aber es wurde kein verfügbarer SSH-Server für die VM gefunden. +computerName=Computer Name +pssComputerNameDescription=Der Computername, zu dem eine Verbindung hergestellt werden soll. Es wird angenommen, dass er bereits in deinen vertrauenswürdigen Hosts enthalten ist. +credentialUser=Berechtigungsnachweis Benutzer +pssCredentialUserDescription=Der Benutzer, als der du dich anmeldest. +credentialPassword=Berechtigungsnachweis Passwort +pssCredentialPasswordDescription=Das Passwort des Benutzers. +sshConfig=SSH-Konfigurationsdateien +autostart=Automatisches Verbinden beim Start von XPipe +acceptHostKey=Host-Schlüssel akzeptieren +modifyHostKeyPermissions=Host Key Berechtigungen ändern +attachContainer=Am Container anhängen +openInVsCode=In VSCode öffnen +containerLogs=Containerprotokolle anzeigen +openSftpClient=In einem externen SFTP-Client öffnen +openTermius=In Termius öffnen +showInternalInstances=Interne Instanzen anzeigen +editPod=Pod bearbeiten +acceptHostKeyDescription=Vertraue dem neuen Host-Schlüssel und fahre fort +modifyHostKeyPermissionsDescription=Versuchen Sie, die Berechtigungen der Originaldatei zu entfernen, damit OpenSSH zufrieden ist +psSession.displayName=PowerShell Remote-Sitzung +psSession.displayDescription=Verbinden über New-PSSession und Enter-PSSession +sshLocalTunnel.displayName=Lokaler SSH-Tunnel +sshLocalTunnel.displayDescription=Einen SSH-Tunnel zu einem entfernten Host einrichten +sshRemoteTunnel.displayName=Entfernter SSH-Tunnel +sshRemoteTunnel.displayDescription=Einen umgekehrten SSH-Tunnel von einem entfernten Host aus aufbauen +sshDynamicTunnel.displayName=Dynamischer SSH-Tunnel +sshDynamicTunnel.displayDescription=Einen SOCKS-Proxy über eine SSH-Verbindung einrichten +shellEnvironmentGroup.displayName=Shell-Umgebungen +shellEnvironmentGroup.displayDescription=Shell-Umgebungen +shellEnvironment.displayName=Benutzerdefinierte Shell-Umgebung +shellEnvironment.displayDescription=Eine angepasste Shell-Init-Umgebung erstellen +shellEnvironment.informationFormat=$TYPE$ umgebung +shellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ umgebung +environmentConnectionDescription=Die Basisverbindung zum Erstellen einer Umgebung aus +environmentScriptDescription=Das optionale benutzerdefinierte Init-Skript, das in der Shell ausgeführt wird +environmentSnippets=Skript-Schnipsel +commandSnippetsDescription=Die optionalen vordefinierten Skriptschnipsel, die zuerst ausgeführt werden +environmentSnippetsDescription=Die optionalen vordefinierten Skript-Snippets, die bei der Initialisierung ausgeführt werden +shellTypeDescription=Der explizite Shell-Typ zum Starten +originPort=Ursprungsport +originAddress=Herkunftsadresse +remoteAddress=Entfernte Adresse +remotePort=Entfernter Anschluss +remoteSourceAddress=Entfernte Quelladresse +remoteSourcePort=Entfernter Quellport +originDestinationPort=Ursprung Zielhafen +originDestinationAddress=Herkunft Zieladresse +origin=Herkunft +remoteHost=Entfernter Host +address=Adresse +proxmox=Proxmox +proxmox.displayName=Proxmox +proxmox.displayDescription=Verbindung zu Systemen in einer virtuellen Umgebung von Proxmox +proxmoxVm.displayName=Proxmox VM +proxmoxVm.displayDescription=Verbindung zu einer virtuellen Maschine in einer Proxmox VE über SSH +proxmoxContainer.displayName=Proxmox Container +proxmoxContainer.displayDescription=Verbindung zu einem Container in einer Proxmox VE +sshDynamicTunnel.originDescription=Das System, von dem aus die ssh-Verbindung geöffnet werden soll +sshDynamicTunnel.hostDescription=Das System, das als SOCKS-Proxy verwendet werden soll +sshDynamicTunnel.bindingDescription=An welche Adressen der Tunnel gebunden werden soll +sshRemoteTunnel.originDescription=Das System, von dem aus die ssh-Verbindung geöffnet werden soll und zu dem der Tunnel geöffnet werden soll +sshRemoteTunnel.hostDescription=Das System, von dem aus der Ferntunnel zum Ursprung gestartet werden soll +sshRemoteTunnel.bindingDescription=An welche Adressen der Tunnel gebunden werden soll +sshLocalTunnel.originDescription=Das System, von dem aus der Tunnel gestartet werden soll +sshLocalTunnel.hostDescription=Das System, zu dem der Tunnel geöffnet werden soll +sshLocalTunnel.bindingDescription=An welche Adressen der Tunnel gebunden werden soll +sshLocalTunnel.localAddressDescription=Die lokale Adresse zum Binden +sshLocalTunnel.remoteAddressDescription=Die zu bindende Remote-Adresse +active=Aktiv +inactive=Inaktiv +cmd.displayName=Benutzerdefinierter Terminal-Befehl +cmd.displayDescription=Einen benutzerdefinierten Befehl auf einem System in deinem Terminal ausführen +k8sPod.displayName=Kubernetes Pod +k8sPod.displayDescription=Verbinden mit einem Pod und seinen Containern über kubectl +k8sContainer.displayName=Kubernetes Container +k8sContainer.displayDescription=Eine Shell für einen Container öffnen +k8sCluster.displayName=Kubernetes Cluster +k8sCluster.displayDescription=Verbinden mit einem Cluster und seinen Pods über kubectl +sshTunnelGroup.displayName=SSH-Tunnel +sshTunnelGroup.displayCategory=Alle Arten von SSH-Tunneln +podmanCmd.displayName=Podman CLI +podmanCmd.displayCategory=Zugriff auf Podman-Container über den CLI-Client +podmanContainers=Podman Container +local.displayName=Lokale Maschine +local.displayDescription=Die Shell des lokalen Rechners +cygwin=Cygwin +msys2=MSYS2 +gitWindows=Git für Windows +gitForWindows.displayName=Git für Windows +gitForWindows.displayDescription=Zugriff auf deine lokale Git For Windows-Umgebung +msys2.displayName=MSYS2 +msys2.displayDescription=Zugriff auf die Shells deiner MSYS2 Umgebung +cygwin.displayName=Cygwin +cygwin.displayDescription=Zugriff auf die Shells deiner Cygwin-Umgebung +namespace=Namespace +gitVaultIdentityStrategy=Git SSH Identität +gitVaultIdentityStrategyDescription=Wenn du dich entschieden hast, eine SSH-Git-URL als Remote zu verwenden und dein Remote-Repository eine SSH-Identität erfordert, dann setze diese Option.\n\n Falls du eine HTTP-URL angegeben hast, kannst du diese Option ignorieren. +dockerContainers=Docker-Container +lxdContainers=LXD-Container +dockerCmd.displayName=docker CLI-Client +dockerCmd.displayDescription=Zugriff auf Docker-Container über den Docker CLI-Client +lxdCmd.displayName=LXD CLI-Client +lxdCmd.displayDescription=Zugriff auf LXD-Container über das lxc CLI cient +wslCmd.displayName=wsl-Client +wslCmd.displayDescription=Zugriff auf WSL-Instanzen über das wsl CLI cient +k8sCmd.displayName=kubectl-Client +k8sCmd.displayDescription=Zugriff auf Kubernetes-Cluster über kubectl +k8sClusters=Kubernetes-Cluster +shells=Verfügbare Muscheln +startContainer=Container starten +stopContainer=Container anhalten +inspectContainer=Container inspizieren +k8sClusterNameDescription=Der Name des Kontexts, in dem sich der Cluster befindet. +pod=Pod +podName=Pod-Name +k8sClusterContext=Kontext +k8sClusterContextDescription=Der Name des Kontexts, in dem sich der Cluster befindet +k8sClusterNamespace=Namespace +k8sClusterNamespaceDescription=Der benutzerdefinierte Namespace oder der Standard-Namespace, falls leer +k8sConfigLocation=Config-Datei +k8sConfigLocationDescription=Die benutzerdefinierte kubeconfig-Datei oder die Standarddatei, wenn sie leer ist +inspectPod=Pod inspizieren +showAllContainers=Nicht laufende Container anzeigen +showAllPods=Nicht laufende Pods anzeigen +wsl=WSL +docker=Docker +k8sPodHostDescription=Der Host, auf dem sich der Pod befindet +k8sContainerDescription=Der Name des Kubernetes-Containers +k8sPodDescription=Der Name des Kubernetes-Pods +podDescription=Der Pod, auf dem sich der Container befindet +k8sClusterHostDescription=Der Host, über den auf den Cluster zugegriffen werden soll. Muss kubectl installiert und konfiguriert haben, um auf den Cluster zugreifen zu können. +script=Init-Skript +connection=Verbindung +shellCommand.displayName=Benutzerdefinierter Shell-Öffner-Befehl +shellCommand.displayDescription=Öffnen einer Standard-Shell durch einen benutzerdefinierten Befehl +ssh.displayName=Einfache SSH-Verbindung +ssh.displayDescription=Verbindung über einen SSH-Befehlszeilen-Client +sshConfig.displayName=SSH-Konfigurationsdatei +sshConfig.displayDescription=Verbindung zu Hosts, die in einer SSH-Konfigurationsdatei definiert sind +sshConfigHost.displayName=SSH-Konfigurationsdatei Host +sshConfigHost.displayDescription=Sich mit einem in einer SSH-Konfigurationsdatei definierten Host verbinden +sshConfigHost.password=Passwort +sshConfigHost.passwordDescription=Gib das optionale Passwort für die Benutzeranmeldung an. +sshConfigHost.identityPassphrase=Identitäts-Passphrase +sshConfigHost.identityPassphraseDescription=Gib die optionale Passphrase für deinen Identitätsschlüssel an. +binary.displayName=Binär +binary.displayDescription=Binäre Daten +text.displayName=Text +shellCommand.hostDescription=Der Host, auf dem der Befehl ausgeführt werden soll +shellCommand.commandDescription=Der Befehl, mit dem eine Shell geöffnet wird +sshAgent=SSH-Agent +none=Keine +commandDescription=Die Befehle, die in einem Shell-Skript auf dem Host ausgeführt werden sollen. +commandHostDescription=Der Host, auf dem der Befehl ausgeführt werden soll +commandDataFlowDescription=Wie dieser Befehl Ein- und Ausgaben behandelt +commandElevationDescription=Ob dieser Befehl mit erweiterten Rechten ausgeführt werden soll +commandShellTypeDescription=Die Shell, die für diesen Befehl verwendet werden soll +ssh.passwordDescription=Das optionale Passwort, das bei der Authentifizierung verwendet wird +keyAuthentication=Schlüsselbasierte Authentifizierung +keyAuthenticationDescription=Die zu verwendende Authentifizierungsmethode, wenn eine schlüsselbasierte Authentifizierung erforderlich ist. +customAgent=Benutzerdefinierter Agent +identityAgent=Identitätsagent +ssh.proxyDescription=Der optionale Proxy-Host, der beim Aufbau der SSH-Verbindung verwendet wird. Es muss ein SSH-Client installiert sein. +usage=Verwendung +wslHostDescription=Der Host, auf dem sich die WSL-Instanz befindet. Muss wsl installiert haben. +wslDistributionDescription=Der Name der WSL-Instanz +wslUsernameDescription=Der explizite Benutzername, mit dem du dich anmeldest. Wenn er nicht angegeben wird, wird der Standardbenutzername verwendet. +wslPasswordDescription=Das Passwort des Benutzers, das für sudo-Befehle verwendet werden kann. +dockerHostDescription=Der Host, auf dem sich der Docker-Container befindet. Muss Docker installiert haben. +dockerContainerDescription=Der Name des Docker-Containers +lxdHostDescription=Der Host, auf dem sich der LXD-Container befindet. Muss lxc installiert haben. +lxdContainerDescription=Der Name des LXD-Containers +localMachine=Lokale Maschine +rootScan=Root-Shell-Umgebung +loginEnvironmentScan=Benutzerdefinierte Anmeldeumgebung +k8sScan=Kubernetes-Cluster +options=Optionen +dockerRunningScan=Docker-Container ausführen +dockerAllScan=Alle Docker-Container +wslScan=WSL-Instanzen +sshScan=SSH-Konfigurationsverbindungen +requiresElevation=Erhöht ausführen +default=Standard +wslHost=WSL-Host +timeout=Timeout +installLocation=Installationsort +installLocationDescription=Der Ort, an dem deine $NAME$ Umgebung installiert ist +wsl.displayName=Windows Subsystem für Linux +wsl.displayDescription=Verbindung zu einer WSL-Instanz unter Windows +docker.displayName=Docker Container +docker.displayDescription=Mit einem Docker-Container verbinden +podman.displayName=Podman Container +podman.displayDescription=Mit einem Podman-Container verbinden +lxd.displayName=LXD-Container +lxd.displayDescription=Verbindung zu einem LXD-Container über lxc +container=Container +host=Host +port=Port +user=Benutzer +password=Passwort +method=Methode +uri=URL +proxy=Proxy +distribution=Vertrieb +username=Benutzername +shellType=Shell-Typ +browseFile=Datei durchsuchen +openShell=Shell im Terminal öffnen +openCommand=Befehl im Terminal ausführen +editFile=Datei bearbeiten +description=Beschreibung +keyFile=Identitätsdatei +keyPassword=Passphrase +key=Schlüssel +furtherCustomization=Weitere Anpassungen +furtherCustomizationDescription=Weitere Konfigurationsoptionen findest du in den ssh-Konfigurationsdateien +location=Standort +browse=Durchsuchen +locationDescription=Der Dateipfad deines entsprechenden privaten Schlüssels +configHost=Host +configHostDescription=Der Host, auf dem sich die Konfiguration befindet +configLocation=Config-Speicherort +configLocationDescription=Der Dateipfad der Konfigurationsdatei +pageant=Pageant +gpgAgent=GPG Agent (Pro) +gateway=Gateway +gatewayDescription=Das optionale Gateway, das bei der Verbindung verwendet wird. +connectionInformation=Verbindungsinformationen +connectionInformationDescription=Wo die Verbindung zu +passwordAuthentication=Passwort-Authentifizierung +passwordDescription=Das optionale Passwort, das zur Authentifizierung verwendet wird. +sshConfigString.displayName=Angepasste SSH-Verbindung +sshConfigString.displayDescription=Eine vollständig angepasste SSH-Verbindung erstellen +sshConfigStringContent=Konfiguration +sshConfigStringContentDescription=SSH-Optionen für die Verbindung im OpenSSH-Config-Format diff --git a/lang/proc/strings/translations_en.properties b/lang/proc/strings/translations_en.properties new file mode 100644 index 000000000..f3a9d9e56 --- /dev/null +++ b/lang/proc/strings/translations_en.properties @@ -0,0 +1,285 @@ +showInternalPods=Show internal pods +showAllNamespaces=Show all namespaces +showInternalContainers=Show internal containers +refresh=Refresh +vmwareGui=Start GUI +monitorVm=Monitor VM +addCluster=Add cluster ... +showNonRunningInstances=Show non-running instances +vmwareGuiDescription=Whether to start a virtual machine in the background or in a window. +vmwareEncryptionPassword=Encryption password +vmwareEncryptionPasswordDescription=The optional password used to encrypt the VM. +vmwarePasswordDescription=The required password for the guest user. +vmwarePassword=User password +vmwareUser=Guest user +runTempContainer=Run temporary container +vmwareUserDescription=The username of your primary guest user +dockerTempRunAlertTitle=Run temporary container +dockerTempRunAlertHeader=This will run a shell process in a temporary container that will get automatically removed once it is stopped. +imageName=Image name +imageNameDescription=The container image identifier to use +containerName=Container name +containerNameDescription=The optional custom container name +vm=Virtual machine +yubikeyPiv=Yubikey PIV (Pro) +vmDescription=The associated configuration file. +vmwareScan=VMware desktop hypervisors +library=Library +customPkcs11Library=Custom PKCS#11 library (Pro) +vmwareMachine.displayName=VMware Virtual Machine +vmwareMachine.displayDescription=Connect to a virtual machine via SSH +vmwareInstallation.displayName=VMware desktop hypervisor installation +vmwareInstallation.displayDescription=Interact with the installed VMs via its CLI +start=Start +stop=Stop +pause=Pause +rdpTunnelHost=Tunnel host +rdpTunnelHostDescription=The optional SSH connection to use as a tunnel +rdpFileLocation=File location +rdpFileLocationDescription=The file path of the .rdp file +rdpPasswordAuthentication=Password authentication +rdpPasswordAuthenticationDescription=The password to automatically fill in if supported +rdpFile.displayName=RDP File +rdpFile.displayDescription=Connect to a system via an existing .rdp file +requiredSshServerAlertTitle=Setup SSH server +requiredSshServerAlertHeader=Unable to find an installed SSH server in the VM. +requiredSshServerAlertContent=To connect to the VM, XPipe is looking for a running SSH server but no available SSH server was detected for the VM.. +computerName=Computer Name +pssComputerNameDescription=The computer name to connect to. It is assumed that it is already included in your trusted hosts. +credentialUser=Credential User +pssCredentialUserDescription=The user to login as. +credentialPassword=Credential Password +pssCredentialPasswordDescription=The password of the user. +sshConfig=SSH config files +autostart=Automatically connect on XPipe startup +acceptHostKey=Accept host key +modifyHostKeyPermissions=Modify host key permissions +attachContainer=Attach to container +openInVsCode=Open in VSCode +containerLogs=Show container logs +openSftpClient=Open in external SFTP client +openTermius=Open in Termius +showInternalInstances=Show internal instances +editPod=Edit pod +acceptHostKeyDescription=Trust the new host key and continue +modifyHostKeyPermissionsDescription=Attempt to remove permissions of the original file so that OpenSSH is happy +psSession.displayName=PowerShell Remote Session +psSession.displayDescription=Connect via New-PSSession and Enter-PSSession +sshLocalTunnel.displayName=Local SSH tunnel +sshLocalTunnel.displayDescription=Establish an SSH tunnel to a remote host +sshRemoteTunnel.displayName=Remote SSH tunnel +sshRemoteTunnel.displayDescription=Establish a reverse SSH tunnel from a remote host +sshDynamicTunnel.displayName=Dynamic SSH tunnel +sshDynamicTunnel.displayDescription=Establish a SOCKS proxy through an SSH connection +shellEnvironmentGroup.displayName=Shell Environments +shellEnvironmentGroup.displayDescription=Shell Environments +shellEnvironment.displayName=Custom Shell Environment +shellEnvironment.displayDescription=Create a customized shell init environment +shellEnvironment.informationFormat=$TYPE$ environment +shellEnvironment.elevatedInformationFormat=$ELEVATION$ $TYPE$ environment +environmentConnectionDescription=The base connection to create an environment from +environmentScriptDescription=The optional custom init script to run in the shell +environmentSnippets=Script snippets +commandSnippetsDescription=The optional predefined script snippets to run first +environmentSnippetsDescription=The optional predefined script snippets to run on initialization +shellTypeDescription=The explicit shell type to launch +originPort=Origin port +originAddress=Origin address +remoteAddress=Remote address +remotePort=Remote port +remoteSourceAddress=Remote source address +remoteSourcePort=Remote source port +originDestinationPort=Origin destination port +originDestinationAddress=Origin destination address +origin=Origin +remoteHost=Remote host +address=Address +proxmox=Proxmox +proxmox.displayName=Proxmox +proxmox.displayDescription=Connect to systems in a Proxmox Virtual Environment +proxmoxVm.displayName=Proxmox VM +proxmoxVm.displayDescription=Connect to a virtual machine in a Proxmox VE via SSH +proxmoxContainer.displayName=Proxmox Container +proxmoxContainer.displayDescription=Connect to a container in a Proxmox VE +sshDynamicTunnel.originDescription=The system from where to open the ssh connection +sshDynamicTunnel.hostDescription=The system to use as SOCKS proxy +sshDynamicTunnel.bindingDescription=What addresses to bind the tunnel to +sshRemoteTunnel.originDescription=The system from where to open the ssh connection and to open the tunnel to +sshRemoteTunnel.hostDescription=The system from which to start the remote tunnel to the origin +sshRemoteTunnel.bindingDescription=What addresses to bind the tunnel to +sshLocalTunnel.originDescription=The system from where to start the tunnel +sshLocalTunnel.hostDescription=The system to open the tunnel to +sshLocalTunnel.bindingDescription=What addresses to bind the tunnel to +sshLocalTunnel.localAddressDescription=The local address to bind +sshLocalTunnel.remoteAddressDescription=The remote address to bind +active=Active +inactive=Inactive +cmd.displayName=Custom Terminal Command +cmd.displayDescription=Run a custom command on a system in your terminal +k8sPod.displayName=Kubernetes Pod +k8sPod.displayDescription=Connect to a pod and its containers via kubectl +k8sContainer.displayName=Kubernetes Container +k8sContainer.displayDescription=Open a shell to a container +k8sCluster.displayName=Kubernetes Cluster +k8sCluster.displayDescription=Connect to a cluster and its pods via kubectl +sshTunnelGroup.displayName=SSH Tunnels +sshTunnelGroup.displayCategory=All types of SSH tunnels +podmanCmd.displayName=Podman CLI +podmanCmd.displayCategory=Access Podman containers via the CLI client +podmanContainers=Podman containers +local.displayName=Local machine +local.displayDescription=The shell of the local machine +cygwin=Cygwin +msys2=MSYS2 +gitWindows=Git For Windows +gitForWindows.displayName=Git For Windows +gitForWindows.displayDescription=Access your local Git For Windows environment +msys2.displayName=MSYS2 +msys2.displayDescription=Access shells of your MSYS2 environment +cygwin.displayName=Cygwin +cygwin.displayDescription=Access shells of your Cygwin environment +namespace=Namespace +gitVaultIdentityStrategy=Git SSH identity +gitVaultIdentityStrategyDescription=If you chose to use an SSH git URL as the remote and your remote repository requires an SSH identity, then set this option.\n\nIn case you provided an HTTP url, you can ignore this option. +dockerContainers=Docker containers +lxdContainers=LXD containers +dockerCmd.displayName=docker CLI client +dockerCmd.displayDescription=Access Docker containers via the docker CLI client +lxdCmd.displayName=LXD CLI client +lxdCmd.displayDescription=Access LXD containers via the lxc CLI cient +wslCmd.displayName=wsl client +wslCmd.displayDescription=Access WSL instances via the wsl CLI cient +k8sCmd.displayName=kubectl client +k8sCmd.displayDescription=Access Kubernetes clusters via kubectl +k8sClusters=Kubernetes clusters +shells=Available shells +startContainer=Start container +stopContainer=Stop container +inspectContainer=Inspect container +k8sClusterNameDescription=The name of the context the cluster is in. +pod=Pod +podName=Pod name +k8sClusterContext=Context +k8sClusterContextDescription=The name of the context the cluster is in +k8sClusterNamespace=Namespace +k8sClusterNamespaceDescription=The custom namespace or the default one if empty +k8sConfigLocation=Config file +k8sConfigLocationDescription=The custom kubeconfig file or the default one if left empty +inspectPod=Inspect pod +showAllContainers=Show non-running containers +showAllPods=Show non-running pods +wsl=WSL +docker=Docker +k8sPodHostDescription=The host on which the pod is located +k8sContainerDescription=The name of the Kubernetes container +k8sPodDescription=The name of the Kubernetes pod +podDescription=The pod on which the container is located +k8sClusterHostDescription=The host through which the cluster should be accessed. Must have kubectl installed and configured to be able to access the cluster. +script=Init Script +connection=Connection +shellCommand.displayName=Custom Shell Opener Command +shellCommand.displayDescription=Open a standard shell through a custom command +ssh.displayName=Simple SSH Connection +ssh.displayDescription=Connect via an SSH command-line client +sshConfig.displayName=SSH Config File +sshConfig.displayDescription=Connect to hosts defined in an SSH config file +sshConfigHost.displayName=SSH Config File Host +sshConfigHost.displayDescription=Connect to a host defined in an SSH config file +sshConfigHost.password=Password +sshConfigHost.passwordDescription=Provide the optional password for the user login. +sshConfigHost.identityPassphrase=Identity passphrase +sshConfigHost.identityPassphraseDescription=Provide the optional passphrase for your identity key. +binary.displayName=Binary +binary.displayDescription=Binary data +text.displayName=Text +shellCommand.hostDescription=The host to execute the command on +shellCommand.commandDescription=The command that will open a shell +sshAgent=SSH-Agent +none=None +commandDescription=The commands to execute in a shell script on the host. +commandHostDescription=The host to run the command on +commandDataFlowDescription=How this command handles input and output +commandElevationDescription=Whether to run this command with elevated permissions +commandShellTypeDescription=The shell to use for this command +ssh.passwordDescription=The optional password to use when authenticating +keyAuthentication=Key-based authentication +keyAuthenticationDescription=The authentication method to use if key-based authentication is required. +customAgent=Custom agent +identityAgent=Identity agent +ssh.proxyDescription=The optional proxy host to use when establishing the SSH connection. Must have an ssh client installed. +usage=Usage +wslHostDescription=The host on which the WSL instance is located on. Must have wsl installed. +wslDistributionDescription=The name of the WSL instance +wslUsernameDescription=The explicit username to login as. If not specified, the default username will be used. +wslPasswordDescription=The user's password which can be used for sudo commands. +dockerHostDescription=The host on which the docker container is located on. Must have docker installed. +dockerContainerDescription=The name of the docker container +lxdHostDescription=The host on which the LXD container is located on. Must have lxc installed. +lxdContainerDescription=The name of the LXD container +localMachine=Local Machine +rootScan=Root shell environment +loginEnvironmentScan=Custom login environment +k8sScan=Kubernetes cluster +options=Options +dockerRunningScan=Running docker containers +dockerAllScan=All docker containers +wslScan=WSL instances +sshScan=SSH config connections +requiresElevation=Run Elevated +default=Default +wslHost=WSL Host +timeout=Timeout +installLocation=Install location +installLocationDescription=The location where your $NAME$ environment is installed +wsl.displayName=Windows Subsystem for Linux +wsl.displayDescription=Connect to a WSL instance running on Windows +docker.displayName=Docker Container +docker.displayDescription=Connect to a docker container +podman.displayName=Podman Container +podman.displayDescription=Connect to a Podman container +lxd.displayName=LXD Container +lxd.displayDescription=Connect to a LXD container via lxc +container=Container +host=Host +port=Port +user=User +password=Password +method=Method +uri=URL +proxy=Proxy +distribution=Distribution +username=Username +shellType=Shell Type +browseFile=Browse File +openShell=Open Shell in Terminal +openCommand=Execute Command in Terminal +editFile=Edit File +description=Description +keyFile=Identity File +keyPassword=Passphrase +key=Key +furtherCustomization=Further customization +furtherCustomizationDescription=For more configuration options, use the ssh config files +location=Location +browse=Browse +locationDescription=The file path of your corresponding private key +configHost=Host +configHostDescription=The host on which the config is located on +configLocation=Config location +configLocationDescription=The file path of the config file +pageant=Pageant +gpgAgent=GPG Agent (Pro) +gateway=Gateway +gatewayDescription=The optional gateway to use when connecting. +connectionInformation=Connection information +connectionInformationDescription=Where to connect to +passwordAuthentication=Password authentication +passwordDescription=The optional password to use to authenticate. +sshConfigString.displayName=Customized SSH Connection +sshConfigString.displayDescription=Create a fully customized SSH connection +sshConfigStringContent=Configuration +sshConfigStringContentDescription=SSH options for the connection in OpenSSH config format +vnc.displayName=VNC connection +vnc.displayDescription=Open a VNC session via an SSH tunnel +binding=Binding +vncPortDescription=The port the VNC server is listening on diff --git a/lang/proc/texts/elevation_de.md b/lang/proc/texts/elevation_de.md new file mode 100644 index 000000000..61dd15bac --- /dev/null +++ b/lang/proc/texts/elevation_de.md @@ -0,0 +1,11 @@ +## Elevation + +Der Prozess der Berechtigungserweiterung ist betriebssystemspezifisch. + +### Linux & macOS + +Jeder erweiterte Befehl wird mit `sudo` ausgeführt. Das optionale `sudo` Passwort wird bei Bedarf über XPipe abgefragt. Du kannst in den Einstellungen festlegen, ob du dein Passwort jedes Mal eingeben willst, wenn es gebraucht wird, oder ob du es für die aktuelle Sitzung zwischenspeichern willst. + +### Windows + +Unter Windows ist es nicht möglich, die Berechtigungen eines untergeordneten Prozesses zu erhöhen, wenn der übergeordnete Prozess nicht ebenfalls mit erhöhten Berechtigungen ausgeführt wird. Wenn XPipe also nicht als Administrator ausgeführt wird, kannst du lokal keine Berechtigungserweiterung nutzen. Bei Fernverbindungen muss das verbundene Benutzerkonto mit Administratorrechten ausgestattet sein. \ No newline at end of file diff --git a/lang/proc/texts/elevation_en.md b/lang/proc/texts/elevation_en.md new file mode 100644 index 000000000..a2e5326b5 --- /dev/null +++ b/lang/proc/texts/elevation_en.md @@ -0,0 +1,11 @@ +## Elevation + +The process of permissions elevation is operating system specific. + +### Linux & macOS + +Any elevated command is executed with `sudo`. The optional `sudo` password is queried via XPipe when needed. You have the ability to adjust the elevation behavior in the settings to control whether you want to enter your password every time it is needed or if you want to cache it for the current session. + +### Windows + +On Windows, it is not possible to elevate the permissions of a child process if the parent process is not running with elevated permissions as well. Therefore, if XPipe is not run as an administrator, you will be unable to use any elevation locally. For remote connections, the connected user account has to be given administrator privileges. \ No newline at end of file diff --git a/lang/proc/texts/environmentScript_de.md b/lang/proc/texts/environmentScript_de.md new file mode 100644 index 000000000..fd0e27226 --- /dev/null +++ b/lang/proc/texts/environmentScript_de.md @@ -0,0 +1,9 @@ +## Init-Skript + +Die optionalen Befehle, die ausgeführt werden, nachdem die Init-Dateien und -Profile der Shell ausgeführt worden sind. + +Du kannst dies wie ein normales Shell-Skript behandeln, d.h. du kannst die gesamte Syntax verwenden, die die Shell in Skripten unterstützt. Alle Befehle, die du ausführst, werden von der Shell übernommen und verändern die Umgebung. Wenn du also zum Beispiel eine Variable setzt, hast du in dieser Shell-Sitzung Zugriff auf diese Variable. + +### Blockierende Befehle + +Beachte, dass blockierende Befehle, die Benutzereingaben erfordern, den Shell-Prozess einfrieren können, wenn XPipe ihn zuerst intern im Hintergrund startet. Um dies zu vermeiden, rufe diese blockierenden Befehle nur auf, wenn die Variable `TERM` nicht auf `dumb` gesetzt ist. XPipe setzt die Variable `TERM=dumb` automatisch, wenn es die Shell-Sitzung im Hintergrund vorbereitet und setzt dann `TERM=xterm-256color`, wenn es das Terminal tatsächlich öffnet. \ No newline at end of file diff --git a/lang/proc/texts/environmentScript_en.md b/lang/proc/texts/environmentScript_en.md new file mode 100644 index 000000000..55fc836fb --- /dev/null +++ b/lang/proc/texts/environmentScript_en.md @@ -0,0 +1,9 @@ +## Init script + +The optional commands to run after the shell's init files and profiles have been executed. + +You can treat this as a normal shell script, i.e. make use of all the syntax that the shell supports in scripts. All commands you execute are sourced by the shell and modify the environment. So if you for example set a variable, you will have access to this variable in this shell session. + +### Blocking commands + +Note that blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background. To avoid this, only call these blocking commands if the variable `TERM` is not set to `dumb`. XPipe automatically sets the variable `TERM=dumb` when it is preparing the shell session in the background and then sets `TERM=xterm-256color` when actually opening the terminal. \ No newline at end of file diff --git a/lang/proc/texts/proxmoxPassword_de.md b/lang/proc/texts/proxmoxPassword_de.md new file mode 100644 index 000000000..c6a0d68f8 --- /dev/null +++ b/lang/proc/texts/proxmoxPassword_de.md @@ -0,0 +1,3 @@ +## Passwort + +Wenn du auf deiner VM eine komplexere SSH-Authentifizierung als ein einfaches Passwort verwendest, kannst du das System einfach als normale SSH-Verbindung in XPipe hinzufügen. Wenn es von außen nicht zugänglich ist, kannst du das übergeordnete PVE-System als SSH-Gateway einrichten. diff --git a/lang/proc/texts/proxmoxPassword_en.md b/lang/proc/texts/proxmoxPassword_en.md new file mode 100644 index 000000000..871a63849 --- /dev/null +++ b/lang/proc/texts/proxmoxPassword_en.md @@ -0,0 +1,3 @@ +## Password + +If you are using a more complex SSH authentication on your VM rather than a simple password, you can just add the system as a normal SSH connection in XPipe. If it is not accessible from the outside, you can set the parent PVE system as an SSH gateway. diff --git a/lang/proc/texts/proxmoxUsername_de.md b/lang/proc/texts/proxmoxUsername_de.md new file mode 100644 index 000000000..2c65d8a86 --- /dev/null +++ b/lang/proc/texts/proxmoxUsername_de.md @@ -0,0 +1,5 @@ +## Benutzername + +Der Benutzername, mit dem du dich anmeldest. XPipe versucht, sich über SSH mit den angegebenen Anmeldedaten zu verbinden. + +Wenn kein SSH-Server läuft, wird versucht, den installierten SSH-Server zu starten. Beachte, dass du dieses Verhalten im Menü Sicherheitseinstellungen deaktivieren kannst. diff --git a/lang/proc/texts/proxmoxUsername_en.md b/lang/proc/texts/proxmoxUsername_en.md new file mode 100644 index 000000000..e3bbed86a --- /dev/null +++ b/lang/proc/texts/proxmoxUsername_en.md @@ -0,0 +1,5 @@ +## Username + +The username to log in as. XPipe will attempt to connect via SSH using the provided credentials. + +If no SSH server is running it will attempt to start the installed SSH server. Note that you can disable this behavior in the security settings menu. diff --git a/lang/proc/texts/rdpPasswordAuthentication_en.md b/lang/proc/texts/rdpPasswordAuthentication_en.md new file mode 100644 index 000000000..dc0a52e0a --- /dev/null +++ b/lang/proc/texts/rdpPasswordAuthentication_en.md @@ -0,0 +1,3 @@ +## RDP Password Authentication + +Not every available RDP client supports automatically supplying passwords. If your currently selected client does not support this feature, you will still have to enter the password manually when connecting. \ No newline at end of file diff --git a/lang/proc/texts/rdpTunnelHost_en.md b/lang/proc/texts/rdpTunnelHost_en.md new file mode 100644 index 000000000..ac56ca932 --- /dev/null +++ b/lang/proc/texts/rdpTunnelHost_en.md @@ -0,0 +1,5 @@ +## RDP Tunnel Host + +You can choose to connect to a remote RDP host via an SSH tunnel. This gives you the ability to use the more advanced SSH authentication features with RDP out of the box. + +When this option is used, the host address in the RDP file will be replaced by the chosen hostname of the SSH connection. Upon first connection, an SSH tunnel will be established and the RDP client will connect to the tunneled connection via localhost instead. \ No newline at end of file diff --git a/lang/proc/texts/runTempContainer_de.md b/lang/proc/texts/runTempContainer_de.md new file mode 100644 index 000000000..3673bba1c --- /dev/null +++ b/lang/proc/texts/runTempContainer_de.md @@ -0,0 +1,5 @@ +## Temporäre Container + +Hiermit wird ein temporärer Container mit dem angegebenen Image gestartet, der automatisch entfernt wird, sobald er gestoppt wird. Der Container läuft auch dann weiter, wenn im Container-Image kein Befehl angegeben ist, der ausgeführt werden soll. + +Das kann nützlich sein, wenn du schnell eine bestimmte Umgebung mit einem bestimmten Container-Image einrichten willst. Du kannst den Container dann wie gewohnt in XPipe betreten, deine Operationen durchführen und den Container stoppen, sobald er nicht mehr benötigt wird. Er wird dann automatisch entfernt. \ No newline at end of file diff --git a/lang/proc/texts/runTempContainer_en.md b/lang/proc/texts/runTempContainer_en.md new file mode 100644 index 000000000..52dc0a62f --- /dev/null +++ b/lang/proc/texts/runTempContainer_en.md @@ -0,0 +1,5 @@ +## Temporary containers + +This will run a temporary container using the specified image that will get automatically removed once it is stopped. The container will keep running even if the container image does not have any command specified that will run. + +This can be useful if you quickly want to set up a certain environment by using a certain container image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically. \ No newline at end of file diff --git a/lang/proc/texts/shellCommand_de.md b/lang/proc/texts/shellCommand_de.md new file mode 100644 index 000000000..eed4ef211 --- /dev/null +++ b/lang/proc/texts/shellCommand_de.md @@ -0,0 +1,30 @@ +## Benutzerdefinierte Shell-Verbindungen + +Öffnet eine Shell mit dem benutzerdefinierten Befehl, indem es den angegebenen Befehl auf dem ausgewählten Hostsystem ausführt. Diese Shell kann entweder lokal oder remote sein. + +Beachte, dass diese Funktion erwartet, dass die Shell von einem Standardtyp wie `cmd`, `bash`, etc. ist. Wenn du andere Arten von Shells und Befehlen in einem Terminal öffnen willst, kannst du stattdessen den benutzerdefinierten Terminalbefehlstyp verwenden. Wenn du Standardshells verwendest, kannst du diese Verbindung auch im Dateibrowser öffnen. + +### Interaktive Eingabeaufforderungen + +Der Shell-Prozess kann eine Zeitüberschreitung verursachen oder sich aufhängen, wenn eine unerwartete +eingabeaufforderung, wie z. B. eine Passwortabfrage. Deshalb solltest du immer darauf achten, dass es keine interaktiven Eingabeaufforderungen gibt. + +Ein Befehl wie `ssh user@host` funktioniert hier zum Beispiel problemlos, solange kein Passwort verlangt wird. + +### Benutzerdefinierte lokale Shells + +In vielen Fällen ist es sinnvoll, eine Shell mit bestimmten Optionen zu starten, die normalerweise standardmäßig deaktiviert sind, damit einige Skripte und Befehle richtig funktionieren. Zum Beispiel: + +- [Verzögerte Erweiterung in + cmd](https://ss64.com/nt/delayedexpansion.html) +- [Powershell-Ausführung + richtlinien](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3) +- [Bash POSIX + Modus](https://www.gnu.org/software/bash/manual/html_node/Bash-POSIX-Mode.html) +- Und jede andere mögliche Startoption für eine Shell deiner Wahl + +Dies kannst du erreichen, indem du benutzerdefinierte Shell-Befehle erstellst, zum Beispiel mit den folgenden Befehlen: + +- `cmd /v` +- `powershell -ExecutionMode Bypass` +- `bash --posix` \ No newline at end of file diff --git a/lang/proc/texts/shellCommand_en.md b/lang/proc/texts/shellCommand_en.md new file mode 100644 index 000000000..cd19fcc8c --- /dev/null +++ b/lang/proc/texts/shellCommand_en.md @@ -0,0 +1,30 @@ +## Custom shell connections + +Opens a shell using the custom command by executing the given command on the selected host system. This shell can either be local or remote. + +Note that this functionality expects the shell to be of a standard type such as `cmd`, `bash`, etc. If you want to open any other types of shells and commands in a terminal, you can use the custom terminal command type instead. Using standard shells allows you to also open this connection in the file browser. + +### Interactive prompts + +The shell process might time out or hang in case there is an unexpected required +input prompt, like a password prompt. Therefore, you should always make sure that there are no interactive input prompts. + +For example, a command like `ssh user@host` will work fine here as long there is no password required. + +### Custom local shells + +In many cases, it is useful to launch a shell with certain options that are usually disabled by default in order to make some scripts and commands work properly. For example: + +- [Delayed Expansion in + cmd](https://ss64.com/nt/delayedexpansion.html) +- [Powershell execution + policies](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3) +- [Bash POSIX + Mode](https://www.gnu.org/software/bash/manual/html_node/Bash-POSIX-Mode.html) +- And any other possible launch option for a shell of your choice + +This can be achieved by creating custom shell commands with for example the following commands: + +- `cmd /v` +- `powershell -ExecutionMode Bypass` +- `bash --posix` \ No newline at end of file diff --git a/lang/proc/texts/sshConfigs_de.md b/lang/proc/texts/sshConfigs_de.md new file mode 100644 index 000000000..464f4ebf7 --- /dev/null +++ b/lang/proc/texts/sshConfigs_de.md @@ -0,0 +1,9 @@ +### SSH-Konfigurationen + +XPipe lädt alle Hosts und wendet alle Einstellungen an, die du in der ausgewählten Datei konfiguriert hast. Wenn du also eine Konfigurationsoption entweder auf globaler oder hostspezifischer Basis angibst, wird sie automatisch auf die von XPipe aufgebaute Verbindung angewendet. + +Wenn du mehr über die Verwendung von SSH-Konfigurationen erfahren möchtest, kannst du `man ssh_config` verwenden oder diese [Anleitung](https://www.ssh.com/academy/ssh/config) lesen. + +### Identitäten + +Beachte, dass du hier auch eine `IdentityFile` Option angeben kannst. Wenn du hier eine Identität angibst, werden alle anderen Identitäten, die weiter unten angegeben werden, ignoriert. \ No newline at end of file diff --git a/lang/proc/texts/sshConfigs_en.md b/lang/proc/texts/sshConfigs_en.md new file mode 100644 index 000000000..f2b34a1e5 --- /dev/null +++ b/lang/proc/texts/sshConfigs_en.md @@ -0,0 +1,9 @@ +### SSH configs + +XPipe loads all hosts and applies all settings that you have configured in the selected file. So by specifying a configuration option on either a global or host-specific basis, it will automatically be applied to the connection established by XPipe. + +If you want to learn more about how to use SSH configs, you can use `man ssh_config` or read this [guide](https://www.ssh.com/academy/ssh/config). + +### Identities + +Note that you can also specify an `IdentityFile` option in here. If any identity is specified in here, any otherwise specified identity later down below will be ignored. \ No newline at end of file diff --git a/lang/proc/texts/sshDynamicTunnelBinding_de.md b/lang/proc/texts/sshDynamicTunnelBinding_de.md new file mode 100644 index 000000000..3635f49fa --- /dev/null +++ b/lang/proc/texts/sshDynamicTunnelBinding_de.md @@ -0,0 +1,5 @@ +## Tunnelbindung + +Die Bindungsinformationen, die du angibst, werden direkt an den `ssh`-Client wie folgt weitergegeben: `-D [Adresse:]Port`. + +Standardmäßig wird die Adresse an die Loopback-Schnittstelle gebunden. Du kannst auch beliebige Platzhalter für die Adresse verwenden, z.B. die Adresse `0.0.0.0`, um an alle Netzwerkschnittstellen zu binden, die über IPv4 erreichbar sind. Wenn du die Adresse komplett weglässt, wird der Platzhalter `*` verwendet, der Verbindungen zu allen Netzwerkschnittstellen erlaubt. Beachte, dass manche Netzwerkschnittstellen-Notation nicht von allen Betriebssystemen unterstützt wird. Windows-Server zum Beispiel unterstützen den Platzhalter `*` nicht. diff --git a/lang/proc/texts/sshDynamicTunnelBinding_en.md b/lang/proc/texts/sshDynamicTunnelBinding_en.md new file mode 100644 index 000000000..91653cc00 --- /dev/null +++ b/lang/proc/texts/sshDynamicTunnelBinding_en.md @@ -0,0 +1,5 @@ +## Tunnel binding + +The binding information you provide is passed straight to the `ssh` client as follows: `-D [address:]port`. + +By default, the address will bind to the loopback interface. You can also make use of any address wildcards, e.g. setting the address to `0.0.0.0` in order to bind to all network interfaces accessible via IPv4. When you completely omit the address, the wildcard `*`, which allows connections on all network interfaces, will be used. Note that some network interfaces notation might not be supported on all operating systems. Windows servers for example don't support the wildcard `*`. diff --git a/lang/proc/texts/sshDynamicTunnelOrigin_de.md b/lang/proc/texts/sshDynamicTunnelOrigin_de.md new file mode 100644 index 000000000..c91cb3f32 --- /dev/null +++ b/lang/proc/texts/sshDynamicTunnelOrigin_de.md @@ -0,0 +1,5 @@ +## Tunnelherkunft + +XPipe ist völlig flexibel, wenn es darum geht, wo ein Befehl ausgeführt werden soll. Deshalb kannst du einen Tunnel nicht nur auf deinem lokalen Rechner, sondern auf jedem beliebigen System einrichten. + +Der Tunnel-Opener-Befehl wird auf dem System ausgeführt, das du hier angibst. Du musst also einen `ssh`-Client auf diesem System installiert haben. Wenn der Ursprung nicht der lokale Rechner ist, hält XPipe im Hintergrund eine Verbindung zu diesem entfernten System offen, um den Tunnel zu verwalten. \ No newline at end of file diff --git a/lang/proc/texts/sshDynamicTunnelOrigin_en.md b/lang/proc/texts/sshDynamicTunnelOrigin_en.md new file mode 100644 index 000000000..cf69836ae --- /dev/null +++ b/lang/proc/texts/sshDynamicTunnelOrigin_en.md @@ -0,0 +1,5 @@ +## Tunnel Origin + +XPipe is fully flexible with regards on where to execute a command. Therefore, you can establish a tunnel on any system in addition to your local machine. + +The tunnel opener command will be executed on the system you specify here, so you need to have an `ssh` client installed on that system. If the origin is not the local machine, XPipe will keep a connection open to that remote system in the background to manage the tunnel. \ No newline at end of file diff --git a/lang/proc/texts/sshGateway_de.md b/lang/proc/texts/sshGateway_de.md new file mode 100644 index 000000000..cfbfa57e5 --- /dev/null +++ b/lang/proc/texts/sshGateway_de.md @@ -0,0 +1,9 @@ +## Shell-Verbindungsgateways + +Wenn diese Option aktiviert ist, öffnet XPipe zuerst eine Shell-Verbindung zum Gateway und von dort aus eine SSH-Verbindung zum angegebenen Host. Der `ssh`-Befehl muss verfügbar sein und sich im `PATH` des gewählten Gateways befinden. + +### Server springen + +Dieser Mechanismus ist den Jump Servern ähnlich, aber nicht gleichwertig. Er ist völlig unabhängig vom SSH-Protokoll, so dass du jede Shell-Verbindung als Gateway verwenden kannst. + +Wenn du auf der Suche nach richtigen SSH-Sprungservern bist, vielleicht auch in Kombination mit einer Agentenweiterleitung, verwende die benutzerdefinierte SSH-Verbindungsfunktion mit der Konfigurationsoption `ProxyJump`. \ No newline at end of file diff --git a/lang/proc/texts/sshGateway_en.md b/lang/proc/texts/sshGateway_en.md new file mode 100644 index 000000000..e74a287b4 --- /dev/null +++ b/lang/proc/texts/sshGateway_en.md @@ -0,0 +1,9 @@ +## Shell connection gateways + +If enabled, XPipe first opens a shell connection to the gateway and from there opens a SSH connection to the specified host. The `ssh` command must be available and located in the `PATH` on your chosen gateway. + +### Jump servers + +This mechanism is similar to jump servers, but not equivalent. It is completely independent of the SSH protocol, so you can use any shell connection as a gateway. + +If you are looking for proper SSH jump servers, maybe also in combination with agent forwarding, use the custom SSH connection functionality with the `ProxyJump` configuration option. \ No newline at end of file diff --git a/lang/proc/texts/sshKey_de.md b/lang/proc/texts/sshKey_de.md new file mode 100644 index 000000000..7ab8fbd45 --- /dev/null +++ b/lang/proc/texts/sshKey_de.md @@ -0,0 +1,55 @@ +### Keine + +Deaktiviert die `publickey`-Authentifizierung. + +### SSH-Agent + +Wenn deine Identitäten im SSH-Agenten gespeichert sind, kann das ssh-Programm sie verwenden, wenn der Agent gestartet ist. +XPipe startet den Agentenprozess automatisch, wenn er noch nicht läuft. + +### Pageant (Windows) + +Wenn du Pageant unter Windows verwendest, prüft XPipe zuerst, ob Pageant läuft. +Aufgrund der Natur von Pageant liegt es in deiner Verantwortung, dass es +da du jedes Mal alle Schlüssel, die du hinzufügen möchtest, manuell eingeben musst. +Wenn es läuft, übergibt XPipe die richtig benannte Pipe über +`-oIdentityAgent=...` an ssh weiter, du musst keine eigenen Konfigurationsdateien einbinden. + +Beachte, dass es einige Implementierungsfehler im OpenSSH-Client gibt, die Probleme verursachen können +wenn dein Benutzername Leerzeichen enthält oder zu lang ist. + +### Pageant (Linux & macOS) + +Wenn deine Identitäten im Pageant-Agent gespeichert sind, kann das ssh-Programm sie verwenden, wenn der Agent gestartet wird. +XPipe startet den Agentenprozess automatisch, wenn er noch nicht läuft. + +### Identitätsdatei + +Du kannst auch eine Identitätsdatei mit einer optionalen Passphrase angeben. +Diese Option ist das Äquivalent zu `ssh -i `. + +Beachte, dass dies der *private* Schlüssel sein sollte, nicht der öffentliche. +Wenn du das verwechselst, wird dir ssh nur kryptische Fehlermeldungen geben. + +### GPG Agent + +Wenn deine Identitäten zum Beispiel auf einer Smartcard gespeichert sind, kannst du sie dem SSH-Client über den `gpg-agent` zur Verfügung stellen. +Diese Option aktiviert automatisch die SSH-Unterstützung des Agenten, falls sie noch nicht aktiviert ist, und startet den GPG-Agent-Daemon mit den richtigen Einstellungen neu. + +### Yubikey PIV + +Wenn deine Identitäten mit der PIV-Chipkartenfunktion des Yubikey gespeichert sind, kannst du sie mit +kannst du sie mit der YKCS11-Bibliothek von Yubico abrufen, die im Lieferumfang des Yubico PIV Tools enthalten ist. + +Beachte, dass du eine aktuelle Version von OpenSSH benötigst, um diese Funktion nutzen zu können. + +### Benutzerdefinierter Agent + +Du kannst auch einen benutzerdefinierten Agenten verwenden, indem du hier entweder den Socket-Speicherort oder den benannten Pipe-Speicherort angibst. +Er wird dann über die Option `IdentityAgent` übergeben. + +### Benutzerdefinierte PKCS#11-Bibliothek + +Hiermit wird der OpenSSH-Client angewiesen, die angegebene Shared-Library-Datei zu laden, die die Authentifizierung übernimmt. + +Beachte, dass du einen aktuellen Build von OpenSSH brauchst, um diese Funktion zu nutzen. diff --git a/lang/proc/texts/sshKey_en.md b/lang/proc/texts/sshKey_en.md new file mode 100644 index 000000000..a70daef48 --- /dev/null +++ b/lang/proc/texts/sshKey_en.md @@ -0,0 +1,55 @@ +### None + +Disables `publickey` authentication. + +### SSH-Agent + +In case your identities are stored in the SSH-Agent, the ssh executable can use them if the agent is started. +XPipe will automatically start the agent process if it is not running yet. + +### Pageant (Windows) + +In case you are using pageant on Windows, XPipe will check whether pageant is running first. +Due to the nature of pageant, it is your responsibility to have it +running as you manually have to specify all keys you would like to add every time. +If it is running, XPipe will pass the proper named pipe via +`-oIdentityAgent=...` to ssh, you don't have to include any custom config files. + +Note that there are some implementation bugs in the OpenSSH client that can cause issues +if your username contains spaces or is too long, so try to use the latest version. + +### Pageant (Linux & macOS) + +In case your identities are stored in the pageant agent, the ssh executable can use them if the agent is started. +XPipe will automatically start the agent process if it is not running yet. + +### Identity file + +You can also specify an identity file with an optional passphrase. +This option is the equivalent of `ssh -i `. + +Note that this should be the *private* key, not the public one. +If you mix that up, ssh will only give you cryptic error messages. + +### GPG Agent + +If your identities are stored for example on a smartcard, you can choose to provide them to the SSH client via the `gpg-agent`. +This option will automatically enable SSH support of the agent if not enabled yet and restart the GPG agent daemon with the correct settings. + +### Yubikey PIV + +If your identities are stored with the PIV smart card function of the Yubikey, you can retreive +them with Yubico's YKCS11 library, which comes bundled with Yubico PIV Tool. + +Note that you need an up-to-date build of OpenSSH in order to use this feature. + +### Custom agent + +You can also use a custom agent by providing either the socket location or named pipe location here. +This will pass it via the `IdentityAgent` option. + +### Custom PKCS#11 library + +This will instruct the OpenSSH client to load the specified shared library file, which will handle the authentication. + +Note that you need an up-to-date build of OpenSSH in order to use this feature. diff --git a/lang/proc/texts/sshLocalTunnelBinding_de.md b/lang/proc/texts/sshLocalTunnelBinding_de.md new file mode 100644 index 000000000..cc3556e7f --- /dev/null +++ b/lang/proc/texts/sshLocalTunnelBinding_de.md @@ -0,0 +1,5 @@ +## Bindung + +Die Bindungsinformationen, die du angibst, werden direkt an den `ssh`-Client wie folgt übergeben: `-L [origin_address:]origin_port:remote_address:remote_port`. + +Standardmäßig wird der Ursprung an die Loopback-Schnittstelle gebunden, wenn nicht anders angegeben. Du kannst auch beliebige Adressplatzhalter verwenden, z.B. indem du die Adresse auf `0.0.0.0` setzt, um an alle Netzwerkschnittstellen zu binden, die über IPv4 erreichbar sind. Wenn du die Adresse komplett weglässt, wird der Platzhalter `*` verwendet, der Verbindungen zu allen Netzwerkschnittstellen erlaubt. Beachte, dass manche Netzwerkschnittstellen-Notation nicht von allen Betriebssystemen unterstützt wird. Windows-Server zum Beispiel unterstützen den Platzhalter `*` nicht. diff --git a/lang/proc/texts/sshLocalTunnelBinding_en.md b/lang/proc/texts/sshLocalTunnelBinding_en.md new file mode 100644 index 000000000..f2b5046fc --- /dev/null +++ b/lang/proc/texts/sshLocalTunnelBinding_en.md @@ -0,0 +1,5 @@ +## Binding + +The binding information you provide is passed straight to the `ssh` client as follows: `-L [origin_address:]origin_port:remote_address:remote_port`. + +By default, the origin will bind to the loopback interface if not specified otherwise. You can also make use of any address wildcards, e.g. setting the address to `0.0.0.0` in order to bind to all network interfaces accessible via IPv4. When you completely omit the address, the wildcard `*`, which allows connections on all network interfaces, will be used. Note that some network interfaces notation might not be supported on all operating systems. Windows servers for example don't support the wildcard `*`. diff --git a/lang/proc/texts/sshLocalTunnelOrigin_de.md b/lang/proc/texts/sshLocalTunnelOrigin_de.md new file mode 100644 index 000000000..684ba0b8a --- /dev/null +++ b/lang/proc/texts/sshLocalTunnelOrigin_de.md @@ -0,0 +1,7 @@ +## Tunnelherkunft + +XPipe ist völlig flexibel, wenn es darum geht, wo ein Befehl ausgeführt werden soll. +Deshalb kannst du einen Tunnel nicht nur auf deinem lokalen Rechner, sondern auch auf jedem anderen System starten. + +Der Befehl zum Öffnen des Tunnels wird auf dem System ausgeführt, das du hier angibst. Du musst also einen `ssh`-Client auf diesem System installiert haben. +Wenn der Ursprung nicht der lokale Rechner ist, hält XPipe im Hintergrund eine Verbindung zu diesem entfernten System offen, um den Tunnel zu verwalten. \ No newline at end of file diff --git a/lang/proc/texts/sshLocalTunnelOrigin_en.md b/lang/proc/texts/sshLocalTunnelOrigin_en.md new file mode 100644 index 000000000..61b81e8d2 --- /dev/null +++ b/lang/proc/texts/sshLocalTunnelOrigin_en.md @@ -0,0 +1,7 @@ +## Tunnel Origin + +XPipe is fully flexible with regards on where to execute a command. +Therefore, you can establish a tunnel starting on any remote system in addition to your local machine. + +The tunnel opener command will be executed on the system you specify here, so you need to have an `ssh` client installed on that system. +If the origin is not the local machine, XPipe will keep a connection open to that remote system in the background to manage the tunnel. \ No newline at end of file diff --git a/lang/proc/texts/sshOptions_de.md b/lang/proc/texts/sshOptions_de.md new file mode 100644 index 000000000..03d534714 --- /dev/null +++ b/lang/proc/texts/sshOptions_de.md @@ -0,0 +1,22 @@ +## SSH-Konfigurationen + +Hier kannst du alle SSH-Optionen angeben, die an die Verbindung übergeben werden sollen. +Während einige Optionen für einen erfolgreichen Verbindungsaufbau unbedingt erforderlich sind, wie z.B. `HostName`, +sind viele andere Optionen rein optional. + +Um einen Überblick über alle möglichen Optionen zu bekommen, kannst du [`man ssh_config`](https://linux.die.net/man/5/ssh_config) verwenden oder diesen [guide](https://www.ssh.com/academy/ssh/config) lesen. +Die genaue Anzahl der unterstützten Optionen hängt ausschließlich von deinem installierten SSH-Client ab. + +### Formatierung + +Der Inhalt hier entspricht einem Host-Abschnitt in einer SSH-Konfigurationsdatei. +Beachte, dass du den `Host`-Eintrag nicht explizit definieren musst, denn das wird automatisch erledigt. + +Wenn du mehr als einen Host-Abschnitt definieren willst, z. B. bei abhängigen Verbindungen wie einem Proxy-Jump-Host, der von einem anderen Config-Host abhängt, solltest du stattdessen eine richtige SSH-Konfigurationsdatei verwenden, da hier nur genau eine Host-Definition unterstützt wird. + +Du musst keine Formatierung mit Leerzeichen oder Einrückung vornehmen, das ist für die Funktion nicht erforderlich. + +### Identitäten + +Beachte, dass du hier auch eine `IdentityFile` Option angeben kannst. +Wenn diese Option hier angegeben wird, werden alle anderen Optionen für die schlüsselbasierte Authentifizierung weiter unten ignoriert. diff --git a/lang/proc/texts/sshOptions_en.md b/lang/proc/texts/sshOptions_en.md new file mode 100644 index 000000000..4724ab223 --- /dev/null +++ b/lang/proc/texts/sshOptions_en.md @@ -0,0 +1,22 @@ +## SSH configurations + +Here you can specify any SSH options that should be passed to the connection. +While some options are essentially required to successfully establish a connection, such as `HostName`, +many other options are purely optional. + +To get an overview over all possible options, you can use [`man ssh_config`](https://linux.die.net/man/5/ssh_config) or read this [guide](https://www.ssh.com/academy/ssh/config). +The exact amount of supported options purely depends on your installed SSH client. + +### Formatting + +The content here is equivalent to one host section in an SSH config file. +Note that you don't have to explicitly define the `Host` entry, as that will be done automatically. + +If you intend to define more than one host section, e.g. with dependent connections such as a proxy jump host that depends on another config host, you should use a proper SSH config file instead as only exactly one host definition is supported here. + +You don't have to perform any formatting with whitespace or indentation, this is not needed for it to function. + +### Identities + +Note that you can also specify an `IdentityFile` option in here. +If this option is specified in here, any otherwise specified key-based authentication option later down below will be ignored. diff --git a/lang/proc/texts/sshRemoteTunnelBinding_de.md b/lang/proc/texts/sshRemoteTunnelBinding_de.md new file mode 100644 index 000000000..2023df54a --- /dev/null +++ b/lang/proc/texts/sshRemoteTunnelBinding_de.md @@ -0,0 +1,5 @@ +## Bindung + +Die Bindungsinformationen, die du angibst, werden direkt an den `ssh`-Client wie folgt weitergegeben: `-R [remote_source_address:]remote_source_port:origin_destination_address:origin_destination_port`. + +Standardmäßig wird die entfernte Quelladresse an die Loopback-Schnittstelle gebunden. Du kannst auch beliebige Adressplatzhalter verwenden, z.B. die Adresse `0.0.0.0`, um an alle über IPv4 erreichbaren Netzwerkschnittstellen zu binden. Wenn du die Adresse komplett weglässt, wird der Platzhalter `*` verwendet, der Verbindungen zu allen Netzwerkschnittstellen erlaubt. Beachte, dass manche Netzwerkschnittstellen-Notation nicht von allen Betriebssystemen unterstützt wird. Windows-Server zum Beispiel unterstützen den Platzhalter `*` nicht. diff --git a/lang/proc/texts/sshRemoteTunnelBinding_en.md b/lang/proc/texts/sshRemoteTunnelBinding_en.md new file mode 100644 index 000000000..58bb0ef56 --- /dev/null +++ b/lang/proc/texts/sshRemoteTunnelBinding_en.md @@ -0,0 +1,5 @@ +## Binding + +The binding information you provide is passed straight to the `ssh` client as follows: `-R [remote_source_address:]remote_source_port:origin_destination_address:origin_destination_port`. + +By default, the remote source address will bind to the loopback interface. You can also make use of any address wildcards, e.g. setting the address to `0.0.0.0` in order to bind to all network interfaces accessible via IPv4. When you completely omit the address, the wildcard `*`, which allows connections on all network interfaces, will be used. Note that some network interfaces notation might not be supported on all operating systems. Windows servers for example don't support the wildcard `*`. diff --git a/lang/proc/texts/sshRemoteTunnelOrigin_de.md b/lang/proc/texts/sshRemoteTunnelOrigin_de.md new file mode 100644 index 000000000..9813d9163 --- /dev/null +++ b/lang/proc/texts/sshRemoteTunnelOrigin_de.md @@ -0,0 +1,7 @@ +## Tunnelherkunft + +XPipe ist völlig flexibel, wenn es darum geht, wo ein Befehl ausgeführt werden soll. +Deshalb kannst du einen Tunnel nicht nur auf deinem lokalen Rechner, sondern auf jedem beliebigen System einrichten. + +Der Tunnel-Opener-Befehl wird auf dem System ausgeführt, das du hier angibst. Du musst also einen `ssh`-Client auf diesem System installiert haben. +Wenn der Ursprung nicht der lokale Rechner ist, hält XPipe im Hintergrund eine Verbindung zu diesem entfernten System offen, um den Tunnel zu verwalten. \ No newline at end of file diff --git a/lang/proc/texts/sshRemoteTunnelOrigin_en.md b/lang/proc/texts/sshRemoteTunnelOrigin_en.md new file mode 100644 index 000000000..d82ace729 --- /dev/null +++ b/lang/proc/texts/sshRemoteTunnelOrigin_en.md @@ -0,0 +1,7 @@ +## Tunnel Origin + +XPipe is fully flexible with regards on where to execute a command. +Therefore, you can establish a tunnel on any system in addition to your local machine. + +The tunnel opener command will be executed on the system you specify here, so you need to have an `ssh` client installed on that system. +If the origin is not the local machine, XPipe will keep a connection open to that remote system in the background to manage the tunnel. \ No newline at end of file diff --git a/lang/proc/texts/unknownShell_de.md b/lang/proc/texts/unknownShell_de.md new file mode 100644 index 000000000..2b7e2ec4b --- /dev/null +++ b/lang/proc/texts/unknownShell_de.md @@ -0,0 +1,7 @@ +## Shell-Typ-Erkennung + +XPipe arbeitet, indem es den Shell-Typ der Verbindung erkennt und dann mit der aktiven Shell interagiert. Dieser Ansatz funktioniert jedoch nur, wenn der Shell-Typ bekannt ist und eine bestimmte Anzahl von Aktionen und Befehlen unterstützt. Alle gängigen Shells wie `bash`, `cmd`, `powershell` und andere werden unterstützt. + +## Unbekannte Shell-Typen + +Wenn du eine Verbindung zu einem System herstellst, auf dem keine bekannte Befehlsshell läuft, z.B. ein Router, Link oder ein IOT-Gerät, kann XPipe den Shell-Typ nicht erkennen und bricht nach einiger Zeit ab. Wenn du diese Option aktivierst, versucht XPipe nicht, den Shell-Typ zu erkennen und startet die Shell so, wie sie ist. Dadurch kannst du die Verbindung ohne Fehler öffnen, aber viele Funktionen, z. B. der Dateibrowser, Skripting, Unterverbindungen und mehr, werden für diese Verbindung nicht unterstützt. diff --git a/lang/proc/texts/unknownShell_en.md b/lang/proc/texts/unknownShell_en.md new file mode 100644 index 000000000..6e7e0b80f --- /dev/null +++ b/lang/proc/texts/unknownShell_en.md @@ -0,0 +1,7 @@ +## Shell type detection + +XPipe works by detecting the shell type of the connection and then interacting with the active shell. This approach only works however when the shell type is known and supports a certain amount of actions and commands. All common shells like `bash`, `cmd`, `powershell`, and more, are supported. + +## Unknown shell types + +If you are connecting to a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe will be unable to detect the shell type and error out after some time. By enabling this option, XPipe will not attempt to identify the shell type and launch the shell as-is. This allows you to open the connection without errors but many features, e.g. the file browser, scripting, subconnections, and more, will not be supported for this connection. diff --git a/lang/proc/texts/vmwarePassword_de.md b/lang/proc/texts/vmwarePassword_de.md new file mode 100644 index 000000000..ba5f4832d --- /dev/null +++ b/lang/proc/texts/vmwarePassword_de.md @@ -0,0 +1,4 @@ +### Passwort + +Um diese Funktion über die VMware-Schnittstelle nutzen zu können, ist ein Passwort erforderlich. +Wenn dein Gastbenutzerkonto derzeit kein Passwort hat, musst du eines festlegen. diff --git a/lang/proc/texts/vmwarePassword_en.md b/lang/proc/texts/vmwarePassword_en.md new file mode 100644 index 000000000..4a6e51023 --- /dev/null +++ b/lang/proc/texts/vmwarePassword_en.md @@ -0,0 +1,4 @@ +### Password + +Having a password is required in order to use this functionality via the VMware interface. +If your guest user account currently does not have a password, please set one. diff --git a/lang/proc/texts/vncTunnelHost_en.md b/lang/proc/texts/vncTunnelHost_en.md new file mode 100644 index 000000000..0bb681f3f --- /dev/null +++ b/lang/proc/texts/vncTunnelHost_en.md @@ -0,0 +1,5 @@ +## VNC Tunnel Host + +You can connect to a remote VNC host via an SSH tunnel. This gives you the ability to use the more advanced SSH authentication features with VNC out of the box. This is important as VNC is fundamentally insecure and unencrypted protocol. Tunneling provides the needed layer of security. + +You also don't have to worry about exposing the VNC port on your remote system as it only needs to be reachable via SSH. \ No newline at end of file diff --git a/lang/uacc/strings/translations_de.properties b/lang/uacc/strings/translations_de.properties new file mode 100644 index 000000000..5bc45feb0 --- /dev/null +++ b/lang/uacc/strings/translations_de.properties @@ -0,0 +1,34 @@ +community=Gemeinschaft +professional=Professionell +communityDescription=Ein Power-Tool für Verbindungen, das perfekt für deine persönlichen Anwendungsfälle ist. +professionalDescription=Professionelles Verbindungsmanagement für deine gesamte Serverinfrastruktur. +proPreview=Pro Vorschau +buyProfessional=Probiere XPipe professional aus +extendProfessional=Upgrade auf die neuesten professionellen Funktionen +communityItem1=Unbegrenzte Verbindungen zu nicht-kommerziellen Systemen und Tools +communityItem2=Nahtlose Integration mit deinen installierten Terminals und Editoren +communityItem3=Voll funktionsfähiger Remote-Dateibrowser +communityItem4=Leistungsstarkes Skripting-System für alle Shells +communityItem5=Git-Integration für die Synchronisierung und den Austausch von Verbindungsinformationen +professionalItem1=Alle Funktionen der Community Edition +professionalItem2=Unbegrenzte Verbindungen zu allen kommerziellen Systemen und Tools +professionalItem3=Unterstützung für Unternehmensauthentifizierungssysteme für Fernverbindungen +professionalItem4=Beinhaltet alle zukünftigen Funktionen der Professional Edition für 1 Jahr +professionalItem5=Erhält immer Funktions- und Sicherheitsupdates +status=Status +type=Typ +licenseAlertTitle=Kommerzielle Nutzung +useCommunity=Weiter mit Community +preview=Professionelle Vorschau +previewDescription=Teste die neuen Funktionen ein paar Wochen lang nach der Veröffentlichung. +tryPreview=Aktiviere die XPipe-Vorschau +previewItem1=Voller Zugang zu neu veröffentlichten professionellen Funktionen für 2 Wochen nach der Veröffentlichung +previewItem2=Enthält alle Funktionen der Community Edition +licensedTo=Lizensiert für +email=E-Mail Adresse +apply=Anwenden +clear=Klar +activate=Aktivieren Sie +validUntil=Gültig bis +licenseActivated=Lizenz aktiviert +restart=Neustart diff --git a/lang/uacc/strings/translations_en.properties b/lang/uacc/strings/translations_en.properties new file mode 100644 index 000000000..ada200c44 --- /dev/null +++ b/lang/uacc/strings/translations_en.properties @@ -0,0 +1,34 @@ +community=Community +professional=Professional +communityDescription=A connection power-tool perfect for your personal use cases. +professionalDescription=Professional connection management for your entire server infrastructure. +proPreview=Pro preview +buyProfessional=Try XPipe professional +extendProfessional=Upgrade to latest professional features +communityItem1=Unlimited connections to non-commercial systems and tools +communityItem2=Seamless integration with your installed terminals and editors +communityItem3=Fully featured remote file browser +communityItem4=Powerful scripting system for all shells +communityItem5=Git integration for synchronization and sharing connection information +professionalItem1=All community edition features +professionalItem2=Unlimited connections to all commercial systems and tools +professionalItem3=Support for enterprise authentication schemes for remote connections +professionalItem4=Includes all future professional edition features for 1 year +professionalItem5=Receives feature and security updates forever +status=Status +type=Type +licenseAlertTitle=Commercial usage +useCommunity=Continue with community +preview=Professional Preview +previewDescription=Try out new features for a couple of weeks after release. +tryPreview=Activate XPipe preview +previewItem1=Full access to newly released professional features for 2 weeks after release +previewItem2=Includes all community edition features +licensedTo=Licensed to +email=Email address +apply=Apply +clear=Clear +activate=Activate +validUntil=Valid until +licenseActivated=License activated +restart=Restart \ No newline at end of file diff --git a/lang/uacc/texts/preview_de.md b/lang/uacc/texts/preview_de.md new file mode 100644 index 000000000..3fb4e41ce --- /dev/null +++ b/lang/uacc/texts/preview_de.md @@ -0,0 +1,11 @@ +# XPipe Pro Vorschau + +Die Funktion, die du verwenden möchtest, ist in der Pro-Preview verfügbar. Diese ermöglicht es allen Interessierten, neu veröffentlichte Funktionen, die nur für Profis gelten, zwei Wochen lang nach ihrer Veröffentlichung auszuprobieren. Außerdem kannst du damit experimentieren, Feedback geben und einen Blick auf alle neuen Funktionen werfen, ohne etwas kaufen zu müssen. + +Als Nebeneffekt erhalten die neuen Funktionen auch eine bessere Testabdeckung, als es ohne die Vorschau für alle möglich wäre. + +## Überprüfung + +Sobald der Preview-Plan aktiviert ist, kommuniziert die Anwendung bei jedem Start mit dem Lizenzserver, um zu überprüfen, ob alles in Ordnung ist. Von dort holt sie sich auch die aktuellen Informationen darüber, welche Funktionen gerade neu sind und noch in der Vorschau enthalten sind. + +Du kannst die Vorschau auch jederzeit wieder deaktivieren. diff --git a/lang/uacc/texts/preview_en.md b/lang/uacc/texts/preview_en.md new file mode 100644 index 000000000..6876c8b21 --- /dev/null +++ b/lang/uacc/texts/preview_en.md @@ -0,0 +1,11 @@ +# XPipe Pro Preview + +The feature you are trying to use is available in the pro preview, which allows anyone who is interested to try it out newly released professional-only features for two weeks after their release. It also allows you to experiment, share feedback, and get a glimpse of all the new features without having to purchase anything. + +As a side effect, the new features also receive a better test coverage then would otherwise be possible without the preview for everyone. + +## Verification + +Once the preview plan is activated, the application will communicate with the license server on every startup to verify that everything is in order. From there, it also fetches the current information of which features are currently new and are still included in the preview. + +You can also disable the preview again at any time.