From ca0c8c632654f4acc4a0b2e637229d1527697202 Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 26 May 2026 20:27:04 +0000 Subject: [PATCH] Rework --- README.md | 4 +- app/build.gradle | 4 +- .../io/xpipe/app/action/ActionConfigComp.java | 7 +- .../xpipe/app/action/ActionConfirmComp.java | 2 +- .../app/action/QuickConnectProvider.java | 2 +- .../io/xpipe/app/beacon/AppBeaconServer.java | 30 +- .../app/beacon/BeaconRequestHandler.java | 7 +- .../app/beacon/impl/AskpassExchangeImpl.java | 10 +- .../io/xpipe/app/beacon/mcp/AppMcpServer.java | 62 +- .../xpipe/app/beacon/mcp/McpToolHandler.java | 16 +- .../io/xpipe/app/beacon/mcp/McpTools.java | 38 +- .../BrowserFileChooserSessionComp.java | 2 +- .../app/browser/BrowserFullSessionComp.java | 2 +- .../impl/TransferFilesActionProvider.java | 13 +- .../app/browser/file/BrowserClipboard.java | 1 - .../file/BrowserConnectionListComp.java | 7 + .../file/BrowserConnectionListFilterComp.java | 6 +- .../app/browser/file/BrowserFileListComp.java | 20 +- .../file/BrowserFileSystemTabModel.java | 3 +- .../browser/file/BrowserHistoryTabComp.java | 10 +- .../app/browser/file/BrowserNavBarComp.java | 4 +- .../file/BrowserTerminalDockTabModel.java | 18 +- .../browser/file/BrowserTransferModel.java | 12 + .../menu/impl/EditFileMenuProvider.java | 8 +- .../impl/OpenFileDefaultMenuProvider.java | 7 +- .../menu/impl/OpenFileWithMenuProvider.java | 8 - .../OpenNativeFileDetailsMenuProvider.java | 7 +- .../browser/menu/impl/RenameMenuProvider.java | 7 +- .../impl/compress/CompressMenuProvider.java | 31 +- .../impl/compress/GunzipActionProvider.java | 47 + .../impl/compress/GunzipUnixMenuProvider.java | 77 + .../impl/compress/GzipActionProvider.java | 44 + .../impl/compress/UntarActionProvider.java | 3 +- .../impl/compress/UnzipActionProvider.java | 6 +- .../app/comp/augment/ContextMenuAugment.java | 20 +- .../io/xpipe/app/comp/base/AppLayoutComp.java | 2 +- .../io/xpipe/app/comp/base/CheckBoxComp.java | 8 +- .../io/xpipe/app/comp/base/ChoiceComp.java | 20 +- .../xpipe/app/comp/base/ChoicePaneComp.java | 25 +- .../ContextualFileReferenceChoiceComp.java | 28 +- .../io/xpipe/app/comp/base/FilterComp.java | 20 +- .../app/comp/base/IntegratedTextAreaComp.java | 42 +- .../xpipe/app/comp/base/ListBoxViewComp.java | 13 +- .../io/xpipe/app/comp/base/MarkdownComp.java | 9 +- .../xpipe/app/comp/base/ModalOverlayComp.java | 11 +- .../app/comp/base/ModalOverlayStackComp.java | 6 +- .../io/xpipe/app/comp/base/OptionsComp.java | 13 +- .../xpipe/app/comp/base/PrettyImageComp.java | 7 +- .../xpipe/app/comp/base/SecretFieldComp.java | 6 +- .../xpipe/app/comp/base/SideMenuBarComp.java | 41 +- .../xpipe/app/comp/base/TestButtonComp.java | 66 + .../io/xpipe/app/comp/base/TextFieldComp.java | 12 - .../java/io/xpipe/app/core/AppCertStore.java | 295 +++ .../app/core/AppConfigurationDialog.java | 8 +- .../xpipe/app/core/AppDesktopIntegration.java | 46 +- .../io/xpipe/app/core/AppDisplayScale.java | 2 +- .../java/io/xpipe/app/core/AppInstance.java | 45 +- .../io/xpipe/app/core/AppLayoutModel.java | 11 +- .../java/io/xpipe/app/core/AppRestart.java | 58 +- .../java/io/xpipe/app/core/AppSystemInfo.java | 2 +- .../AppHardwareAccelerationDisableCheck.java | 1 - .../io/xpipe/app/core/mode/AppBaseMode.java | 6 +- .../io/xpipe/app/core/mode/AppGuiMode.java | 28 +- .../xpipe/app/core/mode/AppOperationMode.java | 4 +- .../xpipe/app/core/window/AppMainWindow.java | 27 +- .../app/core/window/AppModifiedStage.java | 8 +- .../xpipe/app/core/window/AppSideWindow.java | 14 +- .../xpipe/app/core/window/AppWindowStyle.java | 24 +- .../app/cred/CertificateKeyFileStrategy.java | 389 ++++ .../xpipe/app/cred/CustomAgentStrategy.java | 21 +- .../app/cred/CustomPkcs11LibraryStrategy.java | 122 - .../io/xpipe/app/cred/GpgAgentStrategy.java | 23 +- .../io/xpipe/app/cred/InPlaceKeyStrategy.java | 43 +- .../io/xpipe/app/cred/KeyFileStrategy.java | 53 +- .../io/xpipe/app/cred/NoIdentityStrategy.java | 8 +- .../xpipe/app/cred/OpenSshAgentStrategy.java | 17 +- .../app/cred/OtherExternalAgentStrategy.java | 71 - .../cred/OtherExternalIdentityStrategy.java | 43 + .../io/xpipe/app/cred/PageantStrategy.java | 17 +- .../cred/PasswordManagerAgentStrategy.java | 6 +- .../PasswordManagerInPlaceKeyStrategy.java | 83 - .../io/xpipe/app/cred/SecurityKeyImpl.java | 227 ++ .../xpipe/app/cred/SecurityKeyStrategy.java | 183 ++ .../app/cred/ShortLivedCertificateImpl.java | 274 +++ .../io/xpipe/app/cred/SshAgentKeyList.java | 16 +- .../xpipe/app/cred/SshAgentKeyListComp.java | 22 +- .../io/xpipe/app/cred/SshAgentTestComp.java | 4 +- .../app/cred/SshIdentityAgentStrategy.java | 13 +- .../app/cred/SshIdentityKeyListStrategy.java | 14 + .../app/cred/SshIdentityStateManager.java | 3 +- .../xpipe/app/cred/SshIdentityStrategy.java | 25 +- .../io/xpipe/app/cred/YubikeyPivStrategy.java | 87 - .../io/xpipe/app/ext/ContainerStoreState.java | 42 + .../app/ext/CountGroupStoreProvider.java | 41 +- .../app/ext/DataStoreCreationCategory.java | 27 +- .../io/xpipe/app/ext/DataStoreProvider.java | 7 +- .../main/java/io/xpipe/app/ext/GuiDialog.java | 2 +- .../xpipe/app/ext/ProcessControlProvider.java | 20 +- .../main/java/io/xpipe/app/ext/Session.java | 6 + .../java/io/xpipe/app/ext/ShellSession.java | 24 + .../ext/SingletonSessionStoreProvider.java | 7 +- .../io/xpipe/app/ext/StartOnInitStore.java | 43 +- .../hub/action/impl/InitHubLeafProvider.java | 6 +- .../action/impl/RefreshActionProvider.java | 6 +- .../app/hub/comp/DenseStoreEntryComp.java | 2 +- .../io/xpipe/app/hub/comp/OsLogoComp.java | 2 +- .../app/hub/comp/StandardStoreEntryComp.java | 2 +- .../xpipe/app/hub/comp/StoreCategoryComp.java | 27 +- .../app/hub/comp/StoreCategoryConfigComp.java | 93 +- .../app/hub/comp/StoreCategoryWrapper.java | 6 +- .../xpipe/app/hub/comp/StoreChoiceComp.java | 22 +- .../app/hub/comp/StoreChoicePopover.java | 49 +- .../app/hub/comp/StoreComboChoiceComp.java | 4 +- .../xpipe/app/hub/comp/StoreCreationComp.java | 5 +- .../app/hub/comp/StoreCreationDialog.java | 78 +- .../xpipe/app/hub/comp/StoreCreationMenu.java | 80 +- .../app/hub/comp/StoreCreationModel.java | 64 +- .../io/xpipe/app/hub/comp/StoreEntryComp.java | 48 +- .../hub/comp/StoreEntryListBatchBarComp.java | 190 +- .../app/hub/comp/StoreEntryListComp.java | 29 +- .../hub/comp/StoreEntryListOverviewComp.java | 16 +- .../xpipe/app/hub/comp/StoreEntryWrapper.java | 38 +- .../app/hub/comp/StoreFilterFieldComp.java | 16 +- .../xpipe/app/hub/comp/StoreFilterState.java | 21 +- .../app/hub/comp/StoreFilterStateComp.java | 16 +- .../app/hub/comp/StoreIconChoiceComp.java | 3 +- .../io/xpipe/app/hub/comp/StoreIntroComp.java | 14 +- .../xpipe/app/hub/comp/StoreLayoutComp.java | 2 +- .../app/hub/comp/StoreListChoiceComp.java | 20 +- .../xpipe/app/hub/comp/StoreQuickConnect.java | 2 +- .../io/xpipe/app/hub/comp/StoreViewState.java | 12 +- .../io/xpipe/app/icon/SystemIconCache.java | 3 - .../java/io/xpipe/app/issue/ErrorAction.java | 8 + .../java/io/xpipe/app/issue/ErrorEvent.java | 23 +- .../io/xpipe/app/issue/ErrorEventFactory.java | 28 +- .../io/xpipe/app/issue/ErrorHandlerComp.java | 10 +- .../io/xpipe/app/issue/GuiErrorHandler.java | 5 + .../xpipe/app/issue/SentryErrorHandler.java | 32 +- .../io/xpipe/app/issue/SyncErrorHandler.java | 3 +- .../xpipe/app/platform/ClipboardHelper.java | 8 +- .../io/xpipe/app/platform/NativeBridge.java | 26 +- .../io/xpipe/app/platform/OptionsBuilder.java | 40 +- .../io/xpipe/app/platform/PlatformState.java | 33 +- .../java/io/xpipe/app/platform/Validator.java | 20 + .../java/io/xpipe/app/prefs/ApiCategory.java | 3 + .../java/io/xpipe/app/prefs/AppPrefs.java | 72 +- .../java/io/xpipe/app/prefs/AppPrefsComp.java | 2 +- .../xpipe/app/prefs/AppPrefsSidebarComp.java | 26 +- .../app/prefs/ExternalApplicationType.java | 8 +- .../xpipe/app/prefs/ExternalEditorType.java | 278 ++- .../io/xpipe/app/prefs/HttpProxyCategory.java | 182 ++ .../java/io/xpipe/app/prefs/McpCategory.java | 20 +- .../app/prefs/PasswordManagerTestComp.java | 37 +- .../io/xpipe/app/prefs/SecurityCategory.java | 4 +- .../java/io/xpipe/app/prefs/SshCategory.java | 4 + .../java/io/xpipe/app/prefs/SyncCategory.java | 31 +- .../io/xpipe/app/prefs/SystemCategory.java | 19 +- .../io/xpipe/app/prefs/TerminalCategory.java | 102 +- .../io/xpipe/app/prefs/VaultCategory.java | 3 +- .../xpipe/app/prefs/WorkspaceEntryComp.java | 4 +- .../io/xpipe/app/prefs/WorkspaceManager.java | 29 +- .../app/prefs/WorkspaceOverviewComp.java | 2 +- .../xpipe/app/prefs/WorkspacesCategory.java | 8 +- .../io/xpipe/app/process/CommandControl.java | 2 + .../java/io/xpipe/app/process/LocalShell.java | 22 +- .../xpipe/app/process/ParentSystemAccess.java | 16 +- .../io/xpipe/app/process/ShellControl.java | 6 + .../io/xpipe/app/process/ShellDialect.java | 4 +- .../io/xpipe/app/process/ShellDialects.java | 4 +- .../xpipe/app/process/ShellLaunchCommand.java | 2 +- .../java/io/xpipe/app/process/ShellTemp.java | 8 +- .../app/process/WrapperShellControl.java | 12 + .../pwman/HashicorpVaultPasswordManager.java | 331 +-- .../app/pwman/KeeperPasswordManager.java | 27 +- .../app/pwman/OpenBaoPasswordManager.java | 91 + .../app/pwman/PassboltPasswordManager.java | 4 +- .../io/xpipe/app/pwman/PasswordManager.java | 18 + .../app/pwman/PasswordManagerCommand.java | 42 +- .../PasswordManagerKeyConfiguration.java | 8 +- .../app/pwman/PasswordManagerKeyStrategy.java | 30 +- .../app/pwman/PassworkPasswordManager.java | 12 +- .../io/xpipe/app/rdp/CustomRdpClient.java | 2 +- .../xpipe/app/rdp/DevolutionsRdpClient.java | 2 +- .../io/xpipe/app/rdp/ExternalRdpClient.java | 10 +- .../java/io/xpipe/app/rdp/FreeRdpClient.java | 2 +- .../java/io/xpipe/app/rdp/KrdcRdpClient.java | 60 + .../java/io/xpipe/app/rdp/MstscRdpClient.java | 141 +- .../io/xpipe/app/rdp/RdpLaunchConfig.java | 6 + .../io/xpipe/app/rdp/RemminaRdpClient.java | 19 +- .../app/rdp/RemoteDesktopAppRdpClient.java | 12 +- .../io/xpipe/app/rdp/WindowsAppRdpClient.java | 12 +- .../io/xpipe/app/secret/EncryptionKey.java | 62 +- .../io/xpipe/app/secret/EncryptionToken.java | 2 +- .../secret/SecretCustomCommandStrategy.java | 22 +- .../secret/SecretPasswordManagerStrategy.java | 25 +- .../xpipe/app/secret/SecretQueryFilter.java | 2 +- .../xpipe/app/secret/SecretQueryProgress.java | 37 +- .../xpipe/app/secret/VaultKeySecretValue.java | 2 +- .../io/xpipe/app/storage/DataStorage.java | 33 +- .../app/storage/DataStorageGroupStrategy.java | 8 +- .../app/storage/DataStorageVaultKey.java | 59 + .../app/storage/DataStoreCategoryConfig.java | 15 +- .../io/xpipe/app/storage/DataStoreEntry.java | 2 +- .../app/storage/ImpersistentStorage.java | 6 +- .../io/xpipe/app/storage/StandardStorage.java | 34 +- .../terminal/ConfigFileTerminalPrompt.java | 3 +- .../app/terminal/CustomTerminalType.java | 10 +- .../app/terminal/GhosttyTerminalType.java | 41 +- .../app/terminal/ITerm2TerminalType.java | 6 +- .../xpipe/app/terminal/MacOsTerminalType.java | 4 +- .../app/terminal/MobaXTermTerminalType.java | 2 +- .../terminal/ScreenTerminalMultiplexer.java | 2 +- .../io/xpipe/app/terminal/TerminalBanner.java | 90 + .../app/terminal/TerminalDockBrowserComp.java | 146 +- .../app/terminal/TerminalDockHubComp.java | 163 +- .../app/terminal/TerminalDockHubManager.java | 82 +- .../xpipe/app/terminal/TerminalDockView.java | 201 +- .../terminal/TerminalLaunchConfiguration.java | 3 +- .../app/terminal/TerminalLaunchRequest.java | 9 + .../xpipe/app/terminal/TerminalLauncher.java | 45 +- .../app/terminal/TerminalLauncherManager.java | 16 +- .../app/terminal/TerminalMultiplexer.java | 10 +- .../terminal/TerminalPaneConfiguration.java | 29 +- .../io/xpipe/app/terminal/TerminalPrompt.java | 9 +- .../io/xpipe/app/terminal/TerminalView.java | 61 +- .../xpipe/app/terminal/WarpTerminalType.java | 4 +- .../xpipe/app/terminal/WezTerminalType.java | 31 +- .../app/terminal/WindowsTerminalType.java | 11 +- .../terminal/ZellijTerminalMultiplexer.java | 2 +- .../io/xpipe/app/update/AppDownloads.java | 14 +- .../io/xpipe/app/update/ChocoUpdater.java | 36 +- .../io/xpipe/app/util/AppJacksonModule.java | 3 +- .../io/xpipe/app/util/AsciiArtConverter.java | 43 + .../java/io/xpipe/app/util/AskpassAlert.java | 10 +- .../app/util/CacheableConfiguration.java | 38 + .../java/io/xpipe/app/util/Checkable.java | 17 + .../ControllableWindowProcess.java} | 31 +- .../ControllableWindowsProcess.java} | 62 +- .../io/xpipe/app/util/DocumentationLink.java | 20 +- .../app/util/GithubReleaseDownloader.java | 10 +- .../xpipe/app/util/HashicorpVaultConfig.java | 269 +++ .../java/io/xpipe/app/util/HostHelper.java | 8 + .../java/io/xpipe/app/util/HttpHelper.java | 98 +- .../java/io/xpipe/app/util/HttpProxy.java | 87 + .../java/io/xpipe/app/util/LocalExec.java | 13 +- .../NativeMacOsWindowControl.java | 5 +- .../NativeWinWindowControl.java | 35 +- .../java/io/xpipe/app/util/OpenBaoConfig.java | 263 +++ .../xpipe/app/util/RemoteDesktopDockComp.java | 249 +++ .../util/RemoteDesktopDockContentEntry.java | 21 + .../app/util/RemoteDesktopDockEntry.java | 33 + .../xpipe/app/util/RemoteDesktopDockView.java | 179 ++ .../xpipe/app/util/RemoteDesktopWindow.java | 363 +++ .../java/io/xpipe/app/util/ScanDialog.java | 11 +- .../io/xpipe/app/util/ScanDialogBase.java | 8 +- .../xpipe/app/util/ScanSingleDialogComp.java | 8 +- .../io/xpipe/app/util/StoreStateFormat.java | 2 +- .../xpipe/app/util/TlsCertificateFormat.java | 214 ++ .../io/xpipe/app/util/WindowDockComp.java | 170 ++ .../io/xpipe/app/util/WindowDockListener.java | 12 + .../io/xpipe/app/util/WindowsRegistry.java | 12 + .../io/xpipe/app/vnc/ExternalVncClient.java | 29 +- .../io/xpipe/app/vnc/InternalVncClient.java | 39 +- .../java/io/xpipe/app/vnc/KrdcVncClient.java | 44 + app/src/main/java/module-info.java | 4 + .../io/xpipe/app/resources/style/bookmark.css | 4 + .../io/xpipe/app/resources/style/browser.css | 35 +- .../io/xpipe/app/resources/style/category.css | 4 + .../app/resources/style/options-comp.css | 4 + .../resources/style/remote-desktop-dock.css | 71 + .../io/xpipe/app/resources/style/style.css | 2 +- .../java/io/xpipe/beacon/BeaconConfig.java | 28 + build.gradle | 7 +- .../src/main/java/io/xpipe/core/FilePath.java | 13 +- .../java/io/xpipe/core/JacksonMapper.java | 1 + .../src/main/java/io/xpipe/core/KeyValue.java | 21 +- dist/changelog/23.0.1.md | 94 + dist/changelog/23.0.md | 105 + dist/changelog/23.1.md | 6 + dist/changelog/23.2.md | 1 + .../DesktopApplicationStoreProvider.java | 14 +- .../base/host/AbstractHostStoreProvider.java | 46 +- .../ext/base/host/HostAddressChoice.java | 35 +- .../ext/base/host/HostAddressChoiceComp.java | 2 + .../base/identity/IdentityApplyDialog.java | 34 +- .../IdentityApplyHubLeafProvider.java | 5 + .../base/identity/IdentityChoiceBuilder.java | 119 +- .../ext/base/identity/IdentityConvert.java | 86 + .../ext/base/identity/IdentitySelectComp.java | 41 +- .../base/identity/IdentityStoreProvider.java | 5 +- .../LocalIdentityConvertHubLeafProvider.java | 27 +- .../ext/base/identity/LocalIdentityStore.java | 1 - .../identity/LocalIdentityStoreProvider.java | 3 +- .../identity/MultiIdentityStoreProvider.java | 29 +- .../PasswordManagerIdentityStoreProvider.java | 6 +- .../identity/SyncedIdentityStoreProvider.java | 9 +- .../base/script/ScriptCollectionSource.java | 15 +- .../ScriptCollectionSourceStoreProvider.java | 7 +- .../script/ScriptDataStorageProvider.java | 7 +- .../ext/base/script/ScriptHierarchy.java | 18 +- .../ext/base/script/ScriptStoreProvider.java | 88 +- .../ext/base/script/ScriptTextSource.java | 44 +- .../AbstractServiceGroupStoreProvider.java | 40 +- .../service/AbstractServiceStoreProvider.java | 9 +- .../service/CustomServiceStoreProvider.java | 10 +- .../service/FixedServiceStoreProvider.java | 9 +- .../service/MappedServiceStoreProvider.java | 9 +- .../service/ServiceRefreshHubProvider.java | 4 +- .../ext/base/store/ShellStoreProvider.java | 5 +- .../base/store/SyncConfigHubLeafProvider.java | 6 +- .../ext/system/incus/IncusCommandView.java | 44 +- .../IncusContainerActionProviderMenu.java | 3 +- .../incus/IncusContainerStoreProvider.java | 29 +- .../incus/IncusInstallStoreProvider.java | 6 +- .../ext/system/lxd/LxdCmdStoreProvider.java | 6 +- .../xpipe/ext/system/lxd/LxdCommandView.java | 41 +- .../lxd/LxdContainerActionProviderMenu.java | 3 +- .../system/lxd/LxdContainerStoreProvider.java | 29 +- .../system/podman/PodmanCmdStoreProvider.java | 5 +- .../ext/system/podman/PodmanCommandView.java | 19 +- .../system/podman/PodmanContainerStore.java | 11 +- .../podman/PodmanContainerStoreProvider.java | 37 +- get-xpipe.sh | 6 +- gradle/gradle_scripts/extension.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 43764 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 7 +- gradlew.bat | 32 +- img/proc/networkProxy_icon-dark.svg | 55 + img/proc/networkProxy_icon.svg | 55 + lang/strings/fixed_en.properties | 8 +- lang/strings/translations_da.properties | 233 +- lang/strings/translations_de.properties | 269 ++- lang/strings/translations_en.properties | 175 +- lang/strings/translations_es.properties | 299 ++- lang/strings/translations_fr.properties | 259 ++- lang/strings/translations_id.properties | 241 +- lang/strings/translations_it.properties | 293 ++- lang/strings/translations_ja.properties | 355 ++- lang/strings/translations_ko.properties | 243 +- lang/strings/translations_nl.properties | 273 ++- lang/strings/translations_pl.properties | 241 +- lang/strings/translations_pt.properties | 257 ++- lang/strings/translations_ru.properties | 294 ++- lang/strings/translations_sv.properties | 257 ++- lang/strings/translations_tr.properties | 179 +- lang/strings/translations_vi.properties | 1957 +++++++++-------- lang/strings/translations_zh-Hans.properties | 232 +- lang/strings/translations_zh-Hant.properties | 313 ++- version | 2 +- 350 files changed, 12738 insertions(+), 5201 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/comp/base/TestButtonComp.java create mode 100644 app/src/main/java/io/xpipe/app/core/AppCertStore.java create mode 100644 app/src/main/java/io/xpipe/app/cred/CertificateKeyFileStrategy.java delete mode 100644 app/src/main/java/io/xpipe/app/cred/CustomPkcs11LibraryStrategy.java delete mode 100644 app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java create mode 100644 app/src/main/java/io/xpipe/app/cred/OtherExternalIdentityStrategy.java delete mode 100644 app/src/main/java/io/xpipe/app/cred/PasswordManagerInPlaceKeyStrategy.java create mode 100644 app/src/main/java/io/xpipe/app/cred/SecurityKeyImpl.java create mode 100644 app/src/main/java/io/xpipe/app/cred/SecurityKeyStrategy.java create mode 100644 app/src/main/java/io/xpipe/app/cred/ShortLivedCertificateImpl.java create mode 100644 app/src/main/java/io/xpipe/app/cred/SshIdentityKeyListStrategy.java delete mode 100644 app/src/main/java/io/xpipe/app/cred/YubikeyPivStrategy.java create mode 100644 app/src/main/java/io/xpipe/app/prefs/HttpProxyCategory.java create mode 100644 app/src/main/java/io/xpipe/app/pwman/OpenBaoPasswordManager.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/KrdcRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/storage/DataStorageVaultKey.java create mode 100644 app/src/main/java/io/xpipe/app/terminal/TerminalBanner.java create mode 100644 app/src/main/java/io/xpipe/app/util/AsciiArtConverter.java create mode 100644 app/src/main/java/io/xpipe/app/util/CacheableConfiguration.java create mode 100644 app/src/main/java/io/xpipe/app/util/Checkable.java rename app/src/main/java/io/xpipe/app/{terminal/ControllableTerminalSession.java => util/ControllableWindowProcess.java} (53%) rename app/src/main/java/io/xpipe/app/{terminal/WindowsTerminalSession.java => util/ControllableWindowsProcess.java} (69%) create mode 100644 app/src/main/java/io/xpipe/app/util/HashicorpVaultConfig.java create mode 100644 app/src/main/java/io/xpipe/app/util/HttpProxy.java rename app/src/main/java/io/xpipe/app/{platform => util}/NativeMacOsWindowControl.java (92%) rename app/src/main/java/io/xpipe/app/{platform => util}/NativeWinWindowControl.java (90%) create mode 100644 app/src/main/java/io/xpipe/app/util/OpenBaoConfig.java create mode 100644 app/src/main/java/io/xpipe/app/util/RemoteDesktopDockComp.java create mode 100644 app/src/main/java/io/xpipe/app/util/RemoteDesktopDockContentEntry.java create mode 100644 app/src/main/java/io/xpipe/app/util/RemoteDesktopDockEntry.java create mode 100644 app/src/main/java/io/xpipe/app/util/RemoteDesktopDockView.java create mode 100644 app/src/main/java/io/xpipe/app/util/RemoteDesktopWindow.java create mode 100644 app/src/main/java/io/xpipe/app/util/TlsCertificateFormat.java create mode 100644 app/src/main/java/io/xpipe/app/util/WindowDockComp.java create mode 100644 app/src/main/java/io/xpipe/app/util/WindowDockListener.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/KrdcVncClient.java create mode 100644 app/src/main/resources/io/xpipe/app/resources/style/remote-desktop-dock.css create mode 100644 dist/changelog/23.0.1.md create mode 100644 dist/changelog/23.0.md create mode 100644 dist/changelog/23.1.md create mode 100644 dist/changelog/23.2.md create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityConvert.java create mode 100644 img/proc/networkProxy_icon-dark.svg create mode 100644 img/proc/networkProxy_icon.svg diff --git a/README.md b/README.md index 48fa824b2..d37ceaa5a 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,8 @@ are not able to resolve and install any dependency packages. ### RHEL-based distros -The rpm releases are signed with the GPG key https://xpipe.io/signatures/crschnick.asc. -You can import it via `rpm --import https://xpipe.io/signatures/crschnick.asc` to allow your rpm-based package manager to verify the release signature. +The rpm releases are signed with the GPG key https://xpipe.io/signatures/0xDD3E0AD0.asc. +You can import it via `rpm --import https://xpipe.io/signatures/0xDD3E0AD0.asc` to allow your rpm-based package manager to verify the release signature. The following rpm installers are available: diff --git a/app/build.gradle b/app/build.gradle index ed5e56f5e..abb908676 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,11 +64,11 @@ dependencies { } api "com.github.weisj:jsvg:1.7.2" - api 'io.xpipe:vernacular:1.16' + api 'io.xpipe:vernacular:1.17' api 'org.bouncycastle:bcprov-jdk18on:1.83' api 'info.picocli:picocli:4.7.7' api 'org.apache.commons:commons-lang3:3.20.0' - api 'io.sentry:sentry:8.20.0' + api 'io.sentry:sentry:8.41.0' api 'commons-io:commons-io:2.21.0' api "com.fasterxml.jackson.core:jackson-databind:2.21.1" api "com.fasterxml.jackson.core:jackson-annotations:2.21" diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java index 4e846a8d8..b3519aeab 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java @@ -56,7 +56,7 @@ public class ActionConfigComp extends SimpleRegionBuilder { }); var choice = new StoreListChoiceComp<>( - listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory()); + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null); choice.hide(listProp.emptyProperty()); choice.maxHeight(450); return choice; @@ -79,7 +79,8 @@ public class ActionConfigComp extends SimpleRegionBuilder { singleProp, DataStore.class, ref -> true, - StoreViewState.get().getAllConnectionsCategory()); + StoreViewState.get().getAllConnectionsCategory(), + null); choice.hide(singleProp.isNull()); return choice; } @@ -98,7 +99,7 @@ public class ActionConfigComp extends SimpleRegionBuilder { } }); - var area = new IntegratedTextAreaComp(config, false, "action", new SimpleStringProperty("json")); + var area = new IntegratedTextAreaComp(config, false, "action", new SimpleStringProperty("json"), true); area.hide(config.isNull()); return area; } diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java index cc4f0309f..795efcfc7 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java +++ b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java @@ -54,7 +54,7 @@ public class ActionConfirmComp extends SimpleRegionBuilder { } var choice = new StoreListChoiceComp<>( - listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory()); + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory(), null, null); choice.maxHeight(450); choice.setEditable(false); choice.hide(listProp.emptyProperty()); diff --git a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java index fc4e9eb98..da8b99cb1 100644 --- a/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java +++ b/app/src/main/java/io/xpipe/app/action/QuickConnectProvider.java @@ -38,7 +38,7 @@ public interface QuickConnectProvider extends ActionProvider { boolean skipDialogIfPossible(); - default void open(DataStoreEntry e) throws Exception { + default void open(DataStoreEntry e) { OpenHubMenuLeafProvider.Action.builder().ref(e.ref()).build().executeSync(); } } diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index 7e99954d4..4d7073ab2 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -15,9 +15,12 @@ import com.sun.net.httpserver.HttpServer; import lombok.Getter; import java.io.IOException; +import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.Inet4Address; import java.net.InetSocketAddress; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermissions; import java.util.*; @@ -44,18 +47,16 @@ public class AppBeaconServer { @Getter private String localAuthSecret; + private FileChannel localLockFileChannel; + private FileLock localLockFileLock; private AppBeaconServer(int port) { this.port = port; } - public static void setupPort() { - int port = BeaconConfig.getUsedPort(); - INSTANCE = new AppBeaconServer(port); - } - public static void init() { try { + INSTANCE = new AppBeaconServer(BeaconConfig.getUsedPort()); INSTANCE.initAuthSecret(); INSTANCE.start(); TrackEvent.withInfo("Started http server") @@ -113,20 +114,26 @@ public class AppBeaconServer { var file = BeaconConfig.getLocalBeaconAuthFile(); // Create and set temp dir permissions for Linux AppLocalTemp.getLocalTempDataDirectory(); + var id = UUID.randomUUID().toString(); Files.writeString(file, id); if (OsType.ofLocal() != OsType.WINDOWS) { Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----")); } localAuthSecret = id; + + var lockFile = BeaconConfig.getLocalBeaconLockFile(); + localLockFileChannel = new RandomAccessFile(lockFile.toFile(), "rw").getChannel(); + localLockFileLock = localLockFileChannel.tryLock(); } private void deleteAuthSecret() { var file = BeaconConfig.getLocalBeaconAuthFile(); try { Files.delete(file); - } catch (IOException ignored) { - } + localLockFileLock.release(); + localLockFileChannel.close(); + } catch (IOException ignored) {} } private void start() throws IOException { @@ -139,8 +146,11 @@ public class AppBeaconServer { }); return t; }); - server = HttpServer.create( - new InetSocketAddress(Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}), port), 10); + var external = AppPrefs.get().allowExternalApiRequests().get() || Boolean.getBoolean("XPIPE_API_SERVER"); + var addr = external + ? Inet4Address.getByAddress(new byte[] {0, 0, 0, 0}) + : Inet4Address.getByAddress(new byte[] {0x7f, 0x00, 0x00, 0x01}); + server = HttpServer.create(new InetSocketAddress(addr, port), 10); BeaconInterface.getAll().forEach(beaconInterface -> { var handler = new BeaconRequestHandler<>(beaconInterface); server.createContext(beaconInterface.getPath(), exchange -> { @@ -173,7 +183,7 @@ public class AppBeaconServer { private boolean handleCorsHeaders(HttpExchange exchange) throws IOException { if (AppPrefs.get().enableHttpApi().get()) { exchange.getResponseHeaders() - .add("Origin", "http://localhost:" + AppBeaconServer.get().getPort()); + .add("Origin", "http://localhost:" + getPort()); exchange.getResponseHeaders().add("Vary", "Origin"); exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); exchange.getResponseHeaders().add("Access-Control-Allow-Credentials", "true"); diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java index 0c1900b7f..14c885644 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -142,10 +142,13 @@ public class BeaconRequestHandler implements HttpHandler { try { var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0; if (!emptyResponseClass && response != null) { - var redact = AppPrefs.get() == null || !AppPrefs.get().developerMode().getValue() || !AppPrefs.get().developerShowSensitiveCommands().get(); + var redact = AppPrefs.get() == null + || !AppPrefs.get().developerMode().getValue() + || !AppPrefs.get().developerShowSensitiveCommands().get(); var mapper = redact ? JacksonMapper.getRedactedSecretMapper() : JacksonMapper.getUnredactSecretMapper(); TrackEvent.trace("Sending response:\n" + response); - TrackEvent.trace("Sending raw response:\n" + mapper.valueToTree(response).toPrettyString()); + TrackEvent.trace( + "Sending raw response:\n" + mapper.valueToTree(response).toPrettyString()); var bytes = JacksonMapper.getDefault() .valueToTree(response) .toPrettyString() diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java index 25e100740..d2b55e143 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -1,6 +1,8 @@ package io.xpipe.app.beacon.impl; +import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.secret.SecretManager; @@ -32,6 +34,12 @@ public class AskpassExchangeImpl extends AskpassExchange { var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry -> msg.getPrompt() .equals(queueEntry.getName().getValue())); if (!shown) { + var dialogShown = AppCache.getBoolean("touchDialogShown", false); + if (!dialogShown) { + AppDialog.information("touchNotice"); + AppCache.update("touchDialogShown", true); + } + var qe = new AppLayoutModel.QueueEntry( new SimpleStringProperty(msg.getPrompt()), new LabelGraphic.IconGraphic("mdi2f-fingerprint"), @@ -41,7 +49,7 @@ public class AskpassExchangeImpl extends AskpassExchange { () -> { AppLayoutModel.get().getQueueEntries().remove(qe); }, - Duration.ofSeconds(10)); + Duration.ofSeconds(15)); } return Response.builder().value(InPlaceSecretValue.of("")).build(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java b/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java index b254dfc1f..5afd2a437 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java @@ -21,7 +21,6 @@ import lombok.Value; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; @Value public class AppMcpServer { @@ -30,8 +29,7 @@ public class AppMcpServer { McpSyncServer mcpSyncServer; HttpStreamableServerTransportProvider transportProvider; - List readOnlyTools; - List mutationTools; + List tools; public static AppMcpServer get() { return INSTANCE; @@ -47,9 +45,10 @@ public class AppMcpServer { null); var prompt = McpSchemaFiles.load("prompt.md"); - var effectivePrompt = AppPrefs.get().mcpAdditionalContext().getValue() != null ? - prompt.replace("__CUSTOM__", AppPrefs.get().mcpAdditionalContext().getValue()) : - prompt.replace("__CUSTOM__", ""); + var effectivePrompt = AppPrefs.get().mcpAdditionalContext().getValue() != null + ? prompt.replace( + "__CUSTOM__", AppPrefs.get().mcpAdditionalContext().getValue()) + : prompt.replace("__CUSTOM__", ""); McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider) .serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion()) @@ -61,44 +60,27 @@ public class AppMcpServer { .instructions(effectivePrompt) .build(); - var readOnlyTools = new ArrayList(); - readOnlyTools.add(McpTools.help()); - readOnlyTools.add(McpTools.listSystems()); - readOnlyTools.add(McpTools.readFile()); - readOnlyTools.add(McpTools.listFiles()); - readOnlyTools.add(McpTools.findFile()); - readOnlyTools.add(McpTools.getFileInfo()); + var tools = new ArrayList(); + tools.add(McpTools.help()); + tools.add(McpTools.listSystems()); + tools.add(McpTools.readFile()); + tools.add(McpTools.listFiles()); + tools.add(McpTools.findFile()); + tools.add(McpTools.getFileInfo()); + tools.add(McpTools.openTerminal()); + tools.add(McpTools.createFile()); + tools.add(McpTools.writeFile()); + tools.add(McpTools.createDirectory()); + tools.add(McpTools.runCommand()); + tools.add(McpTools.runScript()); + tools.add(McpTools.toggleState()); + tools.add(McpTools.callApi()); - var mutationTools = new ArrayList(); - mutationTools.add(McpTools.openTerminal()); - mutationTools.add(McpTools.createFile()); - mutationTools.add(McpTools.writeFile()); - mutationTools.add(McpTools.createDirectory()); - mutationTools.add(McpTools.runCommand()); - mutationTools.add(McpTools.runScript()); - mutationTools.add(McpTools.toggleState()); - mutationTools.add(McpTools.callApi()); - - for (McpServerFeatures.SyncToolSpecification readOnlyTool : readOnlyTools) { + for (McpServerFeatures.SyncToolSpecification readOnlyTool : tools) { syncServer.addTool(readOnlyTool); } - var toolsAdded = new AtomicBoolean(); - AppPrefs.get().enableMcpMutationTools().subscribe(value -> { - for (var mutationTool : mutationTools) { - if (value) { - syncServer.addTool(mutationTool); - } else if (toolsAdded.get()) { - syncServer.removeTool(mutationTool.tool().name()); - } - } - if (value) { - toolsAdded.set(true); - } - syncServer.notifyToolsListChanged(); - }); - - INSTANCE = new AppMcpServer(syncServer, transportProvider, readOnlyTools, mutationTools); + INSTANCE = new AppMcpServer(syncServer, transportProvider, tools); } public static void reset() { diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java index e47472507..e664d033c 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java @@ -5,7 +5,6 @@ import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.process.ShellControl; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageQuery; -import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.beacon.BeaconClientException; import io.xpipe.core.FilePath; @@ -34,13 +33,19 @@ public interface McpToolHandler } catch (BeaconClientException e) { ErrorEventFactory.fromThrowable(e).expected().omit().handle(); return McpSchema.CallToolResult.builder() - .addTextContent(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()) + .addTextContent( + e.getMessage() != null + ? e.getMessage() + : e.getClass().getSimpleName()) .isError(true) .build(); } catch (Throwable e) { ErrorEventFactory.fromThrowable(e).omit().handle(); return McpSchema.CallToolResult.builder() - .addTextContent(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()) + .addTextContent( + e.getMessage() != null + ? e.getMessage() + : e.getClass().getSimpleName()) .isError(true) .build(); } @@ -131,7 +136,10 @@ public interface McpToolHandler if (found.size() > 1) { throw new BeaconClientException("Multiple connections found: " - + found.stream().map(entry -> DataStorage.get().getStorePath(entry).toString()).toList()); + + found.stream() + .map(entry -> + DataStorage.get().getStorePath(entry).toString()) + .toList()); } var e = found.getFirst(); diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java index 9b11a6067..b2a623c1e 100644 --- a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java @@ -8,8 +8,6 @@ import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.process.ScriptHelper; import io.xpipe.app.process.ShellControl; -import io.xpipe.app.process.TerminalInitScriptConfig; -import io.xpipe.app.process.WorkingDirectoryFunction; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageQuery; import io.xpipe.app.terminal.TerminalLaunch; @@ -46,36 +44,19 @@ public final class McpTools { return McpServerFeatures.SyncToolSpecification.builder() .tool(tool) .callHandler(McpToolHandler.of((req) -> { - var ro = AppMcpServer.get().getReadOnlyTools().stream() + var tools = AppMcpServer.get().getTools().stream() .filter(syncToolSpecification -> !syncToolSpecification.tool().name().equals("help")) .toList(); - var mu = AppMcpServer.get().getMutationTools(); - - var roList = ro.stream() + var toolsList = tools.stream() .map(syncToolSpecification -> "- " + syncToolSpecification.tool().name() + ": " + syncToolSpecification.tool().description()) .collect(Collectors.joining("\n")); - var muList = mu.stream() - .map(syncToolSpecification -> - "- " + syncToolSpecification.tool().name() + ": " - + syncToolSpecification.tool().description()) - .collect(Collectors.joining("\n")); - - var muEnabled = AppPrefs.get().enableMcpMutationTools().get(); - var muStatus = muEnabled ? "Right now, the mutation tools are enabled." : "Right now, the mutation tools are disabled. When you enable them in the settings menu, the MCP client might need a reconnect to see the changes."; - var text = """ - The XPipe MCP server offers the following read-only tools: + The XPipe MCP server offers the following tools: %s - These tools will not modify anything on your system and are safe to use. - - You can also enable the following potentially destructive tools in the settings menu: - %s - These tools can perform write operations and other actions that might be potentially destructive. - %s - """.formatted(roList, muList, muStatus); + """.formatted(toolsList); return McpSchema.CallToolResult.builder() .addTextContent(text) @@ -102,9 +83,14 @@ public final class McpTools { throw new BeaconClientException("No API endpoint found for path " + path); } - var httpReq = HttpRequest.newBuilder().uri(URI.create("http://localhost:" + AppBeaconServer.get().getPort() + path)) - .header("Authorization", "Bearer " + AppPrefs.get().apiKey().get()) - .POST(HttpRequest.BodyPublishers.ofString(payloadJson.toPrettyString())).build(); + var httpReq = HttpRequest.newBuilder() + .uri(URI.create( + "http://localhost:" + AppBeaconServer.get().getPort() + path)) + .header( + "Authorization", + "Bearer " + AppPrefs.get().apiKey().get()) + .POST(HttpRequest.BodyPublishers.ofString(payloadJson.toPrettyString())) + .build(); var httpRes = HttpHelper.client().send(httpReq, HttpResponse.BodyHandlers.ofString()); var resJson = JacksonMapper.getDefault().readTree(httpRes.body()); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java index d4229bbce..1589458a0 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -170,7 +170,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { var splitPane = new LeftSplitPaneComp(vertical, stack) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .applyStructure(struc -> { - struc.getLeft().setMinWidth(250); + struc.getLeft().setMinWidth(270); struc.getLeft().setMaxWidth(500); }); splitPane.disable(model.getBusy()); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java index 1438f1d5d..da9ccfd25 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java @@ -90,7 +90,7 @@ public class BrowserFullSessionComp extends SimpleRegionBuilder { leftSplit.set(d); }); splitPane.applyStructure(struc -> { - struc.getLeft().setMinWidth(250); + struc.getLeft().setMinWidth(270); struc.getLeft().setMaxWidth(500); struc.get().setPickOnBounds(false); }); diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java index 4d16a97ed..6e3f0fd28 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java @@ -6,7 +6,6 @@ import io.xpipe.app.action.StoreContextAction; import io.xpipe.app.browser.file.BrowserFileTransferOperation; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.window.AppDialog; -import io.xpipe.app.ext.ConnectionFileSystem; import io.xpipe.app.ext.FileSystemStore; import io.xpipe.app.process.ParentSystemAccess; import io.xpipe.app.process.ShellControl; @@ -53,12 +52,16 @@ public class TransferFilesActionProvider implements ActionProvider { } var sourceFs = operation.getFiles().getFirst().getFileSystem(); - var sourceAccess = sourceFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null); - var sourceSlowRemote = sourceAccess != null && ParentSystemAccess.isEquivalent(sourceAccess, ParentSystemAccess.none()); + var sourceAccess = + sourceFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null); + var sourceSlowRemote = + sourceAccess != null && ParentSystemAccess.isEquivalent(sourceAccess, ParentSystemAccess.none()); var targetFs = operation.getTarget().getFileSystem(); - var targetAccess = targetFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null); - var targetSlowRemote = targetAccess != null && ParentSystemAccess.isEquivalent(targetAccess, ParentSystemAccess.none()); + var targetAccess = + targetFs.getShell().map(ShellControl::getLocalSystemAccess).orElse(null); + var targetSlowRemote = + targetAccess != null && ParentSystemAccess.isEquivalent(targetAccess, ParentSystemAccess.none()); if (!sourceSlowRemote && !targetSlowRemote) { return true; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java index 313061acf..f65123ca3 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java @@ -11,7 +11,6 @@ import javafx.scene.input.ClipboardContent; import javafx.scene.input.DataFormat; import javafx.scene.input.Dragboard; -import lombok.SneakyThrows; import lombok.Value; import java.awt.datatransfer.Clipboard; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java index 7294b8c27..ac192d613 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListComp.java @@ -9,6 +9,7 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.SetChangeListener; import javafx.css.PseudoClass; import javafx.scene.control.Button; import javafx.scene.layout.Region; @@ -21,6 +22,7 @@ import java.util.function.Predicate; public final class BrowserConnectionListComp extends SimpleRegionBuilder { private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + private static final PseudoClass BUSY = PseudoClass.getPseudoClass("busy"); private final ObservableValue selected; private final Predicate applicable; private final BiConsumer action; @@ -58,6 +60,11 @@ public final class BrowserConnectionListComp extends SimpleRegionBuilder { && newValue.equals(s.getWrapper().getEntry())); }); }); + busyEntries.addListener((SetChangeListener) change -> { + PlatformThread.runLaterIfNeeded(() -> { + struc.pseudoClassStateChanged(BUSY, change.getSet().contains(s)); + }); + }); }); }; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java index d1424689d..884a2bb37 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserConnectionListFilterComp.java @@ -35,14 +35,16 @@ public final class BrowserConnectionListFilterComp extends SimpleRegionBuilder { this.category, true, ignored -> true) + .hgrow() + .maxWidth(10000) .style(Styles.LEFT_PILL) .apply(struc -> { AppFontSizes.base(struc); }); var filter = FilterComp.ofStoreFilter(this.filter) .style(Styles.RIGHT_PILL) - .minWidth(0) - .hgrow() + .minWidth(100) + .prefWidth(120) .apply(struc -> { AppFontSizes.base(struc); filterTrigger.subscribe(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index 034510cab..c319ce0b1 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -53,18 +53,6 @@ public final class BrowserFileListComp extends SimpleRegionBuilder { this.fileList = fileList; } - private static void prepareTableScrollFix(TableView table) { - table.lookupAll(".scroll-bar").stream() - .filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal"))) - .findFirst() - .ifPresent(node -> { - Region region = (Region) node; - region.setMinHeight(0); - region.setPrefHeight(0); - region.setMaxHeight(0); - }); - } - @Override protected Region createSimple() { return createTable(); @@ -162,11 +150,16 @@ public final class BrowserFileListComp extends SimpleRegionBuilder { return null; } + if (fileList.getFileSystemModel().getFilter().getValue() != null) { + return AppI18n.get("emptyFilteredDirectory"); + } + return AppI18n.get("emptyDirectory"); }, AppI18n.activeLanguage(), fileList.getFileSystemModel().getBusy(), - fileList.getFileSystemModel().getCurrentPath()); + fileList.getFileSystemModel().getCurrentPath(), + fileList.getFileSystemModel().getFilter()); placeholder.textProperty().bind(PlatformThread.sync(placeholderText)); table.setPlaceholder(placeholder); AppFontSizes.base(placeholder); @@ -182,7 +175,6 @@ public final class BrowserFileListComp extends SimpleRegionBuilder { table.setFixedCellSize(30.0); prepareColumnVisibility(table, filenameCol, mtimeCol, modeCol, ownerCol, sizeCol); - prepareTableScrollFix(table); prepareTableSelectionModel(table); prepareTableShortcuts(table); prepareTableEntries(table); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java index e08b0f9ce..fcc896f52 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java @@ -299,7 +299,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab createEmptyDisplay() { - var docs = new IntroComp("browserWelcomeDocs", new LabelGraphic.IconGraphic("mdi2b-book-open-variant")); - docs.setButtonAction(() -> { - DocumentationLink.INTRO.open(); - }); - docs.setButtonGraphic(new LabelGraphic.IconGraphic("mdi2w-web")); - docs.setButtonDefault(true); - var open = new IntroComp( "browserWelcomeEmpty", new LabelGraphic.CompGraphic(PrettyImageHelper.ofSpecificFixedSize("welcome/hips.svg", 100, 122))); @@ -133,7 +125,7 @@ public class BrowserHistoryTabComp extends SimpleRegionBuilder { DataStorage.get().local().ref(), null, null, null); }); - var list = new IntroListComp(List.of(docs, open)); + var list = new IntroListComp(List.of(open)); return list; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java index 9b6a440b5..3998f587f 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java @@ -103,9 +103,7 @@ public class BrowserNavBarComp extends RegionStructureBuilder { - Platform.runLater(() -> { - setAlignment(stack, breadcrumbsRegion); - }); + setAlignment(stack, breadcrumbsRegion); }); model.getCurrentPath().addListener((observable, oldValue, newValue) -> { PlatformThread.runLaterIfNeeded(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java index d7db04dd3..9259121db 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java @@ -10,10 +10,7 @@ import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStoreColor; -import io.xpipe.app.terminal.TerminalDockBrowserComp; -import io.xpipe.app.terminal.TerminalDockView; -import io.xpipe.app.terminal.TerminalView; -import io.xpipe.app.terminal.WindowsTerminalType; +import io.xpipe.app.terminal.*; import io.xpipe.app.util.GlobalTimer; import io.xpipe.app.util.ThreadHelper; @@ -76,11 +73,10 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { ThreadHelper.sleep(250); } - var controllable = session.getTerminal().controllable(); - if (controllable.isEmpty()) { + if (!(session.getTerminal() instanceof TerminalView.ControllableTerminalSession t)) { return; } - dockModel.trackTerminal(controllable.get(), true); + dockModel.trackTerminal(t, true); } @Override @@ -96,15 +92,16 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { .filter(shellSession -> shellSession.getTerminal().equals(session.getTerminal())) .count(); if (others == 0) { - session.getTerminal().controllable().ifPresent(controllableTerminalSession -> { - controllableTerminalSession.close(); - }); + if (session.getTerminal() instanceof TerminalView.ControllableTerminalSession t) { + t.getControllable().close(); + } } } } @Override public void onTerminalClosed(TerminalView.TerminalSession instance) { + dockModel.removeTerminal(instance); refreshShowingState(); } }; @@ -153,7 +150,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { GlobalTimer.scheduleUntil(Duration.ofMillis(300), false, () -> { if (viewActive.get()) { - dockModel.clearDeadTerminals(); dockModel.updateCustomBounds(); } return closed; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java index e586d0c31..9939fe723 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java @@ -2,9 +2,11 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.BrowserFullSessionModel; import io.xpipe.app.browser.action.impl.TransferFilesActionProvider; +import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppLocalTemp; import io.xpipe.app.core.AppSystemInfo; import io.xpipe.app.core.mode.AppOperationMode; +import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.process.OsFileSystem; @@ -62,6 +64,16 @@ public class BrowserTransferModel { } }); thread.start(); + + transferring.addListener((observable, oldValue, newValue) -> { + if (!newValue) { + var shown = AppCache.getBoolean("downloadDialogShown", false); + if (!shown) { + AppDialog.information("downloadDialog"); + AppCache.update("downloadDialogShown", true); + } + } + }); } public List getCurrentItems() { diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java index 7d1bc7d8b..2f47a126a 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java @@ -10,6 +10,7 @@ import io.xpipe.app.ext.FileKind; import io.xpipe.app.platform.LabelGraphic; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -46,7 +47,12 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider { @Override public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN); + return switch (OsType.ofLocal()) { + case OsType.Linux linux -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN); + case OsType.MacOs macOs -> + new KeyCodeCombination(KeyCode.DOWN, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN); + case OsType.Windows windows -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHORTCUT_DOWN); + }; } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java index 5dcca14a2..588d3dd62 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java @@ -8,6 +8,7 @@ import io.xpipe.app.browser.menu.BrowserMenuCategory; import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -35,7 +36,11 @@ public class OpenFileDefaultMenuProvider implements BrowserMenuLeafProvider { @Override public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.ENTER); + return switch (OsType.ofLocal()) { + case OsType.Linux ignored -> new KeyCodeCombination(KeyCode.ENTER); + case OsType.MacOs ignored -> new KeyCodeCombination(KeyCode.DOWN, KeyCombination.SHORTCUT_DOWN); + case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.ENTER); + }; } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java index df84b83c5..a6710e8ea 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java @@ -12,9 +12,6 @@ import io.xpipe.app.platform.LabelGraphic; import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; import java.util.List; @@ -42,11 +39,6 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider { return BrowserMenuCategory.OPEN; } - @Override - public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN); - } - @Override public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("openFileWith"); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java index b4bb219e0..3d11c58cd 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java @@ -8,6 +8,7 @@ import io.xpipe.app.browser.menu.BrowserMenuCategory; import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -30,7 +31,11 @@ public class OpenNativeFileDetailsMenuProvider implements BrowserMenuLeafProvide @Override public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN); + return switch (OsType.ofLocal()) { + case OsType.Linux ignored -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN); + case OsType.MacOs ignored -> new KeyCodeCombination(KeyCode.I, KeyCombination.SHORTCUT_DOWN); + case OsType.Windows ignored -> new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN); + }; } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java index afb458fda..36d6b3227 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java @@ -7,6 +7,7 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.FileKind; import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.core.OsType; import javafx.beans.value.ObservableValue; import javafx.scene.input.KeyCode; @@ -44,7 +45,11 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider { @Override public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN); + return switch (OsType.ofLocal()) { + case OsType.Linux linux -> new KeyCodeCombination(KeyCode.F2); + case OsType.MacOs macOs -> new KeyCodeCombination(KeyCode.ENTER); + case OsType.Windows windows -> new KeyCodeCombination(KeyCode.F2); + }; } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java index 1fd5cc31d..c0400806e 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java @@ -30,6 +30,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { sc.view().isInPath("tar", true); sc.view().isInPath("zip", true); + sc.view().isInPath("gzip", true); } @Override @@ -53,7 +54,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { return false; } - var ext = List.of("zip", "tar", "tar.gz", "tgz", "rar", "xar"); + var ext = List.of("zip", "tar", "tar.gz", "tgz", "rar", "xar", "gz", "gzip"); if (entries.stream().anyMatch(browserEntry -> ext.stream().anyMatch(s -> browserEntry .getRawFileEntry() .getPath() @@ -89,7 +90,8 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { protected String getExtension() { return "tar"; } - }); + }, + new GzipActionProvider(false)); } private abstract static class LeafProvider implements BrowserMenuLeafProvider { @@ -107,7 +109,7 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { - var name = new SimpleStringProperty(directory ? entries.getFirst().getFileName() : null); + var name = new SimpleStringProperty(entries.size() == 1 ? entries.getFirst().getFileName() + "." + getExtension() : null); var modal = ModalOverlay.of( "archiveName", RegionBuilder.of(() -> { @@ -178,7 +180,28 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider { protected String getExtension() { return "tar"; } - }); + }, + new GzipActionProvider(directory)); + } + } + + private class GzipActionProvider extends LeafProvider { + + private GzipActionProvider(boolean directory) { + super(directory); + } + + @Override + protected void create(String fileName, BrowserFileSystemTabModel model, List entries) { + var builder = io.xpipe.app.browser.menu.impl.compress.GzipActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.target(model.getTargetDirectoryPath(entries.getFirst()).join(fileName)); + builder.build().executeAsync(); + } + + @Override + protected String getExtension() { + return "gz"; } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java new file mode 100644 index 000000000..8e4f54bfb --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipActionProvider.java @@ -0,0 +1,47 @@ +package io.xpipe.app.browser.menu.impl.compress; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.process.CommandBuilder; +import io.xpipe.app.process.ShellControl; +import io.xpipe.app.process.ShellDialects; +import io.xpipe.core.FilePath; +import io.xpipe.core.OsType; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class GunzipActionProvider implements BrowserActionProvider { + + public static FilePath getTarget(FilePath name) { + return FilePath.of(name.toString().replaceAll("\\.gz$", "").replaceAll("\\.gzip$", "")); + } + + @Override + public String getId() { + return "gunzip"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() throws Exception { + var sc = model.getFileSystem().getShell().orElseThrow(); + var b = CommandBuilder.of().add("gunzip", "--keep", "--force"); + for (BrowserEntry entry : getEntries()) { + b.addFile(entry.getRawFileEntry().getPath()); + } + sc.command(b).execute(); + model.refreshSync(); + } + + @Override + public boolean isMutation() { + return true; + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java new file mode 100644 index 000000000..7bb6ada13 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GunzipUnixMenuProvider.java @@ -0,0 +1,77 @@ +package io.xpipe.app.browser.menu.impl.compress; + +import io.xpipe.app.action.AbstractAction; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.icon.BrowserIconFileType; +import io.xpipe.app.browser.icon.BrowserIcons; +import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.platform.LabelGraphic; +import io.xpipe.app.process.OsFileSystem; +import io.xpipe.core.OsType; + +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class GunzipUnixMenuProvider implements BrowserMenuLeafProvider, BrowserApplicationPathMenuProvider { + + @Override + public LabelGraphic getIcon() { + return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + var dir = entries.size() > 1 + ? "[...]" + : GunzipActionProvider.getTarget( + entries.getFirst().getRawFileEntry().getPath()) + .getFileName(); + return AppI18n.observable("gunzipDirectory", dir); + } + + @Override + public String getExecutable() { + return "gunzip"; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + if (!BrowserApplicationPathMenuProvider.super.isApplicable(model, entries) + || !BrowserMenuLeafProvider.super.isApplicable(model, entries)) { + return false; + } + + return entries.stream() + .allMatch(entry -> { + var s = entry.getRawFileEntry().getPath().toString(); + if (s.endsWith(".tar.gz") || s.endsWith(".tgz") || s.equals("tar.gzip")) { + return false; + } + + return s.endsWith(".gz") || s.endsWith(".gzip"); + }) + && model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + } + + @Override + public AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var builder = GunzipActionProvider.Action.builder(); + builder.initEntries(model, entries); + return builder.build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java new file mode 100644 index 000000000..d692909cc --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/GzipActionProvider.java @@ -0,0 +1,44 @@ +package io.xpipe.app.browser.menu.impl.compress; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.process.CommandBuilder; +import io.xpipe.core.FilePath; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class GzipActionProvider implements BrowserActionProvider { + + @Override + public String getId() { + return "gzip"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + private final FilePath target; + + @Override + public void executeImpl() throws Exception { + var sc = model.getFileSystem().getShell().orElseThrow(); + var b = CommandBuilder.of().add("gzip", "--keep", "--force", "--stdout"); + for (BrowserEntry entry : getEntries()) { + b.addFile(entry.getRawFileEntry().getPath()); + } + b.add(">").addFile(target); + sc.command(b).execute(); + model.refreshSync(); + } + + @Override + public boolean isMutation() { + return true; + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java index 4664c0f82..aff0d2601 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java @@ -40,7 +40,8 @@ public class UntarActionProvider implements BrowserActionProvider { .withWorkingDirectory( toDirectory ? target - : model.getTargetDirectoryPath(getEntries().getFirst())) + : model.getTargetDirectoryPath( + getEntries().getFirst())) .execute(); } model.refreshSync(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java index 78ce258eb..ae99147f7 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java @@ -50,7 +50,8 @@ public class UnzipActionProvider implements BrowserActionProvider { .addFile(getTarget(entry.getRawFileEntry().getPath())); } try (var cc = sc.command(command) - .withWorkingDirectory(model.getTargetDirectoryPath(getEntries().getFirst())) + .withWorkingDirectory( + model.getTargetDirectoryPath(getEntries().getFirst())) .start()) { cc.discardOrThrow(); } @@ -73,7 +74,8 @@ public class UnzipActionProvider implements BrowserActionProvider { } command.add("-Path").addFile(entry.getRawFileEntry().getPath()); sc.command(command) - .withWorkingDirectory(model.getTargetDirectoryPath(getEntries().getFirst())) + .withWorkingDirectory( + model.getTargetDirectoryPath(getEntries().getFirst())) .execute(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/augment/ContextMenuAugment.java b/app/src/main/java/io/xpipe/app/comp/augment/ContextMenuAugment.java index dec567a0f..f10c8d617 100644 --- a/app/src/main/java/io/xpipe/app/comp/augment/ContextMenuAugment.java +++ b/app/src/main/java/io/xpipe/app/comp/augment/ContextMenuAugment.java @@ -43,8 +43,9 @@ public class ContextMenuAugment implements Consumer { var r = struc; r.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { + var hidden = hide.get(); if (mouseEventCheck != null && mouseEventCheck.test(event)) { - if (!hide.get()) { + if (!hidden) { var cm = contextMenu.get(); if (cm != null) { cm.show(r, event.getScreenX(), event.getScreenY()); @@ -67,15 +68,18 @@ public class ContextMenuAugment implements Consumer { } }); r.addEventHandler(KeyEvent.KEY_PRESSED, event -> { - if (keyEventCheck != null && keyEventCheck.test(event)) { - if (!hide.get()) { - var cm = contextMenu.get(); - if (cm != null) { - cm.show(r, Side.BOTTOM, 0, 0); - currentContextMenu.set(cm); + if (keyEventCheck != null) { + var hidden = hide.get(); + if (keyEventCheck.test(event)) { + if (!hidden) { + var cm = contextMenu.get(); + if (cm != null) { + cm.show(r, Side.BOTTOM, 0, 0); + currentContextMenu.set(cm); + } } + event.consume(); } - event.consume(); } }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java index 3768747c9..8d016d85d 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppLayoutComp.java @@ -68,7 +68,7 @@ public class AppLayoutComp extends RegionStructureBuilder extends RegionBuilder> { @Override public ComboBox createSimple() { var cb = MenuHelper.createComboBox(); - cb.setConverter(new StringConverter<>() { + + Supplier> converter = () -> new StringConverter<>() { @Override public String toString(T object) { if (object == null) { @@ -66,7 +70,21 @@ public class ChoiceComp extends RegionBuilder> { public T fromString(String string) { throw new UnsupportedOperationException(); } + }; + cb.setConverter(converter.get()); + + // Reset converter on language change to force an update + // This does not work properly in older JFX versions, see JDK-8384006 + var ref = new WeakReference<>(cb); + AppI18n.activeLanguage().subscribe((v) -> { + var refValue = ref.get(); + if (refValue != null) { + Platform.runLater(() -> { + refValue.setConverter(converter.get()); + }); + } }); + range.subscribe(c -> { PlatformThread.runLaterIfNeeded(() -> { var list = FXCollections.observableArrayList(c.keySet()); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ChoicePaneComp.java b/app/src/main/java/io/xpipe/app/comp/base/ChoicePaneComp.java index 8917124d9..85d17a4a3 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ChoicePaneComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ChoicePaneComp.java @@ -2,9 +2,11 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.comp.RegionBuilder; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.platform.MenuHelper; import io.xpipe.app.platform.PlatformThread; +import javafx.application.Platform; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -16,8 +18,11 @@ import javafx.util.StringConverter; import lombok.Setter; +import java.lang.ref.WeakReference; +import java.util.HashMap; import java.util.List; import java.util.function.Function; +import java.util.function.Supplier; public class ChoicePaneComp extends RegionBuilder { @@ -44,7 +49,8 @@ public class ChoicePaneComp extends RegionBuilder { } }); cb.getSelectionModel().select(selected.getValue()); - cb.setConverter(new StringConverter<>() { + + Supplier> converter = () -> new StringConverter<>() { @Override public String toString(Entry object) { if (object == null || object.name() == null) { @@ -58,6 +64,19 @@ public class ChoicePaneComp extends RegionBuilder { public Entry fromString(String string) { throw new UnsupportedOperationException(); } + }; + cb.setConverter(converter.get()); + + // Reset converter on language change to force an update + // This does not work properly in older JFX versions, see JDK-8384006 + var ref = new WeakReference<>(cb); + AppI18n.activeLanguage().subscribe((v) -> { + var refValue = ref.get(); + if (refValue != null) { + Platform.runLater(() -> { + refValue.setConverter(converter.get()); + }); + } }); var vbox = new VBox(transformer.apply(cb)); @@ -69,13 +88,15 @@ public class ChoicePaneComp extends RegionBuilder { }); cb.prefWidthProperty().bind(vbox.widthProperty()); + + var regionMap = new HashMap(); cb.valueProperty().subscribe(n -> { if (n == null) { if (vbox.getChildren().size() > 1) { vbox.getChildren().remove(1); } } else { - var region = n.comp().build(); + var region = regionMap.computeIfAbsent(n, entry -> entry.comp().build()); if (vbox.getChildren().size() == 1) { vbox.getChildren().add(region); } else { diff --git a/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java index 09603a8e4..ed10f8dcc 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java @@ -5,6 +5,7 @@ import io.xpipe.app.comp.BaseRegionBuilder; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.window.AppDialog; +import io.xpipe.app.cred.SshIdentityStrategy; import io.xpipe.app.ext.FileSystemStore; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEventFactory; @@ -126,9 +127,10 @@ public class ContextualFileReferenceChoiceComp extends RegionBuilder { source.toString().substring(0, source.toString().length() - 4)) : source; - var pubSource = Path.of(sourceBase + ".pub"); + var pubSource = SshIdentityStrategy.getPublicKeyPath(FilePath.of(source)) + .asLocalPath(); if (Files.exists(pubSource)) { - var pubTarget = Path.of(target.toString() + ".pub"); + var pubTarget = sync.getTargetLocation().apply(pubSource); handler.addDataFile(pubSource, pubTarget, sync.getPerUser().get()); } @@ -216,20 +218,30 @@ public class ContextualFileReferenceChoiceComp extends RegionBuilder { filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> { prop.set(s != null ? s.toString() : null); })); - prop.addListener((observable, oldValue, newValue) -> { - filePath.setValue(newValue != null && !newValue.isBlank() ? FilePath.of(newValue.strip()) : null); - }); var fileNameComp = new TextFieldComp(prop).apply(struc -> HBox.setHgrow(struc, Priority.ALWAYS)); - if (prompt != null) { - fileNameComp.apply(struc -> { + fileNameComp.apply(struc -> { + if (prompt != null) { prompt.subscribe(filePath -> { PlatformThread.runLaterIfNeeded(() -> { struc.setPromptText(filePath != null ? filePath.toString() : null); }); }); + } + + prop.addListener((observable, oldValue, newValue) -> { + if (!struc.isFocused()) { + filePath.setValue(newValue != null && !newValue.isBlank() ? FilePath.of(newValue.strip()) : null); + } }); - } + + struc.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue) { + var v = prop.getValue(); + filePath.setValue(v != null && !v.isBlank() ? FilePath.of(v.strip()) : null); + } + }); + }); return fileNameComp; } diff --git a/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java b/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java index 86ab25c50..cf11a447e 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/FilterComp.java @@ -7,6 +7,7 @@ import io.xpipe.app.core.AppOpenArguments; import io.xpipe.app.hub.comp.StoreFilter; import io.xpipe.app.platform.PlatformThread; +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; @@ -17,8 +18,6 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import atlantafx.base.controls.CustomTextField; -import javafx.scene.layout.Pane; -import javafx.scene.shape.Rectangle; import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; @@ -68,6 +67,14 @@ public class FilterComp extends RegionBuilder { filter.focusedProperty())); RegionDescriptor.builder().nameKey("search").build().apply(filter); + filter.focusedProperty().subscribe(f -> { + if (f) { + Platform.runLater(() -> { + filter.selectAll(); + }); + } + }); + filter.addEventFilter(KeyEvent.KEY_PRESSED, event -> { if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) { filter.clear(); @@ -95,15 +102,6 @@ public class FilterComp extends RegionBuilder { filterText.setValue(n != null && n.length() > 0 ? n : null); }); - // Fix caret not being visible on right side when overflowing - filter.setSkin(filter.createDefaultSkin()); - Pane pane = (Pane) filter.getChildrenUnmodifiable().getFirst(); - var rec = new Rectangle(); - rec.widthProperty().bind(pane.widthProperty().add(2)); - rec.heightProperty().bind(pane.heightProperty()); - rec.setSmooth(false); - filter.getChildrenUnmodifiable().getFirst().setClip(rec); - return filter; } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java b/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java index 7f3423ebd..3523c0d8c 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java @@ -14,7 +14,10 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.ScrollPane; import javafx.scene.control.TextArea; +import javafx.scene.control.skin.TextAreaSkin; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Region; @@ -28,17 +31,23 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder fileType; + private final boolean fitHeight; public IntegratedTextAreaComp( - Property value, boolean lazy, String identifier, ObservableValue fileType) { + Property value, + boolean lazy, + String identifier, + ObservableValue fileType, + boolean fitHeight) { this.value = value; this.lazy = lazy; this.identifier = identifier; this.fileType = fileType; + this.fitHeight = fitHeight; } public static IntegratedTextAreaComp script( - ObservableValue> host, Property value) { + ObservableValue> host, Property value, boolean fitHeight) { var type = Bindings.createStringBinding( () -> { return host.getValue() != null @@ -49,10 +58,11 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder value, ObservableValue fileType) { + public static IntegratedTextAreaComp script( + Property value, ObservableValue fileType, boolean fitHeight) { var string = new SimpleStringProperty(); value.subscribe(shellScript -> { string.set(shellScript != null ? shellScript.getValue() : null); @@ -60,7 +70,7 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder { value.setValue(newValue != null ? new ShellScript(newValue) : null); }); - var i = new IntegratedTextAreaComp(string, false, "script", fileType); + var i = new IntegratedTextAreaComp(string, false, "script", fileType, fitHeight); return i; } @@ -110,12 +120,26 @@ public class IntegratedTextAreaComp extends RegionStructureBuilder { + var bar = (ScrollBar) tas.lookup(".scroll-bar:vertical"); + var visible = bar != null && bar.isVisible(); + AnchorPane.setTopAnchor(copyButton, visible ? 14 : 4.0); + AnchorPane.setRightAnchor(copyButton, visible ? 14 : 4.0); + }); + + return new Structure(pane, ta); } @Value 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 82e17a545..24fb166ff 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 @@ -6,6 +6,7 @@ import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.platform.DerivedObservableList; +import io.xpipe.app.util.GlobalTimer; import javafx.animation.AnimationTimer; import javafx.application.Platform; @@ -22,6 +23,7 @@ import javafx.scene.layout.VBox; import lombok.Setter; +import java.time.Duration; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -160,6 +162,13 @@ public class ListBoxViewComp extends RegionBuilder { Platform.runLater(() -> { dirty.set(true); }); + GlobalTimer.delay( + () -> { + Platform.runLater(() -> { + dirty.set(true); + }); + }, + Duration.ofMillis(50)); }); shown.addListener((ListChangeListener) (change) -> { Platform.runLater(() -> { @@ -285,8 +294,8 @@ public class ListBoxViewComp extends RegionBuilder { if (pane.getScene().getHeight() > 200) { var sceneNodeBounds = node.localToScene(node.getBoundsInLocal()); // Add some margin to preload - if (sceneNodeBounds.getMaxY() < -100 - || sceneNodeBounds.getMinY() > pane.getScene().getHeight() + 100) { + if (sceneNodeBounds.getMaxY() < -250 + || sceneNodeBounds.getMinY() > pane.getScene().getHeight() + 250) { return false; } } 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 b683a6b15..602f628c7 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 @@ -88,6 +88,8 @@ public class MarkdownComp extends RegionBuilder { @SneakyThrows private WebView createWebView() { var wv = new WebView(); + wv.setMinWidth(100); + wv.setMinHeight(100); wv.getEngine().setJavaScriptEnabled(false); wv.setContextMenuEnabled(false); wv.setPageFill(Color.TRANSPARENT); @@ -99,10 +101,9 @@ public class MarkdownComp extends RegionBuilder { AppPrefs.get().theme().subscribe((v) -> { var refVal = ref.get(); if (refVal != null && v != null) { - var theme = v.isDark() - ? "misc/github-markdown-dark.css" - : "misc/github-markdown-light.css"; - var url = AppResources.getResourceURL(AppResources.MAIN_MODULE, theme).orElseThrow(); + var theme = v.isDark() ? "misc/github-markdown-dark.css" : "misc/github-markdown-light.css"; + var url = AppResources.getResourceURL(AppResources.MAIN_MODULE, theme) + .orElseThrow(); refVal.getEngine().setUserStyleSheetLocation(url.toString()); } }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java index 3cd48b898..fd4405299 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java @@ -10,6 +10,7 @@ import io.xpipe.app.platform.PlatformThread; import io.xpipe.app.util.BooleanScope; import io.xpipe.core.OsType; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; @@ -65,11 +66,16 @@ public class ModalOverlayComp extends RegionBuilder { if (lastShowValue != null && java.time.Duration.between(lastShowValue, Instant.now()) .toMillis() - > 500) { + > 1000) { mouseHandler.handle(event); } }); } + + @Override + protected Timeline createCloseBlockedAnimation() { + return new Timeline(); + } }); modal.setInTransitionFactory( OsType.ofLocal() == OsType.LINUX ? null : node -> Animations.fadeIn(node, Duration.millis(150))); @@ -216,6 +222,7 @@ public class ModalOverlayComp extends RegionBuilder { newValue.getGraphic() != null ? newValue.getGraphic() : new LabelGraphic.IconGraphic("mdi2i-information-outline"))); + l.style("title"); l.apply(struc -> { struc.setGraphicTextGap(8); AppFontSizes.xl(struc); @@ -235,7 +242,7 @@ public class ModalOverlayComp extends RegionBuilder { var node = o instanceof ModalButton mb ? toButton(mb) : ((BaseRegionBuilder) o).build(); if (o instanceof ModalButton) { node.widthProperty().addListener((observable, oldValue, n) -> { - var d = Math.min(Math.max(n.doubleValue(), 70.0), 200.0); + var d = Math.clamp(n.doubleValue(), 70.0, 200.0); if (d > max.get()) { max.set(d); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java index aaea0a5b7..41ce60ef9 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayStackComp.java @@ -23,7 +23,7 @@ public class ModalOverlayStackComp extends SimpleRegionBuilder { @Override protected Region createSimple() { var current = background; - for (var i = 0; i < 5; i++) { + for (var i = 0; i < 6; i++) { current = buildModalOverlay(current, i); } return current.build(); @@ -35,8 +35,8 @@ public class ModalOverlayStackComp extends SimpleRegionBuilder { modalOverlay.addListener((ListChangeListener) c -> { var ex = prop.get(); // Don't shift just for an index change - if (ex != null && modalOverlay.contains(ex)) { - currentIndex.set(modalOverlay.indexOf(ex)); + if (ex != null && c.getList().contains(ex)) { + currentIndex.set(c.getList().indexOf(ex)); return; } else { currentIndex.set(index); diff --git a/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java b/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java index 22a0d8e54..b06766468 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/OptionsComp.java @@ -28,6 +28,7 @@ import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; import lombok.Getter; import org.int4.fx.builders.common.AbstractRegionBuilder; +import org.kordamp.ikonli.javafx.FontIcon; import java.util.ArrayList; import java.util.HashMap; @@ -116,7 +117,9 @@ public class OptionsComp extends RegionBuilder { vbox.accessibleTextProperty().bind(joined); if (entry.documentationLink() != null) { - var link = new Button("... ?"); + var fi = new FontIcon("mdi2b-book-open-variant"); + fi.getStyleClass().add("graphic"); + var link = new Button("?", fi); link.setMinWidth(Region.USE_PREF_SIZE); link.getStyleClass().add(Styles.BUTTON_OUTLINED); link.getStyleClass().add(Styles.ACCENT); @@ -149,9 +152,13 @@ public class OptionsComp extends RegionBuilder { descriptionBox.visibleProperty().bind(compRegion.visibleProperty()); descriptionBox.managedProperty().bind(compRegion.managedProperty()); } else { - vbox.getChildren().add(description); + var descriptionBox = new HBox(description); + descriptionBox.getStyleClass().add("description-box"); + vbox.getChildren().add(descriptionBox); vbox.getChildren().add(new Spacer(2, Orientation.VERTICAL)); - VBox.setMargin(description, new Insets(0, 0, 0, 1)); + VBox.setMargin(descriptionBox, new Insets(0, 0, 0, 1)); + descriptionBox.visibleProperty().bind(compRegion.visibleProperty()); + descriptionBox.managedProperty().bind(compRegion.managedProperty()); } line.getChildren().add(vbox); diff --git a/app/src/main/java/io/xpipe/app/comp/base/PrettyImageComp.java b/app/src/main/java/io/xpipe/app/comp/base/PrettyImageComp.java index 5fb73203e..101d84e5b 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/PrettyImageComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/PrettyImageComp.java @@ -15,6 +15,7 @@ import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import java.lang.ref.WeakReference; import java.util.function.Consumer; public class PrettyImageComp extends SimpleRegionBuilder { @@ -113,8 +114,12 @@ public class PrettyImageComp extends SimpleRegionBuilder { value.subscribe(update); if (AppPrefs.get() != null) { + var ref = new WeakReference<>(update); AppPrefs.get().theme().addListener((observable, oldValue, newValue) -> { - update.accept(value.getValue()); + var v = ref.get(); + if (v != null) { + v.accept(value.getValue()); + } }); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java index 34a23e81c..c97dc743e 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SecretFieldComp.java @@ -10,6 +10,7 @@ import io.xpipe.app.platform.PlatformThread; import io.xpipe.core.InPlaceSecretValue; import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; @@ -34,11 +35,11 @@ import java.util.Objects; public class SecretFieldComp extends RegionStructureBuilder { - private final Property value; + private final ObjectProperty value; private final boolean allowCopy; private final List> additionalButtons = new ArrayList<>(); - public SecretFieldComp(Property value, boolean allowCopy) { + public SecretFieldComp(ObjectProperty value, boolean allowCopy) { this.value = value; this.allowCopy = allowCopy; } @@ -123,6 +124,7 @@ public class SecretFieldComp extends RegionStructureBuilder { ClipboardHelper.copyPassword(value.getValue(), true); }) + .disable(value.isNull()) .describe(d -> d.nameKey("copy")); var list = new ArrayList>(); 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 35b1ee029..2082d15ff 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 @@ -58,29 +58,34 @@ public class SideMenuBarComp extends RegionBuilder { value.setValue(e); } }); - b.describe(d -> d.name(e.name())); - var stack = createStyle(e, b); + var stack = createStyle(e, b, false); var shortcut = e.combination(); if (shortcut != null) { stack.apply(struc -> struc.getProperties().put("shortcut", shortcut)); } + b.describe(d -> { + d.name(e.name()); + if (shortcut != null) { + d.shortcut(shortcut); + } + }); vbox.getChildren().add(stack.build()); } { var b = new IconButtonComp("mdi2u-update", () -> { - var r = UpdateAvailableDialog.showIfNeeded(false); - if (!r) { - AppPrefs.get().selectCategory("about"); - ThreadHelper.runFailableAsync(() -> { + ThreadHelper.runFailableAsync(() -> { + var r = UpdateAvailableDialog.showIfNeeded(false); + if (!r) { + AppPrefs.get().selectCategory("about"); UpdateHandler uh = AppDistributionType.get().getUpdateHandler(); uh.prepareUpdate(); - }); - } + } + }); }); b.describe(d -> d.nameKey("updateAvailableTooltip")); - var stack = createStyle(null, b); + var stack = createStyle(null, b, false); var h = AppDistributionType.get().getUpdateHandler(); stack.hide(Bindings.createBooleanBinding( () -> { @@ -94,7 +99,7 @@ public class SideMenuBarComp extends RegionBuilder { if (!AppProperties.get().isStaging()) { var b = new IconButtonComp("mdoal-insights", () -> Hyperlinks.open(Hyperlinks.GITHUB_PTB)); b.describe(d -> d.nameKey("ptbAvailableTooltip")); - var stack = createStyle(null, b); + var stack = createStyle(null, b, false); stack.hide(AppLayoutModel.get().getPtbAvailable().not()); vbox.getChildren().add(stack.build()); } @@ -125,8 +130,10 @@ public class SideMenuBarComp extends RegionBuilder { e.consume(); }); }); - var stack = createStyle(null, b); - (item.isTop() ? topQueueButtons : bottomQueueButtons).getChildren().add(stack.build()); + var stack = createStyle(null, b, !item.isTop()); + (item.isTop() ? topQueueButtons : bottomQueueButtons) + .getChildren() + .add(stack.build()); } }); }); @@ -138,7 +145,7 @@ public class SideMenuBarComp extends RegionBuilder { return vbox; } - private BaseRegionBuilder createStyle(AppLayoutModel.Entry e, IconButtonComp b) { + private BaseRegionBuilder createStyle(AppLayoutModel.Entry e, IconButtonComp b, boolean highlight) { var selected = PseudoClass.getPseudoClass("selected"); b.apply(struc -> { @@ -189,14 +196,14 @@ public class SideMenuBarComp extends RegionBuilder { .backgroundProperty() .bind(Bindings.createObjectBinding( () -> { - if (value.getValue().equals(e)) { - return selectedBorder.get(); - } - if (struc.isHover()) { return hoverBorder.get(); } + if (highlight || value.getValue().equals(e)) { + return selectedBorder.get(); + } + return noneBorder.get(); }, struc.hoverProperty(), diff --git a/app/src/main/java/io/xpipe/app/comp/base/TestButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/TestButtonComp.java new file mode 100644 index 000000000..0e2cf1c8f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/TestButtonComp.java @@ -0,0 +1,66 @@ +package io.xpipe.app.comp.base; + +import io.xpipe.app.comp.RegionBuilder; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.FailableSupplier; + +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.layout.Region; + +import atlantafx.base.theme.Styles; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.concurrent.atomic.AtomicReference; + +@Getter +@AllArgsConstructor +public class TestButtonComp extends RegionBuilder