From fdc9bf47f33dd5eaef04b4bbad717d7dffc7623f Mon Sep 17 00:00:00 2001 From: crschnick Date: Sat, 21 Jun 2025 18:54:50 +0000 Subject: [PATCH] Rework --- README.md | 2 + app/build.gradle | 18 +- .../io/xpipe/app/action/AbstractAction.java | 191 +++++ .../io/xpipe/app/action/ActionConfigComp.java | 105 +++ .../xpipe/app/action/ActionConfirmComp.java | 79 ++ .../xpipe/app/action/ActionConfirmation.java | 42 + .../xpipe/app/action/ActionJacksonMapper.java | 105 +++ .../io/xpipe/app/action/ActionPickComp.java | 27 + .../io/xpipe/app/action/ActionProvider.java | 57 ++ .../xpipe/app/action/ActionShortcutComp.java | 88 +++ .../java/io/xpipe/app/action/ActionUrls.java | 146 ++++ .../xpipe/app/action/LauncherUrlProvider.java | 10 + .../xpipe/app/action/SerializableAction.java | 73 ++ .../xpipe/app/action/StoreContextAction.java | 10 + .../io/xpipe/app/action/XPipeUrlProvider.java | 23 + .../io/xpipe/app/beacon/AppBeaconServer.java | 6 +- .../app/beacon/BeaconRequestHandler.java | 18 +- .../java/io/xpipe/app/beacon/BlobManager.java | 4 +- .../app/beacon/impl/AskpassExchangeImpl.java | 22 +- .../impl/ConnectionAddExchangeImpl.java | 6 +- .../impl/ConnectionToggleExchangeImpl.java | 2 +- .../app/beacon/impl/FsScriptExchangeImpl.java | 2 +- .../beacon/impl/TerminalWaitExchangeImpl.java | 3 +- .../BrowserFileChooserSessionComp.java | 9 +- .../app/browser/BrowserFullSessionComp.java | 40 +- .../app/browser/BrowserFullSessionModel.java | 7 +- .../app/browser/BrowserSessionTabsComp.java | 53 +- .../app/browser/action/BrowserAction.java | 194 +++-- .../action/BrowserActionFormatter.java | 25 - .../browser/action/BrowserActionProvider.java | 18 + .../action/BrowserActionProviders.java | 13 + .../app/browser/action/BrowserLeafAction.java | 114 --- .../impl/ApplyFileEditActionProvider.java | 55 ++ .../BrowseInNativeManagerActionProvider.java | 49 ++ .../action/impl/ChgrpActionProvider.java | 61 ++ .../action/impl/ChmodActionProvider.java | 60 ++ .../action/impl/ChownActionProvider.java | 61 ++ .../ComputeDirectorySizesActionProvider.java | 42 + .../action/impl/DeleteActionProvider.java | 34 + .../action/impl/MoveFileActionProvider.java | 36 + .../impl/NewDirectoryActionProvider.java | 44 ++ .../action/impl/NewFileActionProvider.java | 44 ++ .../action/impl/NewLinkActionProvider.java | 48 ++ .../impl/OpenDirectoryActionProvider.java | 37 + .../impl/OpenFileDefaultActionProvider.java | 39 + .../OpenFileNativeDetailsActionProvider.java | 97 +++ .../impl/OpenFileWithActionProvider.java | 41 + .../impl/OpenTerminalActionProvider.java | 56 ++ .../RunCommandInBackgroundActionProvider.java | 63 ++ .../RunCommandInBrowserActionProvider.java | 43 ++ .../RunCommandInTerminalActionProvider.java | 50 ++ .../impl/TransferFilesActionProvider.java | 69 ++ .../xpipe/app/browser/file/BrowserAlerts.java | 85 +- .../browser/file/BrowserBreadcrumbBar.java | 77 +- .../app/browser/file/BrowserClipboard.java | 15 +- .../file/BrowserConnectionListComp.java | 5 +- .../file/BrowserConnectionListFilterComp.java | 4 +- .../app/browser/file/BrowserContextMenu.java | 17 +- .../app/browser/file/BrowserFileListComp.java | 94 ++- .../file/BrowserFileListFilterComp.java | 3 +- .../browser/file/BrowserFileListModel.java | 43 +- .../browser/file/BrowserFileListNameCell.java | 20 +- .../app/browser/file/BrowserFileOpener.java | 113 ++- .../app/browser/file/BrowserFileOutput.java | 35 + .../browser/file/BrowserFileSystemHelper.java | 25 +- .../file/BrowserFileSystemTabComp.java | 27 +- .../file/BrowserFileSystemTabModel.java | 285 ++----- .../file/BrowserFileTransferOperation.java | 190 +++-- .../file/BrowserHistorySavedStateImpl.java | 4 +- .../app/browser/file/BrowserOverviewComp.java | 10 +- .../browser/file/BrowserStatusBarComp.java | 3 +- .../app/browser/file/BrowserTransferComp.java | 2 +- .../browser/file/BrowserTransferModel.java | 19 +- .../icon/BrowserIconDirectoryType.java | 2 +- .../app/browser/icon/BrowserIconFileType.java | 5 +- .../xpipe/app/browser/icon/BrowserIcons.java | 4 +- .../BrowserApplicationPathMenuProvider.java} | 4 +- .../BrowserMenuBranchProvider.java} | 23 +- .../app/browser/menu/BrowserMenuCategory.java | 9 + .../browser/menu/BrowserMenuItemProvider.java | 58 ++ .../browser/menu/BrowserMenuLeafProvider.java | 156 ++++ .../browser/menu/BrowserMenuProviders.java | 38 + .../browser/menu/FileTypeMenuProvider.java | 12 +- .../menu/MultiExecuteMenuProvider.java | 15 +- .../MultiExecuteSelectionMenuProvider.java | 83 ++ .../browser/menu/impl/BackMenuProvider.java | 14 +- .../BrowseInNativeManagerMenuProvider.java | 41 + .../browser/menu/impl/ChgrpMenuProvider.java | 175 +++++ .../browser/menu/impl/ChmodMenuProvider.java | 173 +++++ .../browser/menu/impl/ChownMenuProvider.java | 174 +++++ .../ComputeDirectorySizesMenuProvider.java | 57 ++ .../browser/menu/impl/CopyMenuProvider.java | 19 +- .../menu/impl/CopyPathMenuProvider.java | 61 +- .../browser/menu/impl/DeleteMenuProvider.java | 66 ++ .../menu/impl/DownloadMenuProvider.java | 19 +- .../menu/impl/EditFileMenuProvider.java | 19 +- .../menu/impl/FollowLinkMenuProvider.java | 19 +- .../menu/impl/ForwardMenuProvider.java | 14 +- .../browser/menu/impl/JarMenuProvider.java | 24 +- .../browser/menu/impl/JavapMenuProvider.java | 61 ++ .../menu/impl/NewItemMenuProvider.java | 77 +- .../OpenDirectoryInNewTabMenuProvider.java | 19 +- .../menu/impl/OpenDirectoryMenuProvider.java | 45 ++ .../impl/OpenFileDefaultMenuProvider.java | 45 ++ .../menu/impl/OpenFileWithMenuProvider.java | 27 +- .../OpenNativeFileDetailsMenuProvider.java | 44 ++ .../menu/impl/OpenTerminalMenuProvider.java | 26 +- .../browser/menu/impl/PasteMenuProvider.java | 19 +- .../impl/RefreshDirectoryMenuProvider.java | 14 +- .../browser/menu/impl/RenameMenuProvider.java | 24 +- .../menu/impl/RunFileMenuProvider.java | 18 +- .../impl/compress/BaseUntarMenuProvider.java | 52 +- .../compress/BaseUnzipUnixMenuProvider.java | 50 +- .../BaseUnzipWindowsActionProvider.java | 63 ++ .../impl/compress/CompressMenuProvider.java | 226 ++++++ .../menu/impl/compress/TarActionProvider.java | 68 ++ .../impl/compress/UntarActionProvider.java | 59 ++ .../compress/UntarDirectoryMenuProvider.java | 8 + .../UntarGzDirectoryMenuProvider.java | 8 + .../compress/UntarGzHereMenuProvider.java | 8 + .../impl/compress/UntarHereMenuProvider.java | 8 + .../impl/compress/UnzipActionProvider.java | 85 ++ .../UnzipDirectoryUnixMenuProvider.java | 8 + .../UnzipDirectoryWindowsActionProvider.java | 8 + .../compress/UnzipHereUnixMenuProvider.java | 8 + .../UnzipHereWindowsActionProvider.java | 8 + .../menu/impl/compress/ZipActionProvider.java | 90 +++ .../io/xpipe/app/comp/base/AppLayoutComp.java | 2 +- .../comp/base/AppMainWindowContentComp.java | 83 +- .../io/xpipe/app/comp/base/ChoiceComp.java | 6 - .../xpipe/app/comp/base/ChoicePaneComp.java | 6 + .../ContextualFileReferenceChoiceComp.java | 26 +- .../io/xpipe/app/comp/base/DropdownComp.java | 65 -- .../app/comp/base/FileDropOverlayComp.java | 4 +- .../xpipe/app/comp/base/IconButtonComp.java | 12 +- .../app/comp/base/LazyTextFieldComp.java | 29 +- .../app/comp/base/LeftSplitPaneComp.java | 9 +- .../xpipe/app/comp/base/ListBoxViewComp.java | 4 +- .../xpipe/app/comp/base/LoadingIconComp.java | 71 ++ .../app/comp/base/LoadingOverlayComp.java | 55 +- .../io/xpipe/app/comp/base/MarkdownComp.java | 56 +- .../xpipe/app/comp/base/ModalOverlayComp.java | 4 +- .../app/comp/base/ModalOverlayStackComp.java | 1 + .../io/xpipe/app/comp/base/OptionsComp.java | 59 +- .../xpipe/app/comp/base/PrettyImageComp.java | 30 +- .../app/comp/base/PrettyImageHelper.java | 2 +- .../xpipe/app/comp/base/SideMenuBarComp.java | 17 +- .../io/xpipe/app/comp/base/TextFieldComp.java | 2 - .../xpipe/app/comp/base/TileButtonComp.java | 12 +- .../xpipe/app/comp/base/ToggleGroupComp.java | 50 +- .../xpipe/app/comp/base/ToggleSwitchComp.java | 13 +- .../io/xpipe/app/comp/base/TooltipHelper.java | 5 +- .../xpipe/app/comp/store/StoreEntryComp.java | 554 ------------- .../java/io/xpipe/app/core/AppArguments.java | 12 +- .../main/java/io/xpipe/app/core/AppCache.java | 14 +- .../java/io/xpipe/app/core/AppDataLock.java | 6 +- .../xpipe/app/core/AppDesktopIntegration.java | 9 +- .../xpipe/app/core/AppExtensionManager.java | 16 +- .../io/xpipe/app/core/AppFileWatcher.java | 14 +- .../main/java/io/xpipe/app/core/AppFont.java | 1 - .../java/io/xpipe/app/core/AppFontSizes.java | 2 +- .../io/xpipe/app/core/AppGreetingsDialog.java | 1 - .../main/java/io/xpipe/app/core/AppI18n.java | 4 +- .../java/io/xpipe/app/core/AppI18nData.java | 12 +- .../app/{resources => core}/AppImages.java | 7 +- .../java/io/xpipe/app/core/AppInstance.java | 6 +- .../io/xpipe/app/core/AppLayoutModel.java | 23 +- .../main/java/io/xpipe/app/core/AppLogs.java | 6 +- .../java/io/xpipe/app/core/AppMacros.java | 18 + .../io/xpipe/app/core/AppOpenArguments.java | 68 +- .../java/io/xpipe/app/core/AppProperties.java | 6 +- .../app/{resources => core}/AppResources.java | 14 +- .../main/java/io/xpipe/app/core/AppSid.java | 6 +- .../main/java/io/xpipe/app/core/AppStyle.java | 8 +- .../main/java/io/xpipe/app/core/AppTheme.java | 16 +- .../java/io/xpipe/app/core/AppTrayIcon.java | 10 +- .../io/xpipe/app/core/AppWindowsShutdown.java | 7 +- .../io/xpipe/app/core/check/AppAvCheck.java | 2 +- .../core/check/AppHomebrewCoreutilsCheck.java | 4 +- .../app/core/check/AppJavaOptionsCheck.java | 4 +- .../app/core/check/AppPathCorruptCheck.java | 4 +- .../xpipe/app/core/check/AppRosettaCheck.java | 4 +- .../xpipe/app/core/check/AppShellCheck.java | 127 +-- .../xpipe/app/core/check/AppShellChecker.java | 8 +- .../io/xpipe/app/core/check/AppTempCheck.java | 4 +- .../app/core/check/AppUserDirectoryCheck.java | 4 +- .../java/io/xpipe/app/core/mode/BaseMode.java | 14 +- .../java/io/xpipe/app/core/mode/GuiMode.java | 2 +- .../io/xpipe/app/core/mode/OperationMode.java | 10 +- .../io/xpipe/app/core/window/AppDialog.java | 4 +- .../xpipe/app/core/window/AppMainWindow.java | 14 +- .../app/core/window/AppWindowHelper.java | 13 - .../xpipe/app/core/window/ModifiedStage.java | 6 +- .../core/window/NativeMacOsWindowControl.java | 4 +- .../java/io/xpipe/app/ext/ActionProvider.java | 267 ------- .../xpipe/app/ext/ConnectionFileSystem.java | 60 +- .../app/ext/DataStoreCreationCategory.java | 1 + .../io/xpipe/app/ext/DataStoreProvider.java | 24 +- .../io/xpipe/app/ext/DataStoreProviders.java | 8 +- .../xpipe/app/ext/DataStoreUsageCategory.java | 4 +- .../app/ext/EnabledParentStoreProvider.java | 8 +- .../java/io/xpipe/app/ext/LocalStore.java | 2 - .../xpipe/app/ext}/NetworkTunnelSession.java | 6 +- .../io/xpipe/app/ext}/NetworkTunnelStore.java | 4 +- .../io/xpipe/app/ext/PrefsChoiceValue.java | 10 +- .../java/io/xpipe/app/ext/PrefsHandler.java | 4 +- .../java/io/xpipe/app/ext/PrefsValue.java | 12 + .../xpipe/app/ext/ProcessControlProvider.java | 8 +- .../main/java/io/xpipe/app/ext/Session.java | 59 ++ .../io/xpipe/app/ext}/SessionListener.java | 2 +- .../java/io/xpipe/app/ext/ShellSession.java | 19 +- .../java/io/xpipe/app/ext/ShellStore.java | 20 +- .../xpipe/app/ext}/SingletonSessionStore.java | 18 +- .../ext/SingletonSessionStoreProvider.java | 3 +- .../java/io/xpipe/app/ext/UserBasedValue.java | 13 - .../io/xpipe/app/ext/WrapperFileSystem.java | 192 +++++ .../app/hub/action/BatchHubProvider.java | 64 ++ .../app/hub/action/BatchStoreAction.java | 64 ++ .../app/hub/action/HubBranchProvider.java | 11 + .../xpipe/app/hub/action/HubLeafProvider.java | 41 + .../app/hub/action/HubMenuItemProvider.java | 29 + .../app/hub/action/MultiStoreAction.java | 64 ++ .../io/xpipe/app/hub/action/StoreAction.java | 72 ++ .../app/hub/action/StoreActionCategory.java | 10 + .../action/impl/BrowseHubLeafProvider.java | 73 ++ .../hub/action/impl/CloneHubLeafProvider.java | 66 ++ .../hub/action/impl/EditHubLeafProvider.java | 58 ++ .../impl/LaunchHubMenuLeafProvider.java | 74 ++ .../impl/RefreshChildrenHubLeafProvider.java | 68 ++ .../action/impl/RefreshHubLeafProvider.java | 26 + .../action/impl/SampleHubLeafProvider.java | 90 ++- .../hub/action/impl/ScanHubBatchProvider.java | 62 ++ .../hub/action/impl/ScanHubLeafProvider.java | 78 ++ .../comp}/DenseStoreEntryComp.java | 30 +- .../{comp/store => hub/comp}/OsLogoComp.java | 4 +- .../comp}/StandardStoreEntryComp.java | 21 +- .../store => hub/comp}/StoreActiveComp.java | 2 +- .../store => hub/comp}/StoreCategoryComp.java | 7 +- .../comp}/StoreCategoryConfigComp.java | 12 +- .../comp}/StoreCategoryListComp.java | 2 +- .../comp}/StoreCategoryWrapper.java | 12 +- .../store => hub/comp}/StoreChoiceComp.java | 11 +- .../store => hub/comp}/StoreCreationComp.java | 17 +- .../comp}/StoreCreationConsumer.java | 2 +- .../comp}/StoreCreationDialog.java | 9 +- .../store => hub/comp}/StoreCreationMenu.java | 71 +- .../comp}/StoreCreationModel.java | 45 +- .../comp}/StoreEntryBatchSelectComp.java | 2 +- .../io/xpipe/app/hub/comp/StoreEntryComp.java | 624 +++++++++++++++ .../comp}/StoreEntryListComp.java | 8 +- .../comp}/StoreEntryListOverviewComp.java | 103 ++- .../comp}/StoreEntryListStatusBarComp.java | 58 +- .../store => hub/comp}/StoreEntryWrapper.java | 202 +++-- .../comp}/StoreIconChoiceComp.java | 7 +- .../comp}/StoreIconChoiceDialog.java | 2 +- .../store => hub/comp}/StoreIconComp.java | 2 +- .../comp}/StoreIdentitiesIntroComp.java | 2 +- .../store => hub/comp}/StoreIntroComp.java | 2 +- .../store => hub/comp}/StoreLayoutComp.java | 2 +- .../comp}/StoreListChoiceComp.java | 55 +- .../store => hub/comp}/StoreNotFoundComp.java | 2 +- .../{comp/store => hub/comp}/StoreNotes.java | 2 +- .../store => hub/comp}/StoreNotesComp.java | 4 +- .../app/hub/comp/StoreOrderIndexDialog.java | 33 + .../comp}/StoreProviderChoiceComp.java | 2 +- .../comp}/StoreQuickAccessButtonComp.java | 2 +- .../comp}/StoreScriptsIntroComp.java | 2 +- .../store => hub/comp}/StoreSection.java | 75 +- .../comp}/StoreSectionBaseComp.java | 2 +- .../store => hub/comp}/StoreSectionComp.java | 6 +- .../comp}/StoreSectionMiniComp.java | 2 +- .../comp/StoreSectionSortMode.java} | 53 +- .../store => hub/comp}/StoreSidebarComp.java | 2 +- .../store => hub/comp}/StoreToggleComp.java | 2 +- .../store => hub/comp}/StoreViewState.java | 95 ++- .../store => hub/comp}/SystemStateComp.java | 7 +- .../io/xpipe/app/icon/SystemIconCache.java | 46 +- .../io/xpipe/app/icon/SystemIconManager.java | 25 +- .../io/xpipe/app/icon/SystemIconSource.java | 4 +- .../xpipe/app/icon/SystemIconSourceData.java | 7 +- .../xpipe/app/icon/SystemIconSourceFile.java | 4 +- .../java/io/xpipe/app/issue/ErrorEvent.java | 67 +- .../io/xpipe/app/issue/ErrorEventFactory.java | 76 ++ .../xpipe/app/issue/ErrorHandlerDialog.java | 20 +- .../io/xpipe/app/issue/GuiErrorHandler.java | 29 +- .../xpipe/app/issue/GuiErrorHandlerBase.java | 2 + .../xpipe/app/issue/SentryErrorHandler.java | 8 +- .../xpipe/app/issue/TerminalErrorHandler.java | 4 +- .../io/xpipe/app/issue/UserReportComp.java | 1 - .../password/PasswordManagerFixedCommand.java | 18 - .../io/xpipe/app/prefs/AboutCategory.java | 8 +- .../java/io/xpipe/app/prefs/AppPrefs.java | 50 +- .../io/xpipe/app/prefs/AppPrefsCategory.java | 3 + .../java/io/xpipe/app/prefs/AppPrefsComp.java | 2 - .../xpipe/app/prefs/AppPrefsSidebarComp.java | 11 +- .../app/prefs/AppPrefsStorageHandler.java | 16 +- .../xpipe/app/prefs/AppearanceCategory.java | 9 +- .../app/prefs/ConnectionHubCategory.java | 6 + .../io/xpipe/app/prefs/DeveloperCategory.java | 6 + .../io/xpipe/app/prefs/EditorCategory.java | 6 + .../app/prefs/ExternalApplicationType.java | 148 ++-- .../xpipe/app/prefs/ExternalEditorType.java | 340 +++++--- .../app/prefs/ExternalRdpClientType.java | 412 ---------- .../xpipe/app/prefs/FileBrowserCategory.java | 6 + .../io/xpipe/app/prefs/HttpApiCategory.java | 6 + .../io/xpipe/app/prefs/IconsCategory.java | 56 +- .../io/xpipe/app/prefs/LinksCategory.java | 6 + .../io/xpipe/app/prefs/LoggingCategory.java | 9 +- .../app/prefs/PasswordManagerCategory.java | 85 +- .../app/prefs/PasswordManagerTestComp.java | 121 +++ .../java/io/xpipe/app/prefs/RdpCategory.java | 11 +- .../io/xpipe/app/prefs/SecurityCategory.java | 6 + .../java/io/xpipe/app/prefs/SshCategory.java | 9 +- .../java/io/xpipe/app/prefs/SyncCategory.java | 14 +- .../io/xpipe/app/prefs/SystemCategory.java | 7 +- .../io/xpipe/app/prefs/TerminalCategory.java | 19 +- .../xpipe/app/prefs/ThirdPartyDependency.java | 2 +- .../xpipe/app/prefs/TroubleshootCategory.java | 14 +- .../io/xpipe/app/prefs/UpdateCheckComp.java | 2 +- .../io/xpipe/app/prefs/UpdatesCategory.java | 6 + .../io/xpipe/app/prefs/VaultCategory.java | 6 + .../app/prefs/WorkspaceCreationDialog.java | 4 +- .../xpipe/app/prefs/WorkspacesCategory.java | 6 + .../BitwardenPasswordManager.java | 27 +- .../DashlanePasswordManager.java | 29 +- .../EnpassPasswordManager.java | 61 +- .../KeePassXcAssociationKey.java | 2 +- .../KeePassXcPasswordManager.java} | 37 +- .../KeePassXcProxyClient.java | 47 +- .../KeeperPasswordManager.java | 43 +- .../LastpassPasswordManager.java | 45 +- .../OnePasswordManager.java | 31 +- .../{password => pwman}/PasswordManager.java | 23 +- .../PasswordManagerCommand.java | 23 +- .../PasswordManagerCommandTemplate.java | 2 +- .../PsonoPasswordManager.java | 63 +- .../{password => pwman}/TweetNaClHelper.java | 16 +- .../WindowsCredentialManager.java | 37 +- .../io/xpipe/app/rdp/CustomRdpClient.java | 44 ++ .../xpipe/app/rdp/DevolutionsRdpClient.java | 59 ++ .../io/xpipe/app/rdp/ExternalRdpClient.java | 92 +++ .../java/io/xpipe/app/rdp/FreeRdpClient.java | 38 + .../java/io/xpipe/app/rdp/MstscRdpClient.java | 79 ++ .../io/xpipe/app/rdp/RdpLaunchConfig.java | 16 + .../io/xpipe/app/rdp/RemminaRdpClient.java | 58 ++ .../app/rdp/RemoteDesktopAppRdpClient.java | 33 + .../io/xpipe/app/rdp/WindowsAppRdpClient.java | 33 + .../io/xpipe/app/storage/DataStorage.java | 45 +- .../io/xpipe/app/storage/DataStorageNode.java | 6 +- .../app/storage/DataStorageSyncHandler.java | 2 + .../xpipe/app/storage/DataStoreCategory.java | 25 +- .../app/storage/DataStoreCategoryConfig.java | 16 +- .../io/xpipe/app/storage/DataStoreEntry.java | 100 +-- .../app/storage/ImpersistentStorage.java | 6 +- .../io/xpipe/app/storage/StandardStorage.java | 148 ++-- .../app/terminal/AlacrittyTerminalType.java | 87 ++- .../xpipe/app/terminal/CmdTerminalType.java | 32 +- .../app/terminal/CustomTerminalType.java | 15 +- .../app/terminal/ExternalTerminalType.java | 175 +---- .../xpipe/app/terminal/GnomeConsoleType.java | 26 +- .../xpipe/app/terminal/GnomeTerminalType.java | 27 +- .../app/terminal/ITerm2TerminalType.java | 52 ++ .../xpipe/app/terminal/KittyTerminalType.java | 21 +- .../app/terminal/KonsoleTerminalType.java | 83 ++ .../xpipe/app/terminal/MacOsTerminalType.java | 47 ++ .../app/terminal/MobaXTermTerminalType.java | 46 +- .../app/terminal/PowerShellTerminalType.java | 24 +- .../app/terminal/PtyxisTerminalType.java | 26 +- .../xpipe/app/terminal/PwshTerminalType.java | 45 +- .../app/terminal/SecureCrtTerminalType.java | 43 +- .../xpipe/app/terminal/TabbyTerminalType.java | 68 +- .../xpipe/app/terminal/TerminalDockComp.java | 9 +- .../terminal/TerminalLaunchConfiguration.java | 14 +- .../app/terminal/TerminalLaunchRequest.java | 4 +- .../xpipe/app/terminal/TerminalLauncher.java | 8 +- .../io/xpipe/app/terminal/TerminalPrompt.java | 4 +- .../app/terminal/TerminalPromptManager.java | 4 +- .../app/terminal/TerminalProxyManager.java | 6 +- .../app/terminal/TermiusTerminalType.java | 4 +- .../app/terminal/TrackableTerminalType.java | 2 +- .../xpipe/app/terminal/WarpTerminalType.java | 23 +- .../xpipe/app/terminal/WaveTerminalType.java | 6 +- .../xpipe/app/terminal/WezTerminalType.java | 68 +- .../app/terminal/WindowsTerminalType.java | 12 +- .../app/terminal/XShellTerminalType.java | 33 +- .../xpipe/app/update/AppDistributionType.java | 21 +- .../io/xpipe/app/update/AppDownloads.java | 15 +- .../java/io/xpipe/app/update/AppRelease.java | 19 +- .../io/xpipe/app/update/ChocoUpdater.java | 40 +- .../io/xpipe/app/update/CommandUpdater.java | 4 +- .../io/xpipe/app/update/GitHubUpdater.java | 4 +- .../app/update/UpdateChangelogAlert.java | 27 +- .../io/xpipe/app/update/UpdateHandler.java | 15 +- .../io/xpipe/app/update/UpdateNagDialog.java | 2 +- .../io/xpipe/app/update/WingetUpdater.java | 37 +- .../io/xpipe/app/util/AppJacksonModule.java | 38 +- .../io/xpipe/app/util/BindingsHelper.java | 23 +- .../java/io/xpipe/app/util/BooleanScope.java | 5 + .../io/xpipe/app/util/ChainedValidator.java | 1 - .../main/java/io/xpipe/app/util/Check.java | 145 ++++ .../io/xpipe/app/util/ClipboardHelper.java | 55 +- .../io/xpipe/app/util/CommandSupport.java | 8 +- .../app/util/DataStoreCategoryChoiceComp.java | 4 +- .../io/xpipe/app/util/DataStoreFormatter.java | 33 +- .../java/io/xpipe/app/util/DesktopHelper.java | 8 +- .../io/xpipe/app/util/DocumentationLink.java | 7 +- .../io/xpipe/app/util/ExclusiveValidator.java | 1 - .../java/io/xpipe/app/util/FileBridge.java | 41 +- .../java/io/xpipe/app/util/FileOpener.java | 50 +- .../io/xpipe/app/util/GlobalClipboard.java | 46 +- .../java/io/xpipe/app/util/LocalShell.java | 13 +- .../io/xpipe/app/util/LocalShellCache.java | 42 - .../java/io/xpipe/app/util/ModuleAccess.java | 12 +- .../java/io/xpipe/app/util/NativeBridge.java | 4 +- .../java/io/xpipe/app/util/NodeCallback.java | 6 + .../io/xpipe/app/util/OptionsBuilder.java | 14 +- .../xpipe/app/util/OptionsChoiceBuilder.java | 4 +- .../java/io/xpipe/app/util/PlatformState.java | 4 +- .../io/xpipe/app/util/PlatformThread.java | 10 +- .../java/io/xpipe/app/util/RdpConfig.java | 4 +- .../java/io/xpipe/app/util/RemminaHelper.java | 101 +++ .../io/xpipe/app/util/ScanDialogAction.java | 4 +- .../io/xpipe/app/util/ScanDialogBase.java | 6 +- .../xpipe/app/util/ScanMultiDialogComp.java | 5 +- .../xpipe/app/util/ScanSingleDialogComp.java | 8 +- .../java/io/xpipe/app/util/ScriptHelper.java | 35 +- .../app/util/SecretRetrievalStrategy.java | 30 +- .../util/SecretRetrievalStrategyHelper.java | 5 +- .../java/io/xpipe/app/util/ShellTemp.java | 8 +- .../io/xpipe/app/util/SimpleValidator.java | 1 - .../io/xpipe/app/util/SshLocalBridge.java | 6 +- .../io/xpipe/app/util/StoreStateFormat.java | 2 +- .../java/io/xpipe/app/util/ThreadHelper.java | 6 +- .../java/io/xpipe/app/util/Validator.java | 1 - .../io/xpipe/app/util/WindowsRegistry.java | 12 +- .../io/xpipe/app/vnc/CustomVncClient.java | 54 ++ .../io/xpipe/app/vnc/ExternalVncClient.java | 58 ++ .../io/xpipe/app/vnc/InternalVncClient.java | 41 + .../java/io/xpipe/app/vnc/RealVncClient.java | 106 +++ .../io/xpipe/app/vnc/RemminaVncClient.java | 48 ++ .../xpipe/app/vnc/ScreenSharingVncClient.java | 38 + .../java/io/xpipe/app/vnc/TigerVncClient.java | 142 ++++ .../java/io/xpipe/app/vnc/TightVncClient.java | 52 ++ .../java/io/xpipe/app/vnc/VncBaseStore.java | 15 + .../java/io/xpipe/app/vnc/VncCategory.java | 55 ++ .../io/xpipe/app/vnc/VncLaunchConfig.java | 42 + app/src/main/java/module-info.java | 95 ++- .../resources/img/logo/full/logo_128x128.png | Bin 10988 -> 53278 bytes .../resources/img/logo/full/logo_256x256.png | Bin 25614 -> 159426 bytes .../app/resources/img/logo/loading-dark.png | Bin 0 -> 10632 bytes .../xpipe/app/resources/img/logo/loading.png | Bin 7208 -> 24312 bytes .../img/shortcut/actionShortcut_icon-16.png | Bin 0 -> 593 bytes .../img/shortcut/actionShortcut_icon-24.png | Bin 0 -> 835 bytes .../img/shortcut/actionShortcut_icon-40.png | Bin 0 -> 1318 bytes .../img/shortcut/actionShortcut_icon-80.png | Bin 0 -> 2531 bytes .../io/xpipe/app/resources/style/bookmark.css | 2 +- .../io/xpipe/app/resources/style/browser.css | 23 +- .../io/xpipe/app/resources/style/category.css | 5 +- .../xpipe/app/resources/style/choice-comp.css | 5 + .../io/xpipe/app/resources/style/frame.css | 6 +- .../xpipe/app/resources/style/header-bars.css | 12 +- .../resources/style/modal-overlay-comp.css | 20 + .../xpipe/app/resources/style/popup-menu.css | 11 +- .../io/xpipe/app/resources/style/prefs.css | 4 +- .../app/resources/style/store-entry-comp.css | 30 +- .../resources/style/store-mini-section.css | 4 +- .../io/xpipe/app/resources/style/style.css | 14 +- .../app/resources/style/tile-button-comp.css | 2 +- .../app/resources/theme/cupertinoDark.css | 2 +- .../app/resources/theme/cupertinoLight.css | 2 +- beacon/build.gradle | 4 +- build.gradle | 29 +- core/build.gradle | 4 +- .../xpipe/core/dialog/BaseQueryElement.java | 45 -- .../io/xpipe/core/dialog/BusyElement.java | 21 - .../java/io/xpipe/core/dialog/Choice.java | 49 -- .../io/xpipe/core/dialog/ChoiceElement.java | 85 -- .../java/io/xpipe/core/dialog/Dialog.java | 529 ------------- .../core/dialog/DialogCancelException.java | 26 - .../io/xpipe/core/dialog/DialogElement.java | 31 - .../io/xpipe/core/dialog/DialogMapper.java | 55 -- .../io/xpipe/core/dialog/DialogReference.java | 25 - .../io/xpipe/core/dialog/HeaderElement.java | 31 - .../io/xpipe/core/dialog/QueryConverter.java | 167 ---- .../io/xpipe/core/dialog/QueryElement.java | 46 -- .../io/xpipe/core/process/CommandBuilder.java | 7 + .../core/process/CommandConfiguration.java | 2 +- .../io/xpipe/core/process/CommandControl.java | 2 +- .../java/io/xpipe/core/process/OsType.java | 4 +- .../core/process/ParentSystemAccess.java | 4 +- .../io/xpipe/core/process/ProcessControl.java | 2 + .../core/process/ProcessOutputException.java | 10 +- .../xpipe/core/process/ShellCapabilities.java | 15 - .../io/xpipe/core/process/ShellControl.java | 6 +- .../io/xpipe/core/process/ShellDialect.java | 8 +- .../core/process/ShellDialectAskpass.java | 4 +- .../io/xpipe/core/process/ShellDialects.java | 4 + .../io/xpipe/core/process/ShellDumbMode.java | 1 - .../java/io/xpipe/core/process/ShellView.java | 9 +- .../core/process/WrapperShellControl.java | 17 +- .../java/io/xpipe/core/store/FileEntry.java | 3 + .../java/io/xpipe/core/store/FileNames.java | 59 -- .../java/io/xpipe/core/store/FilePath.java | 56 +- .../java/io/xpipe/core/store/FileSystem.java | 6 + .../java/io/xpipe/core/store/Session.java | 29 - .../io/xpipe/core/util/CoreJacksonModule.java | 11 +- .../io/xpipe/core/util/JacksonExtension.java | 6 - .../io/xpipe/core/util/JacksonMapper.java | 32 - .../java/io/xpipe/core/util/KeyValue.java | 15 + core/src/main/java/module-info.java | 3 - dist/base.gradle | 20 + dist/changelogs/16.7.md | 93 --- dist/changelogs/16.7_incremental.md | 3 - dist/changelogs/17.0.md | 76 ++ dist/debug/linux/xpiped_debug.sh | 2 - dist/debug/mac/xpiped_debug.sh | 2 - dist/debug/windows/xpiped_debug.bat | 2 - dist/jpackage.gradle | 4 +- dist/jpackage/Info.plist | 4 +- dist/licenses/commons-io.properties | 2 +- dist/licenses/graalvm.properties | 2 +- dist/licenses/jackson.properties | 2 +- dist/licenses/jna.properties | 2 +- dist/licenses/lombok.properties | 2 +- dist/licenses/openjfx.properties | 2 +- dist/licenses/picocli.properties | 2 +- dist/licenses/sentry.properties | 2 +- dist/licenses/slf4j.properties | 2 +- dist/licenses/vernacular-vnc.properties | 4 +- dist/logo/hicolor/1024x1024/apps/xpipe.png | Bin 109894 -> 1720473 bytes dist/logo/hicolor/128x128/apps/xpipe.png | Bin 10988 -> 53278 bytes dist/logo/hicolor/256x256/apps/xpipe.png | Bin 25614 -> 159426 bytes dist/logo/hicolor/512x512/apps/xpipe.png | Bin 47253 -> 521579 bytes dist/logo/ico/logo_1024x1024.png | Bin 0 -> 1720473 bytes dist/logo/ico/logo_128x128.png | Bin 10988 -> 53278 bytes dist/logo/ico/logo_256x256.png | Bin 25614 -> 159426 bytes dist/logo/ico/logo_512x512.png | Bin 0 -> 521579 bytes dist/logo/logo.icns | Bin 212561 -> 44611 bytes dist/logo/logo.ico | Bin 133984 -> 1940106 bytes dist/logo/logo.icon/Assets/logo.svg | 104 +++ dist/logo/logo.icon/Assets/shadow.svg | 72 ++ dist/logo/logo.icon/icon.json | 205 +++++ dist/logo/logo.iconset/icon_128x128.png | Bin 8485 -> 0 bytes dist/logo/logo.iconset/icon_128x128@2.png | Bin 20270 -> 0 bytes dist/logo/logo.iconset/icon_16x16.png | Bin 754 -> 0 bytes dist/logo/logo.iconset/icon_16x16@2.png | Bin 1608 -> 0 bytes dist/logo/logo.iconset/icon_256x256.png | Bin 20270 -> 0 bytes dist/logo/logo.iconset/icon_256x256@2.png | Bin 47253 -> 0 bytes dist/logo/logo.iconset/icon_32x32.png | Bin 1608 -> 0 bytes dist/logo/logo.iconset/icon_32x32@2.png | Bin 3640 -> 0 bytes dist/logo/logo.iconset/icon_512x512.png | Bin 47253 -> 0 bytes dist/logo/logo.iconset/icon_512x512@2.png | Bin 109894 -> 0 bytes dist/logo/logo.png | Bin 10988 -> 53278 bytes .../ext/base/action/BrowseStoreAction.java | 65 -- .../base/action/ChangeStoreIconAction.java | 61 -- .../ext/base/action/CloneStoreAction.java | 69 -- .../base/action/EditScriptStoreAction.java | 68 -- .../ext/base/action/EditStoreAction.java | 88 --- .../ext/base/action/LaunchStoreAction.java | 76 -- .../action/RefreshChildrenStoreAction.java | 84 -- .../ext/base/action/RunScriptActionMenu.java | 727 ------------------ .../ext/base/action/ScanStoreAction.java | 108 --- .../ext/base/action/ShareStoreAction.java | 66 -- .../xpipe/ext/base/action/XPipeUrlAction.java | 116 --- .../browser/BrowseInNativeManagerAction.java | 58 -- .../xpipe/ext/base/browser/ChgrpAction.java | 183 ----- .../xpipe/ext/base/browser/ChmodAction.java | 181 ----- .../xpipe/ext/base/browser/ChownAction.java | 182 ----- .../xpipe/ext/base/browser/DeleteAction.java | 59 -- .../ext/base/browser/DeleteLinkAction.java | 51 -- .../browser/ExecuteApplicationAction.java | 36 - .../io/xpipe/ext/base/browser/JavaAction.java | 11 - .../xpipe/ext/base/browser/JavapAction.java | 42 - .../browser/MultiExecuteSelectionAction.java | 126 --- .../ext/base/browser/OpenDirectoryAction.java | 51 -- .../base/browser/OpenFileDefaultAction.java | 54 -- .../browser/OpenNativeFileDetailsAction.java | 108 --- .../ext/base/browser/ToFileCommandAction.java | 28 - .../browser/compress/BaseCompressAction.java | 337 -------- .../compress/BaseUnzipWindowsAction.java | 90 --- .../compress/DirectoryCompressAction.java | 8 - .../browser/compress/FileCompressAction.java | 8 - .../compress/UntarDirectoryAction.java | 8 - .../compress/UntarGzDirectoryAction.java | 8 - .../browser/compress/UntarGzHereAction.java | 8 - .../browser/compress/UntarHereAction.java | 8 - .../compress/UnzipDirectoryUnixAction.java | 8 - .../compress/UnzipDirectoryWindowsAction.java | 8 - .../browser/compress/UnzipHereUnixAction.java | 8 - .../compress/UnzipHereWindowsAction.java | 8 - .../DesktopApplicationStoreProvider.java | 36 +- .../ext/base/identity/IdentityChoice.java | 17 +- .../ext/base/identity/IdentitySelectComp.java | 11 +- .../ext/base/identity/IdentityStore.java | 6 +- .../base/identity/IdentityStoreProvider.java | 17 +- .../ext/base/identity/IdentityValue.java | 10 +- .../identity/LocalIdentityConvertAction.java | 70 -- .../LocalIdentityConvertActionProvider.java | 78 ++ .../ext/base/identity/LocalIdentityStore.java | 7 +- .../identity/LocalIdentityStoreProvider.java | 10 +- .../PasswordManagerIdentityStore.java | 125 +++ .../PasswordManagerIdentityStoreProvider.java | 59 ++ .../identity/SshIdentityStateManager.java | 34 +- .../base/identity/SshIdentityStrategy.java | 198 +++-- .../base/identity/SyncedIdentityStore.java | 7 +- .../identity/SyncedIdentityStoreProvider.java | 4 +- .../ext/base/identity/UsernameStrategy.java | 85 ++ .../base/script/PredefinedScriptStore.java | 2 +- .../RunBackgroundScriptActionProvider.java | 36 + ...on.java => RunFileScriptMenuProvider.java} | 67 +- .../RunHubBatchScriptActionProvider.java | 45 ++ .../script/RunHubScriptActionProvider.java | 38 + .../script/RunScriptActionProviderMenu.java | 503 ++++++++++++ .../RunTerminalScriptActionProvider.java | 38 + .../base/script/ScriptGroupStoreProvider.java | 7 +- .../ext/base/script/ScriptStoreSetup.java | 10 +- .../script/SimpleScriptQuickEditAction.java | 59 -- .../SimpleScriptQuickEditActionProvider.java | 87 +++ .../script/SimpleScriptStoreProvider.java | 16 +- .../AbstractServiceGroupStoreProvider.java | 2 +- .../base/service/AbstractServiceStore.java | 6 +- .../service/AbstractServiceStoreProvider.java | 38 +- .../ext/base/service/CustomServiceStore.java | 2 +- .../service/CustomServiceStoreProvider.java | 6 +- .../ext/base/service/FixedServiceStore.java | 2 +- .../service/FixedServiceStoreProvider.java | 6 +- .../service/MappedServiceStoreProvider.java | 6 +- .../base/service/ServiceControlSession.java | 19 +- .../ext/base/service/ServiceControlStore.java | 4 +- .../service/ServiceControlStoreProvider.java | 11 +- .../service/ServiceCopyAddressAction.java | 67 -- .../ServiceCopyAddressActionProvider.java | 70 ++ .../base/service/ServiceRefreshAction.java | 97 --- .../service/ServiceRefreshActionProvider.java | 84 ++ .../ext/base/store/ShellStoreProvider.java | 42 +- .../ext/base/store/StorePauseAction.java | 81 -- .../base/store/StorePauseActionProvider.java | 77 ++ .../ext/base/store/StoreRestartAction.java | 93 --- .../store/StoreRestartActionProvider.java | 79 ++ .../ext/base/store/StoreStartAction.java | 81 -- .../base/store/StoreStartActionProvider.java | 82 ++ .../xpipe/ext/base/store/StoreStopAction.java | 81 -- .../base/store/StoreStopActionProvider.java | 82 ++ ext/base/src/main/java/module-info.java | 82 +- .../passwordManagerIdentity_icon-16-dark.png | Bin 0 -> 532 bytes .../img/passwordManagerIdentity_icon-16.png | Bin 0 -> 533 bytes .../passwordManagerIdentity_icon-24-dark.png | Bin 0 -> 801 bytes .../img/passwordManagerIdentity_icon-24.png | Bin 0 -> 788 bytes .../passwordManagerIdentity_icon-40-dark.png | Bin 0 -> 1295 bytes .../img/passwordManagerIdentity_icon-40.png | Bin 0 -> 1294 bytes .../passwordManagerIdentity_icon-80-dark.png | Bin 0 -> 2637 bytes .../img/passwordManagerIdentity_icon-80.png | Bin 0 -> 2629 bytes .../ext/system/incus/IncusCommandView.java | 9 +- .../incus/IncusContainerActionMenu.java | 55 -- .../IncusContainerActionProviderMenu.java | 50 ++ .../incus/IncusContainerConsoleAction.java | 60 -- .../IncusContainerConsoleActionProvider.java | 60 ++ .../incus/IncusContainerEditConfigAction.java | 60 -- ...ncusContainerEditConfigActionProvider.java | 65 ++ .../IncusContainerEditRunConfigAction.java | 72 -- ...sContainerEditRunConfigActionProvider.java | 77 ++ .../ext/system/incus/IncusContainerStore.java | 2 +- .../incus/IncusContainerStoreProvider.java | 9 +- .../incus/IncusInstallStoreProvider.java | 8 +- .../ext/system/lxd/LxdCmdStoreProvider.java | 8 +- .../xpipe/ext/system/lxd/LxdCommandView.java | 19 +- .../system/lxd/LxdContainerActionMenu.java | 55 -- .../lxd/LxdContainerActionProviderMenu.java | 50 ++ .../system/lxd/LxdContainerConsoleAction.java | 61 -- .../LxdContainerConsoleActionProvider.java | 61 ++ .../lxd/LxdContainerEditConfigAction.java | 61 -- .../LxdContainerEditConfigActionProvider.java | 66 ++ .../lxd/LxdContainerEditRunConfigAction.java | 72 -- ...dContainerEditRunConfigActionProvider.java | 77 ++ .../ext/system/lxd/LxdContainerStore.java | 2 +- .../system/lxd/LxdContainerStoreProvider.java | 9 +- .../ext/system/podman/PodmanCmdStore.java | 4 +- .../system/podman/PodmanCmdStoreProvider.java | 16 +- .../ext/system/podman/PodmanCommandView.java | 4 +- .../podman/PodmanContainerActionMenu.java | 53 -- .../PodmanContainerActionProviderMenu.java | 48 ++ .../podman/PodmanContainerAttachAction.java | 43 -- .../PodmanContainerAttachActionProvider.java | 53 ++ .../podman/PodmanContainerInspectAction.java | 55 -- .../PodmanContainerInspectActionProvider.java | 55 ++ .../podman/PodmanContainerLogsAction.java | 54 -- .../PodmanContainerLogsActionProvider.java | 54 ++ .../podman/PodmanContainerStoreProvider.java | 9 +- ext/system/src/main/java/module-info.java | 14 +- get-xpipe.ps1 | 4 +- gradle/gradle_scripts/extension.gradle | 4 +- gradle/gradle_scripts/javafx.gradle | 2 +- gradle/gradle_scripts/junit.gradle | 6 +- .../markdowngenerator-1.3.1.1.jar | Bin 37018 -> 0 bytes gradle/gradle_scripts/vernacular-1.16.jar | Bin 117594 -> 0 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- .../passwordManagerIdentity_icon-dark.svg | 64 ++ img/base/passwordManagerIdentity_icon.svg | 63 ++ img/proc/actionMacro_icon.svg | 57 ++ img/proc/actionShortcut_icon.svg | 57 ++ img/proc/appleContainers_icon.png | Bin 0 -> 43581 bytes img/proc/nushell_icon-dark.svg | 28 + img/proc/nushell_icon.svg | 27 + lang/strings/fixed_en.properties | 5 + lang/strings/translations_en.properties | 127 ++- version | 2 +- 706 files changed, 15763 insertions(+), 11343 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/action/AbstractAction.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionConfigComp.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionConfirmation.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionPickComp.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java create mode 100644 app/src/main/java/io/xpipe/app/action/ActionUrls.java create mode 100644 app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java create mode 100644 app/src/main/java/io/xpipe/app/action/SerializableAction.java create mode 100644 app/src/main/java/io/xpipe/app/action/StoreContextAction.java create mode 100644 app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java delete mode 100644 app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/BrowserActionProviders.java delete mode 100644 app/src/main/java/io/xpipe/app/browser/action/BrowserLeafAction.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java rename app/src/main/java/io/xpipe/app/browser/{action/BrowserApplicationPathAction.java => menu/BrowserApplicationPathMenuProvider.java} (81%) rename app/src/main/java/io/xpipe/app/browser/{action/BrowserBranchAction.java => menu/BrowserMenuBranchProvider.java} (59%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuCategory.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java => app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java (61%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java => app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java (91%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java (76%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java (66%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java (85%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/DownloadAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java (73%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java (76%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java (69%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java (76%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java (54%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/JavapMenuProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java (59%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java (74%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java (58%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalMenuProvider.java (72%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java (81%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java (75%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java (67%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java (80%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUntarAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java (52%) rename ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUnzipUnixAction.java => app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java (51%) create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarDirectoryMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzDirectoryMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzHereMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarHereMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryUnixMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryWindowsActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereUnixMenuProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereWindowsActionProvider.java create mode 100644 app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java create mode 100644 app/src/main/java/io/xpipe/app/comp/base/LoadingIconComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java rename app/src/main/java/io/xpipe/app/{resources => core}/AppImages.java (96%) create mode 100644 app/src/main/java/io/xpipe/app/core/AppMacros.java rename app/src/main/java/io/xpipe/app/{resources => core}/AppResources.java (91%) delete mode 100644 app/src/main/java/io/xpipe/app/ext/ActionProvider.java rename {core/src/main/java/io/xpipe/core/store => app/src/main/java/io/xpipe/app/ext}/NetworkTunnelSession.java (66%) rename {core/src/main/java/io/xpipe/core/store => app/src/main/java/io/xpipe/app/ext}/NetworkTunnelStore.java (91%) create mode 100644 app/src/main/java/io/xpipe/app/ext/PrefsValue.java create mode 100644 app/src/main/java/io/xpipe/app/ext/Session.java rename {core/src/main/java/io/xpipe/core/store => app/src/main/java/io/xpipe/app/ext}/SessionListener.java (73%) rename {core/src/main/java/io/xpipe/core/store => app/src/main/java/io/xpipe/app/ext}/SingletonSessionStore.java (82%) delete mode 100644 app/src/main/java/io/xpipe/app/ext/UserBasedValue.java create mode 100644 app/src/main/java/io/xpipe/app/ext/WrapperFileSystem.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/BatchHubProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/BatchStoreAction.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/HubBranchProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/HubLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/HubMenuItemProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/MultiStoreAction.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/StoreAction.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/StoreActionCategory.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/BrowseHubLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/CloneHubLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/EditHubLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/LaunchHubMenuLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/RefreshChildrenHubLeafProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/RefreshHubLeafProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java => app/src/main/java/io/xpipe/app/hub/action/impl/SampleHubLeafProvider.java (65%) create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/ScanHubBatchProvider.java create mode 100644 app/src/main/java/io/xpipe/app/hub/action/impl/ScanHubLeafProvider.java rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/DenseStoreEntryComp.java (82%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/OsLogoComp.java (97%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StandardStoreEntryComp.java (84%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreActiveComp.java (97%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCategoryComp.java (97%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCategoryConfigComp.java (91%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCategoryListComp.java (97%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCategoryWrapper.java (96%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreChoiceComp.java (98%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCreationComp.java (86%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCreationConsumer.java (81%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCreationDialog.java (97%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCreationMenu.java (69%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreCreationModel.java (89%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreEntryBatchSelectComp.java (98%) create mode 100644 app/src/main/java/io/xpipe/app/hub/comp/StoreEntryComp.java rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreEntryListComp.java (95%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreEntryListOverviewComp.java (68%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreEntryListStatusBarComp.java (77%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreEntryWrapper.java (65%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreIconChoiceComp.java (97%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreIconChoiceDialog.java (98%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreIconComp.java (98%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreIdentitiesIntroComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreIntroComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreLayoutComp.java (98%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreListChoiceComp.java (53%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreNotFoundComp.java (88%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreNotes.java (86%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreNotesComp.java (98%) create mode 100644 app/src/main/java/io/xpipe/app/hub/comp/StoreOrderIndexDialog.java rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreProviderChoiceComp.java (98%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreQuickAccessButtonComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreScriptsIntroComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreSection.java (85%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreSectionBaseComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreSectionComp.java (96%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreSectionMiniComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store/StoreSortMode.java => hub/comp/StoreSectionSortMode.java} (70%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreSidebarComp.java (98%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreToggleComp.java (99%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/StoreViewState.java (84%) rename app/src/main/java/io/xpipe/app/{comp/store => hub/comp}/SystemStateComp.java (94%) create mode 100644 app/src/main/java/io/xpipe/app/issue/ErrorEventFactory.java delete mode 100644 app/src/main/java/io/xpipe/app/password/PasswordManagerFixedCommand.java delete mode 100644 app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java create mode 100644 app/src/main/java/io/xpipe/app/prefs/PasswordManagerTestComp.java rename app/src/main/java/io/xpipe/app/{password => pwman}/BitwardenPasswordManager.java (72%) rename app/src/main/java/io/xpipe/app/{password => pwman}/DashlanePasswordManager.java (61%) rename app/src/main/java/io/xpipe/app/{password => pwman}/EnpassPasswordManager.java (66%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeePassXcAssociationKey.java (88%) rename app/src/main/java/io/xpipe/app/{password/KeePassXcManager.java => pwman/KeePassXcPasswordManager.java} (84%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeePassXcProxyClient.java (92%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeeperPasswordManager.java (63%) rename app/src/main/java/io/xpipe/app/{password => pwman}/LastpassPasswordManager.java (50%) rename app/src/main/java/io/xpipe/app/{password => pwman}/OnePasswordManager.java (54%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PasswordManager.java (63%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PasswordManagerCommand.java (86%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PasswordManagerCommandTemplate.java (98%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PsonoPasswordManager.java (63%) rename app/src/main/java/io/xpipe/app/{password => pwman}/TweetNaClHelper.java (98%) rename app/src/main/java/io/xpipe/app/{password => pwman}/WindowsCredentialManager.java (68%) create mode 100644 app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/DevolutionsRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/FreeRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/RdpLaunchConfig.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/RemminaRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/RemoteDesktopAppRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/WindowsAppRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/terminal/ITerm2TerminalType.java create mode 100644 app/src/main/java/io/xpipe/app/terminal/KonsoleTerminalType.java create mode 100644 app/src/main/java/io/xpipe/app/terminal/MacOsTerminalType.java create mode 100644 app/src/main/java/io/xpipe/app/util/Check.java delete mode 100644 app/src/main/java/io/xpipe/app/util/LocalShellCache.java create mode 100644 app/src/main/java/io/xpipe/app/util/RemminaHelper.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/CustomVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/ExternalVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/InternalVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/RealVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/RemminaVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/ScreenSharingVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/TigerVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/TightVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/VncBaseStore.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/VncCategory.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/VncLaunchConfig.java create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/logo/loading-dark.png create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/shortcut/actionShortcut_icon-16.png create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/shortcut/actionShortcut_icon-24.png create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/shortcut/actionShortcut_icon-40.png create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/shortcut/actionShortcut_icon-80.png delete mode 100644 core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/BusyElement.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/Choice.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/Dialog.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/DialogCancelException.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/DialogElement.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/DialogMapper.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/DialogReference.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/HeaderElement.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/QueryConverter.java delete mode 100644 core/src/main/java/io/xpipe/core/dialog/QueryElement.java delete mode 100644 core/src/main/java/io/xpipe/core/process/ShellCapabilities.java delete mode 100644 core/src/main/java/io/xpipe/core/store/Session.java delete mode 100644 core/src/main/java/io/xpipe/core/util/JacksonExtension.java create mode 100644 core/src/main/java/io/xpipe/core/util/KeyValue.java delete mode 100644 dist/changelogs/16.7.md delete mode 100644 dist/changelogs/16.7_incremental.md create mode 100644 dist/changelogs/17.0.md create mode 100644 dist/logo/ico/logo_1024x1024.png create mode 100644 dist/logo/ico/logo_512x512.png create mode 100644 dist/logo/logo.icon/Assets/logo.svg create mode 100644 dist/logo/logo.icon/Assets/shadow.svg create mode 100644 dist/logo/logo.icon/icon.json delete mode 100644 dist/logo/logo.iconset/icon_128x128.png delete mode 100644 dist/logo/logo.iconset/icon_128x128@2.png delete mode 100644 dist/logo/logo.iconset/icon_16x16.png delete mode 100644 dist/logo/logo.iconset/icon_16x16@2.png delete mode 100644 dist/logo/logo.iconset/icon_256x256.png delete mode 100644 dist/logo/logo.iconset/icon_256x256@2.png delete mode 100644 dist/logo/logo.iconset/icon_32x32.png delete mode 100644 dist/logo/logo.iconset/icon_32x32@2.png delete mode 100644 dist/logo/logo.iconset/icon_512x512.png delete mode 100644 dist/logo/logo.iconset/icon_512x512@2.png delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/ChangeStoreIconAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/EditScriptStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/RefreshChildrenStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/RunScriptActionMenu.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/BrowseInNativeManagerAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/ChgrpAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/ChmodAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/ChownAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteLinkAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/ExecuteApplicationAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/JavaAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/OpenNativeFileDetailsAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseCompressAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUnzipWindowsAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/DirectoryCompressAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/FileCompressAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UntarDirectoryAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UntarGzDirectoryAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UntarGzHereAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UntarHereAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UnzipDirectoryUnixAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UnzipDirectoryWindowsAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UnzipHereUnixAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/browser/compress/UnzipHereWindowsAction.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityConvertAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/identity/LocalIdentityConvertActionProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStore.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/identity/PasswordManagerIdentityStoreProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/identity/UsernameStrategy.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/RunBackgroundScriptActionProvider.java rename ext/base/src/main/java/io/xpipe/ext/base/script/{RunScriptAction.java => RunFileScriptMenuProvider.java} (65%) create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/RunHubBatchScriptActionProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/RunHubScriptActionProvider.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptActionProviderMenu.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/RunTerminalScriptActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptQuickEditAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptQuickEditActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyAddressAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyAddressActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/service/ServiceRefreshActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StoreRestartActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartActionProvider.java delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopActionProvider.java create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-16-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-16.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-24-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-24.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-40-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-40.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-80-dark.png create mode 100644 ext/base/src/main/resources/io/xpipe/ext/base/resources/img/passwordManagerIdentity_icon-80.png delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerActionMenu.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerActionProviderMenu.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerConsoleAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerConsoleActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerEditConfigAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerEditConfigActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerEditRunConfigAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/incus/IncusContainerEditRunConfigActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerActionMenu.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerActionProviderMenu.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerConsoleAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerConsoleActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerEditConfigAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerEditConfigActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerEditRunConfigAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/lxd/LxdContainerEditRunConfigActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerActionMenu.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerActionProviderMenu.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerAttachAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerAttachActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerInspectAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerInspectActionProvider.java delete mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerLogsAction.java create mode 100644 ext/system/src/main/java/io/xpipe/ext/system/podman/PodmanContainerLogsActionProvider.java delete mode 100644 gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar delete mode 100644 gradle/gradle_scripts/vernacular-1.16.jar create mode 100644 img/base/passwordManagerIdentity_icon-dark.svg create mode 100644 img/base/passwordManagerIdentity_icon.svg create mode 100644 img/proc/actionMacro_icon.svg create mode 100644 img/proc/actionShortcut_icon.svg create mode 100644 img/proc/appleContainers_icon.png create mode 100644 img/proc/nushell_icon-dark.svg create mode 100644 img/proc/nushell_icon.svg diff --git a/README.md b/README.md index 58a50a92d..7d5ff1f1d 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,12 @@ Note that this is a desktop application that should be run on your local desktop Installers are the easiest way to get started and come with an optional automatic update functionality: - [Windows .msi Installer (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-x86_64.msi) +- [Windows .msi Installer (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-installer-windows-arm64.msi) If you don't like installers, you can also use a portable version that is packaged as an archive: - [Windows .zip Portable (x86-64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-x86_64.zip) +- [Windows .zip Portable (ARM 64)](https://github.com/xpipe-io/xpipe/releases/latest/download/xpipe-portable-windows-arm64.zip) Alternatively, you can also use the following package managers: - [choco](https://community.chocolatey.org/packages/xpipe) to install it with `choco install xpipe`. diff --git a/app/build.gradle b/app/build.gradle index 9d9090cd6..26e7b4296 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ dependencies { api project(':beacon') compileOnly 'org.hamcrest:hamcrest:3.0' - compileOnly 'org.junit.jupiter:junit-jupiter-api:5.11.4' - compileOnly 'org.junit.jupiter:junit-jupiter-params:5.11.4' + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.12.2' + compileOnly 'org.junit.jupiter:junit-jupiter-params:5.12.2' api 'com.vladsch.flexmark:flexmark:0.64.8' api 'com.vladsch.flexmark:flexmark-util:0.64.8' @@ -49,21 +49,21 @@ dependencies { api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8' api("com.github.weisj:jsvg:1.7.1") - api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar") + api 'io.xpipe:vernacular:1.15' api 'org.bouncycastle:bcprov-jdk18on:1.80' api 'info.picocli:picocli:4.7.6' api 'org.apache.commons:commons-lang3:3.17.0' - api 'io.sentry:sentry:8.7.0' - api 'commons-io:commons-io:2.18.0' - api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.3" - api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.3" + api 'io.sentry:sentry:8.13.3' + api 'commons-io:commons-io:2.19.0' + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.19.1" + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.19.1" api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0" - api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.16' - api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.16' + api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17' + api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.17' api 'io.xpipe:modulefs:0.1.6' api 'net.synedra:validatorfx:0.4.2' api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar") diff --git a/app/src/main/java/io/xpipe/app/action/AbstractAction.java b/app/src/main/java/io/xpipe/app/action/AbstractAction.java new file mode 100644 index 000000000..a4db6c2d3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/AbstractAction.java @@ -0,0 +1,191 @@ +package io.xpipe.app.action; + +import io.xpipe.app.comp.base.ModalButton; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.core.AppCache; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.core.window.AppDialog; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.app.util.ThreadHelper; + +import lombok.experimental.SuperBuilder; + +import java.util.*; +import java.util.function.Consumer; + +@SuperBuilder +public abstract class AbstractAction { + + private static final Set active = new HashSet<>(); + private static boolean closed; + private static Consumer pick; + + private static final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry( + AppI18n.observable("cancelActionPicker"), new LabelGraphic.IconGraphic("mdal-cancel_presentation"), () -> { + cancelPick(); + }); + + public static synchronized void expectPick() { + if (pick != null) { + return; + } + + var show = !AppCache.getBoolean("pickIntroductionShown", false); + if (show) { + var modal = ModalOverlay.of("actionPickerTitle", AppDialog.dialogTextKey("actionPickerDescription")); + modal.addButton(ModalButton.ok()); + modal.showAndWait(); + AppCache.update("pickIntroductionShown", true); + } + + AppLayoutModel.get().getQueueEntries().add(queueEntry); + pick = action -> { + cancelPick(); + var modal = ModalOverlay.of("actionShortcuts", new ActionPickComp(action).prefWidth(600)); + modal.show(); + }; + } + + public static synchronized void cancelPick() { + AppLayoutModel.get().getQueueEntries().remove(queueEntry); + pick = null; + } + + public static void reset() { + closed = true; + for (int i = 10; i > 0; i--) { + synchronized (active) { + var count = active.size(); + if (count == 0) { + break; + } + } + + // Wait 10s max + ThreadHelper.sleep(1000); + } + + synchronized (active) { + for (AbstractAction abstractAction : active) { + TrackEvent.trace("Action has not quit after timeout: " + abstractAction.toString()); + } + } + } + + public void executeSync() { + if (closed) { + return; + } + + synchronized (AbstractAction.class) { + if (pick != null) { + TrackEvent.withTrace("Picked action").tags(toDisplayMap()).handle(); + pick.accept(this); + pick = null; + return; + } + } + + executeSyncImpl(); + } + + public void executeAsync() { + if (closed) { + return; + } + + synchronized (AbstractAction.class) { + if (pick != null) { + TrackEvent.withTrace("Picked action").tags(toDisplayMap()).handle(); + pick.accept(this); + pick = null; + return; + } + } + + ThreadHelper.runAsync(() -> { + executeSyncImpl(); + }); + } + + private void executeSyncImpl() { + if (!ActionConfirmation.confirmAction(this)) { + return; + } + + if (closed) { + return; + } + + synchronized (active) { + active.add(this); + } + + TrackEvent.withTrace("Starting action execution").tags(toDisplayMap()).handle(); + + try { + if (!beforeExecute()) { + return; + } + } catch (Throwable t) { + ErrorEventFactory.fromThrowable(t).handle(); + return; + } + + try { + executeImpl(); + } catch (Throwable t) { + ErrorEventFactory.fromThrowable(t).handle(); + } finally { + afterExecute(); + synchronized (active) { + active.remove(this); + } + + TrackEvent.withTrace("Finished action execution").tag("id", getId()).handle(); + } + } + + public String getId() { + return getProvider().getId(); + } + + public String getDisplayName() { + var id = getId(); + return id != null ? DataStoreFormatter.camelCaseToName(id) : "?"; + } + + public ActionProvider getProvider() { + var clazz = getClass(); + var enc = clazz.getEnclosingClass(); + if (enc == null) { + throw new IllegalStateException("No enclosing instance of " + clazz); + } + return ActionProvider.ALL.stream() + .filter(actionProvider -> actionProvider.getClass().equals(enc)) + .findFirst() + .orElseThrow(IllegalStateException::new); + } + + public String getShortcutName() { + return getDisplayName(); + } + + public abstract void executeImpl() throws Exception; + + protected boolean beforeExecute() throws Exception { + return true; + } + + public boolean isMutation() { + return false; + } + + protected void afterExecute() {} + + public abstract Map toDisplayMap(); +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java new file mode 100644 index 000000000..cd026ffe4 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionConfigComp.java @@ -0,0 +1,105 @@ +package io.xpipe.app.action; + +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.SimpleComp; +import io.xpipe.app.comp.base.*; +import io.xpipe.app.hub.action.BatchStoreAction; +import io.xpipe.app.hub.action.MultiStoreAction; +import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.app.hub.comp.StoreChoiceComp; +import io.xpipe.app.hub.comp.StoreListChoiceComp; +import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.*; +import io.xpipe.core.store.DataStore; + +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.scene.layout.Region; + +public class ActionConfigComp extends SimpleComp { + + private final Property action; + + public ActionConfigComp(Property action) { + this.action = action; + } + + @Override + protected Region createSimple() { + var options = new OptionsBuilder(); + options.nameAndDescription("actionStore") + .addComp(createChooser()) + .nameAndDescription("actionStores") + .addComp(createMultiChooser()); + options.nameAndDescription("actionConfiguration").addComp(createTextArea()); + return options.build(); + } + + @SuppressWarnings("unchecked") + private Comp createMultiChooser() { + var listProp = new SimpleListProperty>(FXCollections.observableArrayList()); + if (action.getValue() instanceof BatchStoreAction ba) { + listProp.setAll(((BatchStoreAction) ba).getRefs()); + } else if (action.getValue() instanceof MultiStoreAction ma) { + listProp.setAll(((MultiStoreAction) ma).getRefs()); + } else { + listProp.clear(); + } + + listProp.addListener((obs, o, n) -> { + if (action.getValue() instanceof BatchStoreAction ba) { + action.setValue(((BatchStoreAction) ba).withRefs(n)); + } else if (action.getValue() instanceof MultiStoreAction ma) { + action.setValue(((MultiStoreAction) ma).withRefs(n)); + } + }); + + var choice = new StoreListChoiceComp<>( + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory()); + choice.hide(listProp.emptyProperty()); + return choice; + } + + @SuppressWarnings("unchecked") + private Comp createChooser() { + var singleProp = new SimpleObjectProperty>(); + var s = action.getValue() instanceof StoreAction sa ? sa.getRef() : null; + singleProp.set((DataStoreEntryRef) s); + + singleProp.addListener((obs, o, n) -> { + if (action.getValue() instanceof StoreAction sa) { + action.setValue(sa.withRef(n.asNeeded())); + } + }); + + var choice = new StoreChoiceComp<>( + StoreChoiceComp.Mode.OTHER, + null, + singleProp, + DataStore.class, + ref -> true, + StoreViewState.get().getAllConnectionsCategory()); + choice.hide(singleProp.isNull()); + return choice; + } + + private Comp createTextArea() { + var config = new SimpleStringProperty(); + var s = action.getValue() instanceof SerializableAction sa ? sa.toConfigNode() : null; + config.set(s != null && s.size() > 0 ? s.toPrettyString() : null); + + config.addListener((obs, o, n) -> { + if (action.getValue() instanceof SerializableAction aa && n != null) { + var with = aa.withConfigString(n); + if (with.isPresent()) { + action.setValue(with.get()); + } + } + }); + + var area = new IntegratedTextAreaComp(config, false, "action", new SimpleStringProperty("json")); + 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 new file mode 100644 index 000000000..9a81dd508 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionConfirmComp.java @@ -0,0 +1,79 @@ +package io.xpipe.app.action; + +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.SimpleComp; +import io.xpipe.app.hub.action.BatchStoreAction; +import io.xpipe.app.hub.action.MultiStoreAction; +import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.app.hub.comp.StoreListChoiceComp; +import io.xpipe.app.hub.comp.StoreViewState; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.store.DataStore; + +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Region; + +import atlantafx.base.theme.Styles; + +import java.util.List; +import java.util.Map; + +public class ActionConfirmComp extends SimpleComp { + + private final AbstractAction action; + + public ActionConfirmComp(AbstractAction action) { + this.action = action; + } + + @Override + protected Region createSimple() { + var options = new OptionsBuilder(); + var plural = action instanceof BatchStoreAction || action instanceof MultiStoreAction; + options.nameAndDescription(plural ? "actionConnections" : "actionConnection") + .addComp(createList()); + options.nameAndDescription("actionConfiguration").addComp(createTable()); + return options.build(); + } + + @SuppressWarnings("unchecked") + private Comp createList() { + var listProp = new SimpleListProperty>(FXCollections.observableArrayList()); + if (action instanceof BatchStoreAction ba) { + listProp.setAll(((BatchStoreAction) ba).getRefs()); + } else if (action instanceof MultiStoreAction ma) { + listProp.setAll(((MultiStoreAction) ma).getRefs()); + } else if (action instanceof StoreAction sa) { + listProp.setAll(List.of(sa.getRef().asNeeded())); + } + + var choice = new StoreListChoiceComp<>( + listProp, DataStore.class, null, StoreViewState.get().getAllConnectionsCategory()); + choice.setEditable(false); + choice.hide(listProp.emptyProperty()); + return choice; + } + + private Comp createTable() { + var map = action.toDisplayMap(); + return Comp.of(() -> { + var grid = new GridPane(); + grid.setHgap(11); + grid.setVgap(2); + var row = 0; + for (Map.Entry e : map.entrySet()) { + var name = new Label(e.getKey()); + var value = new Label(e.getValue()); + value.getStyleClass().add(Styles.TEXT_BOLD); + grid.add(name, 0, row); + grid.add(value, 1, row); + row++; + } + return grid; + }); + } +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionConfirmation.java b/app/src/main/java/io/xpipe/app/action/ActionConfirmation.java new file mode 100644 index 000000000..1ed26079a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionConfirmation.java @@ -0,0 +1,42 @@ +package io.xpipe.app.action; + +import io.xpipe.app.comp.base.ModalButton; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; + +import javafx.beans.property.SimpleBooleanProperty; + +import java.util.List; + +public class ActionConfirmation { + + public static boolean confirmAction(AbstractAction action) { + if (!action.isMutation() || !confirmAllModifications(action)) { + return true; + } + + var ok = new SimpleBooleanProperty(false); + var modal = ModalOverlay.of("confirmAction", new ActionConfirmComp(action).prefWidth(550)); + modal.addButton(ModalButton.cancel()); + modal.addButton(ModalButton.ok(() -> ok.set(true))); + modal.showAndWait(); + return ok.get(); + } + + private static boolean confirmAllModifications(AbstractAction action) { + var context = getContext(action); + return context.stream().anyMatch(dataStoreEntry -> { + var config = DataStorage.get().getEffectiveCategoryConfig(dataStoreEntry); + return config.getConfirmAllModifications() != null && config.getConfirmAllModifications(); + }); + } + + private static List getContext(AbstractAction action) { + if (action instanceof StoreContextAction ca) { + return ca.getStoreEntryContext(); + } + + return List.of(); + } +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java b/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java new file mode 100644 index 000000000..ce9c4da68 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java @@ -0,0 +1,105 @@ +package io.xpipe.app.action; + +import io.xpipe.app.hub.action.BatchStoreAction; +import io.xpipe.app.hub.action.MultiStoreAction; +import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.JacksonMapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.ArrayList; + +public class ActionJacksonMapper { + + @SuppressWarnings("unchecked") + public static T parse(JsonNode tree) throws JsonProcessingException { + if (!tree.isObject()) { + return null; + } + + var id = tree.get("id"); + if (id == null || !id.isTextual()) { + return null; + } + + var provider = ActionProvider.ALL.stream() + .filter(actionProvider -> id.textValue().equals(actionProvider.getId())) + .findFirst(); + if (provider.isEmpty()) { + return null; + } + + var clazz = provider.get().getActionClass(); + if (clazz.isEmpty()) { + return null; + } + + var object = (ObjectNode) tree; + var ref = tree.get("ref"); + + if (ref != null && !ref.isArray() && StoreAction.class.isAssignableFrom(clazz.get())) { + var action = JacksonMapper.getDefault().treeToValue(tree, clazz.get()); + return (T) action; + } + + var makeBatch = ref != null && ref.isArray() && !MultiStoreAction.class.isAssignableFrom(clazz.get()); + if (makeBatch) { + if (ref.size() == 0) { + return null; + } + + var batchActions = new ArrayList>(); + object.remove("ref"); + for (JsonNode batchRef : ref) { + object.set("ref", batchRef); + var action = JacksonMapper.getDefault().treeToValue(object, clazz.get()); + batchActions.add((StoreAction) action); + } + return (T) BatchStoreAction.builder().actions(batchActions).build(); + } + + var makeMulti = ref != null && ref.isArray() && MultiStoreAction.class.isAssignableFrom(clazz.get()); + if (makeMulti) { + object.remove("ref"); + object.set("refs", ref); + var action = JacksonMapper.getDefault().treeToValue(object, clazz.get()); + return (T) action; + } + + return null; + } + + public static ObjectNode write(AbstractAction value) { + if (value instanceof BatchStoreAction b) { + var arrayNode = JsonNodeFactory.instance.arrayNode(); + b.getActions().stream() + .map(a -> { + var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(a); + return tree.get("ref"); + }) + .forEach(n -> arrayNode.add(n)); + var tree = (ObjectNode) + JacksonMapper.getDefault().valueToTree(b.getActions().getFirst()); + tree.set("ref", arrayNode); + tree.put("id", b.getActions().getFirst().getId()); + return tree; + } + + var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(value); + + if (value instanceof MultiStoreAction m) { + var refs = tree.get("refs"); + tree.remove("refs"); + tree.set("ref", refs); + tree.put("id", m.getId()); + return tree; + } + + tree.put("id", value.getId()); + return tree; + } +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionPickComp.java b/app/src/main/java/io/xpipe/app/action/ActionPickComp.java new file mode 100644 index 000000000..405cb8ad8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionPickComp.java @@ -0,0 +1,27 @@ +package io.xpipe.app.action; + +import io.xpipe.app.comp.base.ModalOverlayContentComp; +import io.xpipe.app.util.OptionsBuilder; + +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.layout.Region; + +public class ActionPickComp extends ModalOverlayContentComp { + + private final AbstractAction action; + + public ActionPickComp(AbstractAction action) { + this.action = action; + } + + @Override + protected Region createSimple() { + var prop = new SimpleObjectProperty<>(action); + var top = new ActionConfigComp(prop); + var bottom = new ActionShortcutComp(prop, () -> { + getModalOverlay().close(); + }); + var options = new OptionsBuilder().addComp(top).addComp(bottom); + return options.build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionProvider.java b/app/src/main/java/io/xpipe/app/action/ActionProvider.java new file mode 100644 index 000000000..8cf030d1d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionProvider.java @@ -0,0 +1,57 @@ +package io.xpipe.app.action; + +import io.xpipe.app.ext.DataStoreProviders; +import io.xpipe.app.issue.ErrorEventFactory; +import io.xpipe.app.issue.TrackEvent; +import io.xpipe.core.util.ModuleLayerLoader; + +import java.util.*; + +public interface ActionProvider { + + List ALL = new ArrayList<>(); + + static void initProviders() { + TrackEvent.trace("Starting action provider initialization"); + for (ActionProvider actionProvider : ALL) { + try { + actionProvider.init(); + } catch (Throwable t) { + ErrorEventFactory.fromThrowable(t).handle(); + } + } + TrackEvent.trace("Finished action provider initialization"); + } + + default void init() throws Exception {} + + default String getLicensedFeatureId() { + return null; + } + + default String getId() { + return null; + } + + @SuppressWarnings("unchecked") + default Optional> getActionClass() { + var child = Arrays.stream(getClass().getDeclaredClasses()) + .filter(aClass -> aClass.getSimpleName().equals("Action")) + .findFirst() + .map(aClass -> (Class) aClass); + return child.isPresent() ? Optional.of(child.get()) : Optional.empty(); + } + + class Loader implements ModuleLayerLoader { + + @Override + public void init(ModuleLayer layer) { + ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream() + .map(p -> p.get()) + .toList()); + for (var p : DataStoreProviders.getAll()) { + ALL.addAll(p.getActionProviders()); + } + } + } +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java b/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java new file mode 100644 index 000000000..19ead9702 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionShortcutComp.java @@ -0,0 +1,88 @@ +package io.xpipe.app.action; + +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.SimpleComp; +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.comp.base.InputGroupComp; +import io.xpipe.app.comp.base.TextFieldComp; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.*; + +import javafx.beans.property.Property; +import javafx.beans.property.SimpleStringProperty; +import javafx.scene.layout.Region; + +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class ActionShortcutComp extends SimpleComp { + + private final Property action; + private final Runnable onCreateMacro; + + public ActionShortcutComp(Property action, Runnable onCreateMacro) { + this.action = action; + this.onCreateMacro = onCreateMacro; + } + + @Override + protected Region createSimple() { + var options = new OptionsBuilder(); + options.nameAndDescription("actionDesktopShortcut").addComp(createDesktopComp()); + options.nameAndDescription("actionUrlShortcut").addComp(createUrlComp()); + // options.nameAndDescription("actionMacro") + // .addComp(createMacroComp()); + return options.build(); + } + + private Comp createUrlComp() { + var url = new SimpleStringProperty(); + action.subscribe((v) -> { + var s = ActionUrls.toUrl(v); + PlatformThread.runLaterIfNeeded(() -> { + url.set(s); + }); + }); + + var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> { + ClipboardHelper.copyUrl(url.getValue()); + }) + .grow(false, true) + .tooltipKey("createShortcut"); + var field = new TextFieldComp(url); + field.grow(true, false); + field.apply(struc -> struc.get().setEditable(false)); + var group = new InputGroupComp(List.of(field, copyButton)); + return group; + } + + private Comp createDesktopComp() { + var url = BindingsHelper.map(action, abstractAction -> ActionUrls.toUrl(abstractAction)); + var name = new SimpleStringProperty(); + action.subscribe((v) -> { + var s = v.getShortcutName(); + PlatformThread.runLaterIfNeeded(() -> { + name.set(s); + }); + }); + var copyButton = new ButtonComp(null, new FontIcon("mdi2f-file-move-outline"), () -> { + ThreadHelper.runFailableAsync(() -> { + var file = DesktopShortcuts.createCliOpen(url.getValue(), name.getValue()); + DesktopHelper.browseFileInDirectory(file); + }); + }) + .grow(false, true) + .tooltipKey("createShortcut"); + var field = new TextFieldComp(name); + field.grow(true, false); + var group = new InputGroupComp(List.of(field, copyButton)); + return group; + } + + private Comp createMacroComp() { + var button = new ButtonComp( + AppI18n.observable("createMacro"), new FontIcon("mdi2c-clipboard-multiple-outline"), onCreateMacro); + return button; + } +} diff --git a/app/src/main/java/io/xpipe/app/action/ActionUrls.java b/app/src/main/java/io/xpipe/app/action/ActionUrls.java new file mode 100644 index 000000000..578546885 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/ActionUrls.java @@ -0,0 +1,146 @@ +package io.xpipe.app.action; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.core.util.InPlaceSecretValue; +import io.xpipe.core.util.JacksonMapper; +import io.xpipe.core.util.UuidHelper; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.SneakyThrows; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +public class ActionUrls { + + private static String encodeValue(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + + private static List nodeToString(JsonNode node) { + if (node.isTextual()) { + return List.of(encodeValue(node.asText())); + } + + if (node.isArray()) { + var list = new ArrayList(); + for (JsonNode c : node) { + var r = nodeToString(c); + if (r.size() == 1) { + list.add(r.getFirst()); + } + } + return list; + } + + var enc = InPlaceSecretValue.of(node.toPrettyString()).getEncryptedValue(); + return List.of(enc); + } + + @SneakyThrows + public static String toUrl(AbstractAction action) { + if (!(action instanceof SerializableAction sa)) { + return null; + } + + var json = sa.toNode(); + var parsed = + JacksonMapper.getDefault().treeToValue(json, new TypeReference>() {}); + + Map> requestParams = new LinkedHashMap<>(); + for (Map.Entry e : parsed.entrySet()) { + var value = nodeToString(e.getValue()); + requestParams.put(e.getKey(), value); + } + + String encodedURL = requestParams.keySet().stream() + .map(key -> { + var vals = requestParams.get(key); + return vals.stream().map(s -> key + "=" + s).collect(Collectors.joining("&")); + }) + .collect(Collectors.joining("&", "xpipe://action?", "")); + return encodedURL; + } + + public static Optional parse(String queryString) throws Exception { + var query = splitQuery(queryString); + + var id = query.get("id"); + if (id == null || id.size() != 1) { + return Optional.empty(); + } + + var provider = ActionProvider.ALL.stream() + .filter(actionProvider -> id.getFirst().equals(actionProvider.getId())) + .findFirst(); + if (provider.isEmpty()) { + return Optional.empty(); + } + + var clazz = provider.get().getActionClass(); + if (clazz.isEmpty()) { + return Optional.empty(); + } + + if (!SerializableAction.class.isAssignableFrom(clazz.get())) { + return Optional.empty(); + } + + var stores = query.get("ref"); + if (stores == null || stores.isEmpty()) { + return Optional.empty(); + } + + for (String store : stores) { + var uuid = UuidHelper.parse(store); + if (uuid.isEmpty()) { + throw new IllegalArgumentException("Invalid store id: " + store); + } + + var entry = DataStorage.get().getStoreEntryIfPresent(uuid.get()); + if (entry.isEmpty()) { + throw new IllegalArgumentException("Store not found for id: " + store); + } + + if (!entry.get().getValidity().isUsable()) { + throw new IllegalArgumentException( + "Store " + DataStorage.get().getStorePath(entry.get()) + " is incomplete"); + } + } + + var fixedMap = query.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey(), + entry -> entry.getValue().size() == 1 ? entry.getValue().getFirst() : entry.getValue())); + var json = (ObjectNode) JacksonMapper.getDefault().valueToTree(fixedMap); + var instance = ActionJacksonMapper.parse(json); + return Optional.ofNullable(instance); + } + + private static Map> splitQuery(String query) { + if (query == null || query.isBlank()) { + return Collections.emptyMap(); + } + + return Arrays.stream(query.split("&")) + .map(ActionUrls::splitQueryParameter) + .collect(Collectors.groupingBy( + AbstractMap.SimpleImmutableEntry::getKey, + LinkedHashMap::new, + Collectors.mapping(Map.Entry::getValue, Collectors.toList()))); + } + + private static AbstractMap.SimpleImmutableEntry splitQueryParameter(String it) { + final int idx = it.indexOf("="); + final String key = idx > 0 ? it.substring(0, idx) : it; + final String value = idx > 0 && it.length() > idx + 1 ? it.substring(idx + 1) : null; + return new AbstractMap.SimpleImmutableEntry<>( + URLDecoder.decode(key, StandardCharsets.UTF_8), + value != null ? URLDecoder.decode(value, StandardCharsets.UTF_8) : null); + } +} diff --git a/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java b/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java new file mode 100644 index 000000000..669829c57 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/LauncherUrlProvider.java @@ -0,0 +1,10 @@ +package io.xpipe.app.action; + +import java.net.URI; + +public interface LauncherUrlProvider extends ActionProvider { + + String getScheme(); + + AbstractAction createAction(URI uri) throws Exception; +} diff --git a/app/src/main/java/io/xpipe/app/action/SerializableAction.java b/app/src/main/java/io/xpipe/app/action/SerializableAction.java new file mode 100644 index 000000000..33a3166cd --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/SerializableAction.java @@ -0,0 +1,73 @@ +package io.xpipe.app.action; + +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.core.util.JacksonMapper; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.experimental.SuperBuilder; + +import java.util.*; + +@SuperBuilder +public abstract class SerializableAction extends AbstractAction { + + public String toString() { + return toNode().toPrettyString(); + } + + public ObjectNode toNode() { + var json = ActionJacksonMapper.write(this); + return json; + } + + public ObjectNode toConfigNode() { + var json = toNode(); + json.remove("ref"); + json.remove("refs"); + return json; + } + + public Optional withConfigString(String configString) { + try { + var tree = (ObjectNode) JacksonMapper.getDefault().readTree(configString); + tree.put("id", getId()); + SerializableAction action = ActionJacksonMapper.parse(tree); + return Optional.ofNullable(action); + } catch (Exception ex) { + return Optional.empty(); + } + } + + @Override + public Map toDisplayMap() { + var node = toConfigNode(); + + var map = new LinkedHashMap(); + map.put("Action", getDisplayName()); + for (Map.Entry property : node.properties()) { + if (property.getKey().equals("id")) { + continue; + } + + var name = DataStoreFormatter.camelCaseToName(property.getKey()); + var value = property.getValue().asText(); + if (!value.isEmpty()) { + map.put(name, value); + } else if (property.getValue().isArray()) { + var list = new ArrayList(); + for (JsonNode jsonNode : property.getValue()) { + var s = jsonNode.asText(); + if (!s.isEmpty()) { + list.add(s); + } + } + + if (!list.isEmpty()) { + map.put(name, String.join("\n", list)); + } + } + } + return map; + } +} diff --git a/app/src/main/java/io/xpipe/app/action/StoreContextAction.java b/app/src/main/java/io/xpipe/app/action/StoreContextAction.java new file mode 100644 index 000000000..d0c1750b2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/StoreContextAction.java @@ -0,0 +1,10 @@ +package io.xpipe.app.action; + +import io.xpipe.app.storage.DataStoreEntry; + +import java.util.List; + +public interface StoreContextAction { + + List getStoreEntryContext(); +} diff --git a/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java b/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java new file mode 100644 index 000000000..75f3f917f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/action/XPipeUrlProvider.java @@ -0,0 +1,23 @@ +package io.xpipe.app.action; + +import java.net.URI; + +public class XPipeUrlProvider implements LauncherUrlProvider { + + @Override + public String getScheme() { + return "xpipe"; + } + + @Override + public AbstractAction createAction(URI uri) throws Exception { + var a = uri.getHost(); + if (!"action".equals(a)) { + return null; + } + + var query = uri.getQuery(); + var action = ActionUrls.parse(query); + return action.orElse(null); + } +} 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 c12a70ce4..1ed7dbc45 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -1,6 +1,6 @@ package io.xpipe.app.beacon; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.util.DocumentationLink; import io.xpipe.beacon.BeaconConfig; @@ -75,7 +75,7 @@ public class AppBeaconServer { } catch (Exception ex) { // Not terminal! // We can still continue without the running server - ErrorEvent.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex) + ErrorEventFactory.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex) .build() .handle(); } @@ -135,7 +135,7 @@ public class AppBeaconServer { t.setDaemon(true); t.setName("http handler"); t.setUncaughtExceptionHandler((t1, e) -> { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); }); return t; }); 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 22561c0fd..8fc5a1ddc 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -1,7 +1,7 @@ package io.xpipe.app.beacon; import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; @@ -106,12 +106,12 @@ public class BeaconRequestHandler implements HttpHandler { response = beaconInterface.handle(exchange, object); } } catch (BeaconClientException clientException) { - ErrorEvent.fromThrowable(clientException).omit().expected().handle(); + ErrorEventFactory.fromThrowable(clientException).omit().expected().handle(); writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400); return; } catch (BeaconServerException serverException) { var cause = serverException.getCause() != null ? serverException.getCause() : serverException; - var event = ErrorEvent.fromThrowable(cause).omit().handle(); + var event = ErrorEventFactory.fromThrowable(cause).omit().handle(); var link = event.getLink(); writeError(exchange, new BeaconServerErrorResponse(cause, link), 500); return; @@ -119,9 +119,9 @@ public class BeaconRequestHandler implements HttpHandler { // Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection // is broken if (!ex.getClass().getName().contains("jackson")) { - ErrorEvent.fromThrowable(ex).omit().expected().handle(); + ErrorEventFactory.fromThrowable(ex).omit().expected().handle(); } else { - ErrorEvent.fromThrowable(ex).omit().expected().handle(); + ErrorEventFactory.fromThrowable(ex).omit().expected().handle(); // Make deserialization error message more readable var message = ex.getMessage() .replace("$RequestBuilder", "") @@ -133,7 +133,7 @@ public class BeaconRequestHandler implements HttpHandler { } return; } catch (Throwable other) { - var event = ErrorEvent.fromThrowable(other).omit().expected().handle(); + var event = ErrorEventFactory.fromThrowable(other).omit().expected().handle(); var link = event.getLink(); writeError(exchange, new BeaconServerErrorResponse(other, link), 500); return; @@ -159,10 +159,10 @@ public class BeaconRequestHandler implements HttpHandler { } catch (IOException ioException) { // The exchange implementation might have already sent a response manually if (!"headers already sent".equals(ioException.getMessage())) { - ErrorEvent.fromThrowable(ioException).omit().expected().handle(); + ErrorEventFactory.fromThrowable(ioException).omit().expected().handle(); } } catch (Throwable other) { - var event = ErrorEvent.fromThrowable(other).handle(); + var event = ErrorEventFactory.fromThrowable(other).handle(); var link = event.getLink(); writeError(exchange, new BeaconServerErrorResponse(other, link), 500); } @@ -177,7 +177,7 @@ public class BeaconRequestHandler implements HttpHandler { os.write(bytes); } } catch (IOException ex) { - ErrorEvent.fromThrowable(ex).omit().expected().handle(); + ErrorEventFactory.fromThrowable(ex).omit().expected().handle(); } } diff --git a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java index fc12a1b42..571ec7e8d 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java +++ b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java @@ -1,6 +1,6 @@ package io.xpipe.app.beacon; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.util.ShellTemp; import io.xpipe.beacon.BeaconClientException; @@ -36,7 +36,7 @@ public class BlobManager { } catch (IOException ignored) { } } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); } } 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 a99719526..4c35b398a 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,15 +1,15 @@ package io.xpipe.app.beacon.impl; -import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.terminal.TerminalView; import io.xpipe.app.util.*; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.AskpassExchange; +import io.xpipe.core.util.InPlaceSecretValue; + +import javafx.beans.property.SimpleStringProperty; import com.sun.net.httpserver.HttpExchange; -import io.xpipe.core.util.InPlaceSecretValue; -import javafx.beans.property.SimpleStringProperty; import java.time.Duration; @@ -25,15 +25,19 @@ public class AskpassExchangeImpl extends AskpassExchange { // SSH auth with a smartcard will prompt to confirm user presence // Maybe we can show some dialog for this in the future if (msg.getPrompt() != null && msg.getPrompt().toLowerCase().contains("confirm user presence")) { - var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry -> - msg.getPrompt().equals(queueEntry.getName().getValue())); + var shown = AppLayoutModel.get().getQueueEntries().stream().anyMatch(queueEntry -> msg.getPrompt() + .equals(queueEntry.getName().getValue())); if (!shown) { - var qe = new AppLayoutModel.QueueEntry(new SimpleStringProperty(msg.getPrompt()), new LabelGraphic.IconGraphic("mdi2f-fingerprint"), + var qe = new AppLayoutModel.QueueEntry( + new SimpleStringProperty(msg.getPrompt()), + new LabelGraphic.IconGraphic("mdi2f-fingerprint"), () -> {}); AppLayoutModel.get().getQueueEntries().add(qe); - GlobalTimer.delay(() -> { - AppLayoutModel.get().getQueueEntries().remove(qe); - }, Duration.ofSeconds(10)); + GlobalTimer.delay( + () -> { + AppLayoutModel.get().getQueueEntries().remove(qe); + }, + Duration.ofSeconds(10)); } return Response.builder().value(InPlaceSecretValue.of("")).build(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java index b95c0e41d..0ab17b7b6 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java @@ -1,6 +1,6 @@ package io.xpipe.app.beacon.impl; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.beacon.BeaconClientException; @@ -42,10 +42,10 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange { } } catch (Throwable ex) { if (ex instanceof ValidationException) { - ErrorEvent.expected(ex); + ErrorEventFactory.expected(ex); } else if (ex instanceof StackOverflowError) { // Cycles in connection graphs can fail hard but are expected - ErrorEvent.expected(ex); + ErrorEventFactory.expected(ex); } throw ex; } finally { diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java index 6025ce0eb..f71ef2e57 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java @@ -1,9 +1,9 @@ package io.xpipe.app.beacon.impl; +import io.xpipe.app.ext.SingletonSessionStore; import io.xpipe.app.storage.DataStorage; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionToggleExchange; -import io.xpipe.core.store.SingletonSessionStore; import com.sun.net.httpserver.HttpExchange; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java index 2bfb1a61c..92b7b9dd4 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/FsScriptExchangeImpl.java @@ -20,7 +20,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange { try (var in = BlobManager.get().getBlob(msg.getBlob())) { data = new String(in.readAllBytes(), StandardCharsets.UTF_8); } - data = shell.getControl().getShellDialect().prepareScriptContent(data); + data = shell.getControl().getShellDialect().prepareScriptContent(shell.getControl(), data); var file = ScriptHelper.createExecScript(shell.getControl(), data); return Response.builder().path(file).build(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java index 2765ed077..0c9f44734 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java @@ -1,7 +1,6 @@ package io.xpipe.app.beacon.impl; import io.xpipe.app.terminal.TerminalLauncherManager; -import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconServerException; import io.xpipe.beacon.api.TerminalWaitExchange; @@ -10,7 +9,7 @@ import com.sun.net.httpserver.HttpExchange; public class TerminalWaitExchangeImpl extends TerminalWaitExchange { @Override - public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException { + public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException { TerminalLauncherManager.waitExchange(msg.getRequest()); return Response.builder().build(); } 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 d3434cb8d..3ca5f8596 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionComp.java @@ -7,11 +7,11 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabComp; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.base.*; -import io.xpipe.app.comp.store.StoreEntryWrapper; -import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.hub.comp.StoreEntryWrapper; +import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BindingsHelper; import io.xpipe.app.util.FileReference; @@ -43,7 +43,10 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp { } public static void openSingleFile( - Supplier> store, Supplier initialPath, Consumer file, boolean save) { + Supplier> store, + Supplier initialPath, + Consumer file, + boolean save) { var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE); model.setOnFinish(fileStores -> { file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null); 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 ffc417591..e25145bdd 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionComp.java @@ -6,11 +6,12 @@ import io.xpipe.app.browser.file.BrowserTransferComp; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.comp.base.*; -import io.xpipe.app.comp.store.StoreEntryWrapper; -import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.ext.ShellStore; +import io.xpipe.app.hub.comp.StoreEntryWrapper; +import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.util.BindingsHelper; import io.xpipe.app.util.PlatformThread; import io.xpipe.app.util.ThreadHelper; @@ -61,9 +62,9 @@ public class BrowserFullSessionComp extends SimpleComp { .bind(Bindings.createObjectBinding( () -> new Insets(tabs.getHeaderHeight().get(), 0, 0, 0), tabs.getHeaderHeight())); }); - var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy()) + var loadingIndicator = new LoadingIconComp(model.getBusy(), AppFontSizes::xxxl) .apply(struc -> { - AnchorPane.setTopAnchor(struc.get(), 3.0); + AnchorPane.setTopAnchor(struc.get(), 0.0); AnchorPane.setRightAnchor(struc.get(), 0.0); }) .styleClass("tab-loading-indicator"); @@ -117,21 +118,21 @@ public class BrowserFullSessionComp extends SimpleComp { return true; } - return storeEntryWrapper.getEntry().getProvider().browserAction(model, storeEntryWrapper.getEntry(), null) + return storeEntryWrapper.getEntry().getProvider().launchBrowser(model, storeEntryWrapper.getEntry(), null) != null; }; BiConsumer action = (w, busy) -> { - ThreadHelper.runFailableAsync(() -> { - var entry = w.getEntry(); - if (!entry.getValidity().isUsable()) { - return; - } + var entry = w.getEntry(); + if (!entry.getValidity().isUsable()) { + return; + } - var a = entry.getProvider().browserAction(model, entry, busy); - if (a != null) { - a.execute(); - } - }); + var a = entry.getProvider().launchBrowser(model, entry, busy); + if (a != null) { + ThreadHelper.runFailableAsync(() -> { + a.run(); + }); + } }; var category = new SimpleObjectProperty<>( @@ -160,7 +161,7 @@ public class BrowserFullSessionComp extends SimpleComp { }) .vgrow(); var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()) - .hide(PlatformThread.sync(Bindings.createBooleanBinding( + .hide(Bindings.createBooleanBinding( () -> { if (model.getSessionEntries().size() == 0) { return true; @@ -169,7 +170,7 @@ public class BrowserFullSessionComp extends SimpleComp { return false; }, model.getSessionEntries(), - model.getSelectedEntry()))); + model.getSelectedEntry())); localDownloadStage.prefHeight(200); localDownloadStage.maxHeight(200); var vertical = @@ -212,6 +213,11 @@ public class BrowserFullSessionComp extends SimpleComp { struc.get().setMaxWidth(newValue.doubleValue()); }); + var clip = new Rectangle(); + clip.widthProperty().bind(struc.get().widthProperty()); + clip.heightProperty().bind(struc.get().heightProperty()); + struc.get().setClip(clip); + AnchorPane.setBottomAnchor(struc.get(), 0.0); AnchorPane.setRightAnchor(struc.get(), 0.0); tabs.getHeaderHeight().subscribe(number -> { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java index 2cb8fe6d9..604943b76 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java @@ -232,7 +232,12 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { - node.setClip(null); node.setPickOnBounds(false); var r = (Region) node; @@ -417,28 +418,24 @@ public class BrowserSessionTabsComp extends SimpleComp { }); if (tabModel.getIcon() != null) { - var ring = new RingProgressIndicator(0, false); - ring.setMinSize(16, 16); - ring.setPrefSize(16, 16); - ring.setMaxSize(16, 16); - ring.progressProperty() - .bind(Bindings.createDoubleBinding( - () -> tabModel.getBusy().get() - && !AppPrefs.get().performanceMode().get() - ? -1d - : 0, - PlatformThread.sync(tabModel.getBusy()), - AppPrefs.get().performanceMode())); + var loading = new LoadingIconComp(tabModel.getBusy(), AppFontSizes::base); + loading.prefWidth(16); + loading.prefHeight(16); var image = tabModel.getIcon(); - var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16).createRegion(); + var logo = PrettyImageHelper.ofFixedSizeSquare(image, 16); + logo.apply(struc -> { + struc.get() + .opacityProperty() + .bind(PlatformThread.sync(Bindings.createDoubleBinding( + () -> { + return !tabModel.getBusy().get() ? 1.0 : 0.15; + }, + tabModel.getBusy()))); + }); - tab.graphicProperty() - .bind(Bindings.createObjectBinding( - () -> { - return tabModel.getBusy().get() ? ring : logo; - }, - PlatformThread.sync(tabModel.getBusy()))); + var stack = new StackComp(List.of(logo, loading)); + tab.setGraphic(stack.createRegion()); } if (tabModel.getBrowserModel() instanceof BrowserFullSessionModel sessionModel) { @@ -460,13 +457,15 @@ public class BrowserSessionTabsComp extends SimpleComp { Comp comp = tabModel.comp(); var compRegion = comp.createRegion(); + var empty = new StackPane(); - empty.setMinWidth(450); + empty.setMinWidth(0); empty.widthProperty().addListener((observable, oldValue, newValue) -> { if (tabModel.isCloseable() && tabs.getSelectionModel().getSelectedItem() == tab) { rightPadding.setValue(newValue.doubleValue()); } }); + var split = new SplitPane(compRegion); if (tabModel.isCloseable()) { split.getItems().add(empty); @@ -496,6 +495,7 @@ public class BrowserSessionTabsComp extends SimpleComp { if (newValue != null) { Platform.runLater(() -> { Label l = (Label) tabs.lookup("#" + id + " .tab-label"); + l.setGraphicTextGap(7); var w = l.maxWidthProperty(); l.minWidthProperty().bind(w); l.prefWidthProperty().bind(w); @@ -512,10 +512,11 @@ public class BrowserSessionTabsComp extends SimpleComp { if (color != null) { c.getStyleClass().add(color.getId()); } - c.addEventHandler( - DragEvent.DRAG_ENTERED, - mouseEvent -> Platform.runLater( - () -> tabs.getSelectionModel().select(tab))); + c.addEventHandler(DragEvent.DRAG_ENTERED, mouseEvent -> { + if (tabModel.isCloseable()) { + Platform.runLater(() -> tabs.getSelectionModel().select(tab)); + } + }); }); } }); diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java index 4ab2c2ef0..1643083c1 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -1,118 +1,112 @@ package io.xpipe.app.browser.action; +import io.xpipe.app.browser.BrowserFullSessionModel; +import io.xpipe.app.browser.BrowserStoreSessionTab; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.core.util.ModuleLayerLoader; +import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.core.store.FilePath; +import io.xpipe.core.store.FileSystemStore; -import javafx.beans.value.ObservableValue; -import javafx.scene.Node; -import javafx.scene.control.MenuItem; -import javafx.scene.input.KeyCombination; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.experimental.SuperBuilder; -import java.util.ArrayList; import java.util.List; -import java.util.ServiceLoader; -public interface BrowserAction { +@SuperBuilder +public abstract class BrowserAction extends StoreAction { - List ALL = new ArrayList<>(); + protected final List files; - static List getFlattened(BrowserFileSystemTabModel model, List entries) { - return ALL.stream() - .map(browserAction -> getFlattened(browserAction, model, entries)) - .flatMap(List::stream) + @JsonIgnore + protected BrowserFileSystemTabModel model; + + @JsonIgnore + private List entries; + + @Override + protected boolean beforeExecute() throws Exception { + AppLayoutModel.get().selectBrowser(); + + if (model == null) { + var found = BrowserFullSessionModel.DEFAULT.getAllTabs().stream() + .filter(t -> t instanceof BrowserStoreSessionTab bs + && bs.getEntry().equals(ref)) + .findFirst(); + if (found.isPresent()) { + model = (BrowserFileSystemTabModel) found.get(); + } else { + model = BrowserFullSessionModel.DEFAULT.openFileSystemSync( + ref.asNeeded(), + model -> { + var isFile = model.getFileSystem().fileExists(files.getFirst()); + if (isFile) { + return files.getFirst().getParent(); + } else { + var dir = files.getFirst().getParent(); + if (!model.getFileSystem().directoryExists(dir)) { + throw new IllegalArgumentException("Directory does not exist: " + dir); + } + return dir; + } + }, + null, + true); + } + } + + model.getBusy().set(true); + + // Start shell in case we exited + model.getFileSystem().getShell().orElseThrow().start(); + + return true; + } + + @Override + protected void afterExecute() { + model.getBusy().set(false); + } + + protected List getEntries() { + if (entries != null) { + return entries; + } + + entries = files.stream() + .map(filePath -> { + var be = model.getFileList().getAll().getValue().stream() + .filter(browserEntry -> + browserEntry.getRawFileEntry().getPath().equals(filePath)) + .findFirst(); + if (be.isPresent()) { + return be.get(); + } + + return null; + }) + .filter(browserEntry -> browserEntry != null) .toList(); + return entries; } - static List getFlattened( - BrowserAction browserAction, BrowserFileSystemTabModel model, List entries) { - return browserAction instanceof BrowserLeafAction - ? List.of((BrowserLeafAction) browserAction) - : ((BrowserBranchAction) browserAction) - .getBranchingActions(model, entries).stream() - .map(action -> getFlattened(action, model, entries)) - .flatMap(List::stream) - .toList(); - } + public abstract static class BrowserActionBuilder> + extends StoreActionBuilder { - static BrowserLeafAction byId(String id, BrowserFileSystemTabModel model, List entries) { - return getFlattened(model, entries).stream() - .filter(browserAction -> id.equals(browserAction.getId())) - .findAny() - .orElseThrow(); - } - - default List resolveFilesIfNeeded(List selected) { - return automaticallyResolveLinks() - ? selected.stream() - .map(browserEntry -> - new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel())) - .toList() - : selected; - } - - MenuItem toMenuItem(BrowserFileSystemTabModel model, List selected); - - default void init(BrowserFileSystemTabModel model) throws Exception {} - - default String getProFeatureId() { - return null; - } - - default Node getIcon(BrowserFileSystemTabModel model, List entries) { - return null; - } - - default Category getCategory() { - return null; - } - - default KeyCombination getShortcut() { - return null; - } - - default boolean acceptsEmptySelection() { - return false; - } - - ObservableValue getName(BrowserFileSystemTabModel model, List entries); - - default boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return true; - } - - default boolean automaticallyResolveLinks() { - return true; - } - - default boolean isActive(BrowserFileSystemTabModel model, List entries) { - return true; - } - - enum Category { - CUSTOM, - OPEN, - NATIVE, - COPY_PASTE, - MUTATION - } - - class Loader implements ModuleLayerLoader { - - @Override - public void init(ModuleLayer layer) { - ALL.addAll(ServiceLoader.load(layer, BrowserAction.class).stream() - .map(actionProviderProvider -> actionProviderProvider.get()) - .filter(provider -> { - try { - return true; - } catch (Throwable e) { - ErrorEvent.fromThrowable(e).handle(); - return false; - } - }) + public void initEntries(BrowserFileSystemTabModel model, List entries) { + ref(model.getEntry().asNeeded()); + model(model); + files(entries.stream() + .map(browserEntry -> browserEntry.getRawFileEntry().getPath()) .toList()); + entries(entries); + } + + public void initFiles(BrowserFileSystemTabModel model, List entries) { + ref(model.getEntry().asNeeded()); + model(model); + files(entries); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java deleted file mode 100644 index fd7b02903..000000000 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.app.browser.action; - -import io.xpipe.app.browser.file.BrowserEntry; - -import java.util.List; - -public class BrowserActionFormatter { - - public static String filesArgument(List entries) { - return entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")"; - } - - public static String centerEllipsis(String input, int length) { - if (input == null) { - return ""; - } - - if (input.length() <= length) { - return input; - } - - var half = (length / 2) - 5; - return input.substring(0, half) + " ... " + input.substring(input.length() - half); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java new file mode 100644 index 000000000..0a3b5e732 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProvider.java @@ -0,0 +1,18 @@ +package io.xpipe.app.browser.action; + +import io.xpipe.app.action.ActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; + +import java.util.List; + +public interface BrowserActionProvider extends ActionProvider { + + default boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return true; + } + + default boolean isActive(BrowserFileSystemTabModel model, List entries) { + return true; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProviders.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProviders.java new file mode 100644 index 000000000..ab055ec22 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionProviders.java @@ -0,0 +1,13 @@ +package io.xpipe.app.browser.action; + +import io.xpipe.app.action.ActionProvider; + +public class BrowserActionProviders { + + public static BrowserActionProvider forClass(Class clazz) { + return (BrowserActionProvider) ActionProvider.ALL.stream() + .filter(actionProvider -> actionProvider.getClass().equals(clazz)) + .findFirst() + .orElseThrow(); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserLeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserLeafAction.java deleted file mode 100644 index 0c7a91348..000000000 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserLeafAction.java +++ /dev/null @@ -1,114 +0,0 @@ -package io.xpipe.app.browser.action; - -import io.xpipe.app.browser.file.BrowserEntry; -import io.xpipe.app.browser.file.BrowserFileSystemTabModel; -import io.xpipe.app.comp.base.TooltipHelper; -import io.xpipe.app.util.BindingsHelper; -import io.xpipe.app.util.BooleanScope; -import io.xpipe.app.util.LicenseProvider; -import io.xpipe.app.util.ThreadHelper; - -import javafx.scene.control.Button; -import javafx.scene.control.MenuItem; -import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.Region; - -import org.kordamp.ikonli.javafx.FontIcon; - -import java.util.List; - -public interface BrowserLeafAction extends BrowserAction { - - void execute(BrowserFileSystemTabModel model, List entries) throws Exception; - - default Button toButton(Region root, BrowserFileSystemTabModel model, List selected) { - var b = new Button(); - b.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(model.getBusy(), () -> { - if (model.getFileSystem() == null) { - return; - } - - // Start shell in case we exited - model.getFileSystem().getShell().orElseThrow().start(); - execute(model, selected); - }); - }); - event.consume(); - }); - var name = getName(model, selected); - Tooltip.install(b, TooltipHelper.create(name, getShortcut())); - var graphic = getIcon(model, selected); - if (graphic != null) { - b.setGraphic(graphic); - } - b.setMnemonicParsing(false); - b.accessibleTextProperty().bind(name); - root.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (getShortcut() != null && getShortcut().match(event)) { - b.fire(); - event.consume(); - } - }); - - b.setDisable(!isActive(model, selected)); - model.getCurrentPath().addListener((observable, oldValue, newValue) -> { - b.setDisable(!isActive(model, selected)); - }); - - if (getProFeatureId() != null - && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { - b.setDisable(true); - b.setGraphic(new FontIcon("mdi2p-professional-hexagon")); - } - - return b; - } - - default MenuItem toMenuItem(BrowserFileSystemTabModel model, List selected) { - var name = getName(model, selected); - var mi = new MenuItem(); - mi.textProperty().bind(BindingsHelper.map(name, s -> { - if (getProFeatureId() != null) { - return LicenseProvider.get().getFeature(getProFeatureId()).suffix(s); - } - return s; - })); - mi.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(model.getBusy(), () -> { - if (model.getFileSystem() == null) { - return; - } - - // Start shell in case we exited - model.getFileSystem().getShell().orElseThrow().start(); - execute(model, selected); - }); - }); - event.consume(); - }); - if (getShortcut() != null) { - mi.setAccelerator(getShortcut()); - } - var graphic = getIcon(model, selected); - if (graphic != null) { - mi.setGraphic(graphic); - } - mi.setMnemonicParsing(false); - mi.setDisable(!isActive(model, selected)); - - if (getProFeatureId() != null - && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { - mi.setDisable(true); - } - - return mi; - } - - default String getId() { - return null; - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java new file mode 100644 index 000000000..aa7d5f07a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java @@ -0,0 +1,55 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.action.AbstractAction; +import io.xpipe.app.action.ActionProvider; +import io.xpipe.app.browser.file.BrowserFileOutput; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +public class ApplyFileEditActionProvider implements ActionProvider { + + @Override + public String getId() { + return "applyFileEdit"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends AbstractAction { + + @NonNull + String target; + + @NonNull + InputStream input; + + @NonNull + BrowserFileOutput output; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + try (var out = output.open()) { + input.transferTo(out); + } + } + + @Override + public Map toDisplayMap() { + var map = new LinkedHashMap(); + map.put("action", getDisplayName()); + map.put("target", target); + return map; + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java new file mode 100644 index 000000000..cb26969e8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java @@ -0,0 +1,49 @@ +package io.xpipe.app.browser.action.impl; + +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.util.DesktopHelper; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.ShellControl; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class BrowseInNativeManagerActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() throws Exception { + ShellControl sc = model.getFileSystem().getShell().orElseThrow(); + for (BrowserEntry entry : getEntries()) { + var e = entry.getRawFileEntry().getPath(); + var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e); + try (var local = LocalShell.getShell().start()) { + DesktopHelper.browsePathRemote( + local, localFile, entry.getRawFileEntry().getKind()); + } + } + } + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return model.getFileSystem() + .getShell() + .orElseThrow() + .getLocalSystemAccess() + .supportsFileSystemAccess(); + } + + @Override + public String getId() { + return "browseInNativeFileManager"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java new file mode 100644 index 000000000..fcd0cfeca --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java @@ -0,0 +1,61 @@ +package io.xpipe.app.browser.action.impl; + +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.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class ChgrpActionProvider implements BrowserActionProvider { + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var os = model.getFileSystem().getShell().orElseThrow().getOsType(); + return os != OsType.WINDOWS && os != OsType.MACOS; + } + + @Override + public String getId() { + return "chgrp"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + private final String group; + + private final boolean recursive; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + model.getFileSystem() + .getShell() + .orElseThrow() + .executeSimpleCommand(CommandBuilder.of() + .add("chgrp") + .addIf(recursive, "-R") + .addLiteral(group) + .addFiles(getEntries().stream() + .map(browserEntry -> browserEntry + .getRawFileEntry() + .getPath() + .toString()) + .toList())); + model.refreshEntriesSync(getEntries()); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java new file mode 100644 index 000000000..c3a85048d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java @@ -0,0 +1,60 @@ +package io.xpipe.app.browser.action.impl; + +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.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class ChmodActionProvider implements BrowserActionProvider { + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + } + + @Override + public String getId() { + return "chmod"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + private final String permissions; + + private final boolean recursive; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + model.getFileSystem() + .getShell() + .orElseThrow() + .executeSimpleCommand(CommandBuilder.of() + .add("chmod") + .addIf(recursive, "-R") + .addLiteral(permissions) + .addFiles(getEntries().stream() + .map(browserEntry -> browserEntry + .getRawFileEntry() + .getPath() + .toString()) + .toList())); + model.refreshEntriesSync(getEntries()); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java new file mode 100644 index 000000000..fec60a56d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java @@ -0,0 +1,61 @@ +package io.xpipe.app.browser.action.impl; + +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.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class ChownActionProvider implements BrowserActionProvider { + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var os = model.getFileSystem().getShell().orElseThrow().getOsType(); + return os != OsType.WINDOWS && os != OsType.MACOS; + } + + @Override + public String getId() { + return "chown"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + private final String owner; + + private final boolean recursive; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + model.getFileSystem() + .getShell() + .orElseThrow() + .executeSimpleCommand(CommandBuilder.of() + .add("chown") + .addIf(recursive, "-R") + .addLiteral(owner) + .addFiles(getEntries().stream() + .map(browserEntry -> browserEntry + .getRawFileEntry() + .getPath() + .toString()) + .toList())); + model.refreshSync(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java new file mode 100644 index 000000000..985a64eda --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java @@ -0,0 +1,42 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.core.store.FileKind; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class ComputeDirectorySizesActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() throws Exception { + var entries = getEntries(); + if (entries.size() == 1 && entries.getFirst().getRawFileEntry().equals(model.getCurrentDirectory())) { + entries = model.getFileList().getAll().getValue(); + } + + for (BrowserEntry be : entries) { + if (be.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) { + continue; + } + + var size = model.getFileSystem() + .getDirectorySize(be.getRawFileEntry().resolved().getPath()); + var fileEntry = be.getRawFileEntry(); + fileEntry.resolved().setSize("" + size); + model.getFileList().updateEntry(be.getRawFileEntry().getPath(), fileEntry); + } + } + } + + @Override + public String getId() { + return "computeDirectorySizes"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java new file mode 100644 index 000000000..14b69bba7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java @@ -0,0 +1,34 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.*; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class DeleteActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + var toDelete = + getEntries().stream().map(entry -> entry.getRawFileEntry()).toList(); + BrowserFileSystemHelper.delete(toDelete); + model.refreshSync(); + } + } + + @Override + public String getId() { + return "deleteFile"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java new file mode 100644 index 000000000..80b5be997 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java @@ -0,0 +1,36 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.core.store.FilePath; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class MoveFileActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + FilePath target; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + model.getFileSystem().move(getEntries().getFirst().getRawFileEntry().getPath(), target); + model.refreshSync(); + } + } + + @Override + public String getId() { + return "moveFile"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java new file mode 100644 index 000000000..1df184799 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java @@ -0,0 +1,44 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.core.store.FileKind; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class NewDirectoryActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + String name; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + for (BrowserEntry entry : getEntries()) { + if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) { + continue; + } + + var file = entry.getRawFileEntry().getPath().join(name); + model.getFileSystem().mkdirs(file); + } + model.refreshSync(); + } + } + + @Override + public String getId() { + return "newDirectory"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java new file mode 100644 index 000000000..22572b9b9 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java @@ -0,0 +1,44 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.core.store.FileKind; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class NewFileActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + String name; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + for (BrowserEntry entry : getEntries()) { + if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) { + continue; + } + + var file = entry.getRawFileEntry().getPath().join(name); + model.getFileSystem().touch(file); + } + model.refreshSync(); + } + } + + @Override + public String getId() { + return "newFile"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java new file mode 100644 index 000000000..17d3b1ae9 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java @@ -0,0 +1,48 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FilePath; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class NewLinkActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + String name; + + @NonNull + FilePath target; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + for (BrowserEntry entry : getEntries()) { + if (entry.getRawFileEntry().getKind() != FileKind.DIRECTORY) { + continue; + } + + var file = entry.getRawFileEntry().getPath().join(name); + model.getFileSystem().symbolicLink(file, target); + } + model.refreshSync(); + } + } + + @Override + public String getId() { + return "newLink"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java new file mode 100644 index 000000000..387d06313 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java @@ -0,0 +1,37 @@ +package io.xpipe.app.browser.action.impl; + +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.core.store.FileKind; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class OpenDirectoryActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() { + var first = getEntries().getFirst(); + model.cdSync(first.getRawFileEntry().getPath().toString()); + } + } + + @Override + public String getId() { + return "openDirectory"; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.size() == 1 + && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java new file mode 100644 index 000000000..8ef809211 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java @@ -0,0 +1,39 @@ +package io.xpipe.app.browser.action.impl; + +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.BrowserFileOpener; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.core.store.FileKind; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class OpenFileDefaultActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() { + for (var entry : getEntries()) { + BrowserFileOpener.openInDefaultApplication(model, entry.getRawFileEntry()); + } + } + } + + @Override + public String getId() { + return "openFileDefault"; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return model.getFileList().getEditing().getValue() == null + && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java new file mode 100644 index 000000000..8836f301f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java @@ -0,0 +1,97 @@ +package io.xpipe.app.browser.action.impl; + +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.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FileKind; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class OpenFileNativeDetailsActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() throws Exception { + ShellControl sc = model.getFileSystem().getShell().get(); + for (BrowserEntry entry : getEntries()) { + var e = entry.getRawFileEntry().getPath(); + var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e); + switch (OsType.getLocal()) { + case OsType.Windows windows -> { + var parent = localFile.getParent(); + // If we execute this on a drive root there will be no parent, so we have to check for that! + var content = parent != null + ? String.format( + "$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').ParseName('%s').InvokeVerb('Properties')", + parent, localFile.getFileName()) + : String.format( + "$shell = New-Object -ComObject Shell.Application; $shell.NameSpace('%s').Self.InvokeVerb('Properties')", + localFile); + + // The Windows shell invoke verb functionality behaves kinda weirdly and only shows the window + // as + // long as the parent process is running. + // So let's keep one process running + LocalShell.getLocalPowershell() + .command(content) + .notComplex() + .execute(); + } + case OsType.Linux linux -> { + var dbus = String.format( + """ + dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:"file://%s" string:"" + """, + localFile); + var success = sc.executeSimpleBooleanCommand(dbus); + if (success) { + return; + } + + sc.command(CommandBuilder.of() + .add("xdg-open") + .addFile( + entry.getRawFileEntry().getKind() == FileKind.DIRECTORY + ? e + : e.getParent())) + .execute(); + } + case OsType.MacOs macOs -> { + sc.osascriptCommand(String.format( + """ + set fileEntry to (POSIX file "%s") as text + tell application "Finder" + activate + open information window of alias fileEntry + end tell + """, + localFile)) + .execute(); + } + } + } + } + } + + @Override + public String getId() { + return "openFileNativeDetails"; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var sc = model.getFileSystem().getShell().orElseThrow(); + return sc.getLocalSystemAccess().supportsFileSystemAccess(); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java new file mode 100644 index 000000000..d195951b7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java @@ -0,0 +1,41 @@ +package io.xpipe.app.browser.action.impl; + +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.BrowserFileOpener; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileKind; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class OpenFileWithActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() { + for (var entry : getEntries()) { + BrowserFileOpener.openWithAnyApplication(model, entry.getRawFileEntry()); + } + } + } + + @Override + public String getId() { + return "openFileWith"; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return OsType.getLocal().equals(OsType.WINDOWS) + && entries.size() == 1 + && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java new file mode 100644 index 000000000..918785722 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java @@ -0,0 +1,56 @@ +package io.xpipe.app.browser.action.impl; + +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.prefs.AppPrefs; +import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FilePath; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.Collections; +import java.util.List; + +public class OpenTerminalActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @Override + public void executeImpl() throws Exception { + var entries = getEntries(); + var dirs = entries.size() > 0 + ? entries.stream() + .map(browserEntry -> browserEntry.getRawFileEntry().getPath()) + .toList() + : model.getCurrentDirectory() != null + ? List.of(model.getCurrentDirectory().getPath()) + : Collections.singletonList((FilePath) null); + for (var dir : dirs) { + var name = (dir != null ? dir + " - " : "") + model.getName().getValue(); + model.openTerminalSync( + name, dir, model.getFileSystem().getShell().orElseThrow(), dirs.size() == 1); + } + } + } + + @Override + public String getId() { + return "openTerminalInDirectory"; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + } + + @Override + public boolean isActive(BrowserFileSystemTabModel model, List entries) { + var t = AppPrefs.get().terminalType().getValue(); + return t != null; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java new file mode 100644 index 000000000..48468d736 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java @@ -0,0 +1,63 @@ +package io.xpipe.app.browser.action.impl; + +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.issue.ErrorEventFactory; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.ProcessOutputException; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.concurrent.atomic.AtomicReference; + +public class RunCommandInBackgroundActionProvider implements BrowserActionProvider { + + @Override + public String getId() { + return "runFileInBackground"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + String command; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + var cmd = CommandBuilder.of().addFile(command); + for (BrowserEntry entry : getEntries()) { + cmd.addFile(entry.getRawFileEntry().getPath()); + } + + AtomicReference out = new AtomicReference<>(); + AtomicReference err = new AtomicReference<>(); + long exitCode; + try (var command = model.getFileSystem() + .getShell() + .orElseThrow() + .command(cmd) + .withWorkingDirectory(model.getCurrentDirectory().getPath()) + .start()) { + var r = command.readStdoutAndStderr(); + out.set(r[0]); + err.set(r[1]); + exitCode = command.getExitCode(); + } + + // Only throw actual error output + if (exitCode != 0) { + throw ErrorEventFactory.expected(ProcessOutputException.of(exitCode, out.get(), err.get())); + } + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java new file mode 100644 index 000000000..0f23c6f23 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java @@ -0,0 +1,43 @@ +package io.xpipe.app.browser.action.impl; + +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.util.CommandDialog; +import io.xpipe.core.process.CommandBuilder; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class RunCommandInBrowserActionProvider implements BrowserActionProvider { + + @Override + public String getId() { + return "runCommandInBrowser"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + String command; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() { + var builder = CommandBuilder.of().addFile(command); + for (BrowserEntry entry : getEntries()) { + builder.addFile(entry.getRawFileEntry().getPath()); + } + + var cmd = model.getFileSystem().getShell().orElseThrow().command(builder); + CommandDialog.runAsyncAndShow(cmd); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java new file mode 100644 index 000000000..8129495a1 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java @@ -0,0 +1,50 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.core.process.CommandBuilder; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class RunCommandInTerminalActionProvider implements BrowserActionProvider { + + @Override + public String getId() { + return "runCommandInTerminal"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + String title; + + @NonNull + String command; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + var cmd = CommandBuilder.of().addFile(command); + for (BrowserEntry entry : getEntries()) { + cmd.addFile(entry.getRawFileEntry().getPath()); + } + + model.openTerminalSync( + title, + model.getCurrentDirectory() != null + ? model.getCurrentDirectory().getPath() + : null, + model.getFileSystem().getShell().orElseThrow().command(cmd), + true); + } + } +} 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 new file mode 100644 index 000000000..0b97ca9d3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java @@ -0,0 +1,69 @@ +package io.xpipe.app.browser.action.impl; + +import io.xpipe.app.action.AbstractAction; +import io.xpipe.app.action.ActionProvider; +import io.xpipe.app.action.StoreContextAction; +import io.xpipe.app.browser.file.BrowserFileTransferOperation; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.store.FileSystemStore; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class TransferFilesActionProvider implements ActionProvider { + + @Override + public String getId() { + return "transferFiles"; + } + + @Jacksonized + @SuperBuilder + public static class Action extends AbstractAction implements StoreContextAction { + + @NonNull + DataStoreEntryRef target; + + @NonNull + BrowserFileTransferOperation operation; + + boolean download; + + @Override + public boolean isMutation() { + return !download; + } + + @Override + public void executeImpl() throws Exception { + operation.execute(); + } + + @Override + public Map toDisplayMap() { + var map = new LinkedHashMap(); + map.put("action", getDisplayName()); + map.put( + "sources", + operation.getFiles().stream() + .map(fileEntry -> fileEntry.getName()) + .collect(Collectors.joining("\n"))); + map.put("target", DataStorage.get().getStoreEntryDisplayName(target.get())); + map.put("targetDirectory", operation.getTarget().getPath().toString()); + return map; + } + + @Override + public List getStoreEntryContext() { + return List.of(target.get()); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java index 6679e180c..e7846f735 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java @@ -1,61 +1,45 @@ package io.xpipe.app.browser.file; +import io.xpipe.app.comp.base.ModalButton; +import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.core.window.AppWindowHelper; -import io.xpipe.app.storage.DataStorage; import io.xpipe.core.store.FileEntry; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FilePath; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.Alert; -import javafx.scene.control.ButtonBar; -import javafx.scene.control.ButtonType; -import java.util.LinkedHashMap; import java.util.List; import java.util.stream.Collectors; public class BrowserAlerts { public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) { - var map = new LinkedHashMap(); - map.put(new ButtonType(AppI18n.get("cancel"), ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL); - if (multiple) { - map.put(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP); - map.put(new ButtonType(AppI18n.get("skipAll"), ButtonBar.ButtonData.OTHER), FileConflictChoice.SKIP_ALL); - } - map.put(new ButtonType(AppI18n.get("replace"), ButtonBar.ButtonData.OTHER), FileConflictChoice.REPLACE); - if (multiple) { - map.put( - new ButtonType(AppI18n.get("replaceAll"), ButtonBar.ButtonData.OTHER), - FileConflictChoice.REPLACE_ALL); - } - map.put(new ButtonType(AppI18n.get("rename"), ButtonBar.ButtonData.OTHER), FileConflictChoice.RENAME); - if (multiple) { - map.put( - new ButtonType(AppI18n.get("renameAll"), ButtonBar.ButtonData.OTHER), - FileConflictChoice.RENAME_ALL); - } + var choice = new SimpleObjectProperty(); + var key = multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent"; var w = multiple ? 700 : 400; - return AppWindowHelper.showBlockingAlert(alert -> { - alert.setTitle(AppI18n.get("fileConflictAlertTitle")); - alert.setHeaderText(AppI18n.get("fileConflictAlertHeader")); - alert.setAlertType(Alert.AlertType.CONFIRMATION); - alert.getButtonTypes().clear(); - alert.getDialogPane() - .setContent(AppWindowHelper.alertContentText( - AppI18n.get( - multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent", - file), - w - 50)); - alert.getDialogPane().setMinWidth(w); - alert.getDialogPane().setPrefWidth(w); - alert.getDialogPane().setMaxWidth(w); - map.sequencedKeySet() - .forEach(buttonType -> alert.getButtonTypes().add(buttonType)); - }) - .map(map::get) - .orElse(FileConflictChoice.CANCEL); + var modal = ModalOverlay.of( + "fileConflictAlertTitle", + AppDialog.dialogText(AppI18n.observable(key, file)).prefWidth(w)); + modal.addButton(new ModalButton("cancel", () -> choice.set(FileConflictChoice.CANCEL), true, false)); + if (multiple) { + modal.addButton(new ModalButton("skip", () -> choice.set(FileConflictChoice.SKIP), true, false)); + modal.addButton(new ModalButton("skipAll", () -> choice.set(FileConflictChoice.SKIP_ALL), true, false)); + } + modal.addButton(new ModalButton("replace", () -> choice.set(FileConflictChoice.REPLACE), true, false)); + if (multiple) { + modal.addButton( + new ModalButton("replaceAll", () -> choice.set(FileConflictChoice.REPLACE_ALL), true, false)); + } + modal.addButton(new ModalButton("rename", () -> choice.set(FileConflictChoice.RENAME), true, false)); + if (multiple) { + modal.addButton(new ModalButton("renameAll", () -> choice.set(FileConflictChoice.RENAME_ALL), true, false)); + } + modal.showAndWait(); + return choice.get() != null ? choice.get() : FileConflictChoice.CANCEL; } public static boolean showMoveAlert(List source, FileEntry target) { @@ -74,25 +58,6 @@ public class BrowserAlerts { .orElse(false); } - public static boolean showDeleteAlert(BrowserFileSystemTabModel model, List source) { - var config = - DataStorage.get().getEffectiveCategoryConfig(model.getEntry().get()); - if (!Boolean.TRUE.equals(config.getConfirmAllModifications()) - && source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) { - return true; - } - - return AppWindowHelper.showBlockingAlert(alert -> { - alert.setTitle(AppI18n.get("deleteAlertTitle")); - alert.setHeaderText(AppI18n.get("deleteAlertHeader", source.size())); - alert.getDialogPane() - .setContent(AppWindowHelper.alertContentText(getSelectedElementsString(source))); - alert.setAlertType(Alert.AlertType.CONFIRMATION); - }) - .map(b -> b.getButtonData().isDefaultButton()) - .orElse(false); - } - private static String getSelectedElementsString(List source) { var namesHeader = AppI18n.get("selectedElements"); var names = namesHeader + "\n" diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java index 16ed30682..07f2f4be4 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserBreadcrumbBar.java @@ -2,7 +2,7 @@ package io.xpipe.app.browser.file; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.util.PlatformThread; -import io.xpipe.core.store.FileNames; +import io.xpipe.core.store.FilePath; import javafx.scene.Node; import javafx.scene.control.Button; @@ -14,6 +14,7 @@ import javafx.util.Callback; import atlantafx.base.controls.Breadcrumbs; import java.util.ArrayList; +import java.util.List; public class BrowserBreadcrumbBar extends SimpleComp { @@ -26,7 +27,7 @@ public class BrowserBreadcrumbBar extends SimpleComp { @Override protected Region createSimple() { Callback, ButtonBase> crumbFactory = crumb -> { - var name = crumb.getValue().equals("/") ? "/" : FileNames.getFileName(crumb.getValue()); + var name = crumb.getValue().equals("/") ? "/" : FilePath.of(crumb.getValue()).getFileName(); var btn = new Button(name, null); btn.setMnemonicParsing(false); btn.setFocusTraversable(false); @@ -41,37 +42,39 @@ public class BrowserBreadcrumbBar extends SimpleComp { var breadcrumbs = new Breadcrumbs(); breadcrumbs.setMinWidth(0); - PlatformThread.sync(model.getCurrentPath()).subscribe(val -> { - if (val == null) { - breadcrumbs.setSelectedCrumb(null); - return; - } + model.getCurrentPath().subscribe(val -> { + PlatformThread.runLaterIfNeeded(() -> { + if (val == null) { + breadcrumbs.setSelectedCrumb(null); + return; + } - var sc = model.getFileSystem().getShell(); - if (sc.isEmpty()) { - breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null); - } else { - breadcrumbs.setDividerFactory(item -> { - if (item == null) { - return null; - } + var sc = model.getFileSystem().getShell(); + if (sc.isEmpty()) { + breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null); + } else { + breadcrumbs.setDividerFactory(item -> { + if (item == null) { + return null; + } - if (item.isFirst() && item.getValue().equals("/")) { - return new Label(""); - } + if (item.isFirst() && item.getValue().equals("/")) { + return new Label(""); + } - return new Label(sc.get().getOsType().getFileSystemSeparator()); - }); - } + return new Label(sc.get().getOsType().getFileSystemSeparator()); + }); + } - var elements = val.splitHierarchy(); - var modifiedElements = new ArrayList<>(elements); - if (val.toString().startsWith("/")) { - modifiedElements.addFirst("/"); - } - Breadcrumbs.BreadCrumbItem items = - Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new)); - breadcrumbs.setSelectedCrumb(items); + var elements = createBreadcumbHierarchy(val); + var modifiedElements = new ArrayList<>(elements); + if (val.toString().startsWith("/")) { + modifiedElements.addFirst("/"); + } + Breadcrumbs.BreadCrumbItem items = + Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new)); + breadcrumbs.setSelectedCrumb(items); + }); }); if (crumbFactory != null) { @@ -87,4 +90,20 @@ public class BrowserBreadcrumbBar extends SimpleComp { return breadcrumbs; } + + private List createBreadcumbHierarchy(FilePath filePath) { + var f = filePath.toString() + "/"; + var list = new ArrayList(); + int lastElementStart = 0; + for (int i = 0; i < f.length(); i++) { + if (f.charAt(i) == '\\' || f.charAt(i) == '/') { + if (i - lastElementStart > 0) { + list.add(f.substring(0, i)); + } + + lastElementStart = i + 1; + } + } + return list; + } } 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 8fb5b2f8b..4afee4563 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 @@ -1,11 +1,9 @@ package io.xpipe.app.browser.file; import io.xpipe.app.ext.ProcessControlProvider; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.util.GlobalClipboard; -import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileEntry; -import io.xpipe.core.util.FailableRunnable; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; @@ -16,7 +14,6 @@ import javafx.scene.input.Dragboard; import lombok.SneakyThrows; import lombok.Value; -import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.io.File; @@ -45,7 +42,10 @@ public class BrowserClipboard { List data = (List) clipboard.getData(DataFlavor.javaFileListFlavor); // Sometimes file data can contain invalid chars. Why? - var files = data.stream().filter(file -> file.toString().chars().noneMatch(value -> Character.isISOControl(value))).map(f -> f.toPath()).toList(); + var files = data.stream() + .filter(file -> file.toString().chars().noneMatch(value -> Character.isISOControl(value))) + .map(f -> f.toPath()) + .toList(); if (files.size() == 0) { return; } @@ -55,9 +55,10 @@ public class BrowserClipboard { entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file)); } - currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY)); + currentCopyClipboard.setValue( + new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY)); } catch (Exception e) { - ErrorEvent.fromThrowable(e).expected().omit().handle(); + ErrorEventFactory.fromThrowable(e).expected().omit().handle(); } } }); 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 d1d541861..d0b09206e 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 @@ -3,7 +3,7 @@ package io.xpipe.app.browser.file; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.comp.SimpleComp; -import io.xpipe.app.comp.store.*; +import io.xpipe.app.hub.comp.*; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.PlatformThread; @@ -73,7 +73,8 @@ public final class BrowserConnectionListComp extends SimpleComp { filter, category, StoreViewState.get().getEntriesListVisibilityObservable(), - StoreViewState.get().getEntriesListUpdateObservable()), + StoreViewState.get().getEntriesListUpdateObservable(), + new ReadOnlyBooleanWrapper(true)), augment, selectedAction -> { BooleanProperty busy = new SimpleBooleanProperty(false); 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 cb5222bea..f5b65673d 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 @@ -3,9 +3,9 @@ package io.xpipe.app.browser.file; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.comp.base.FilterComp; import io.xpipe.app.comp.base.HorizontalComp; -import io.xpipe.app.comp.store.StoreCategoryWrapper; -import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppFontSizes; +import io.xpipe.app.hub.comp.StoreCategoryWrapper; +import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.util.DataStoreCategoryChoiceComp; import javafx.beans.property.Property; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java index b11ea11c3..9241a809e 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java @@ -1,6 +1,8 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.action.ActionProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuItemProvider; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.util.InputHelper; @@ -44,8 +46,10 @@ public final class BrowserContextMenu extends ContextMenu { return; } - for (BrowserAction.Category cat : BrowserAction.Category.values()) { - var all = BrowserAction.ALL.stream() + for (var cat : BrowserMenuCategory.values()) { + var all = ActionProvider.ALL.stream() + .map(actionProvider -> actionProvider instanceof BrowserMenuItemProvider ba ? ba : null) + .filter(browserActionProvider -> browserActionProvider != null) .filter(browserAction -> browserAction.getCategory() == cat) .filter(browserAction -> { if (model.isClosed()) { @@ -72,13 +76,16 @@ public final class BrowserContextMenu extends ContextMenu { getItems().add(new SeparatorMenuItem()); } - for (BrowserAction a : all) { + for (var a : all) { if (model.isClosed()) { return; } var used = a.resolveFilesIfNeeded(selected); - getItems().add(a.toMenuItem(model, used)); + var item = a.toMenuItem(model, used); + if (item != null) { + getItems().add(item); + } } } } 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 400aaf766..d17ab0357 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 @@ -1,6 +1,6 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.menu.BrowserMenuProviders; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.util.*; @@ -29,6 +29,7 @@ import java.time.Instant; import java.time.ZoneId; import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import static io.xpipe.app.util.HumanReadableFormat.byteCount; import static javafx.scene.control.TableColumn.SortType.ASCENDING; @@ -76,7 +77,6 @@ public final class BrowserFileListComp extends SimpleComp { param.getValue().getRawFileEntry().resolved().getSize())); sizeCol.setCellFactory(col -> new FileSizeCell()); sizeCol.setResizable(false); - sizeCol.setPrefWidth(120); sizeCol.setReorderable(false); var mtimeCol = new TableColumn(); @@ -124,13 +124,14 @@ public final class BrowserFileListComp extends SimpleComp { }); table.setFixedCellSize(30.0); - prepareColumnVisibility(table, ownerCol, filenameCol); + prepareColumnVisibility(table, filenameCol, mtimeCol, modeCol, ownerCol, sizeCol); prepareTableScrollFix(table); prepareTableSelectionModel(table); prepareTableShortcuts(table); prepareTableEntries(table); prepareTableChanges(table, filenameCol, mtimeCol, modeCol, ownerCol); prepareTypedSelectionModel(table); + table.setMinWidth(0); return table; } @@ -148,8 +149,11 @@ public final class BrowserFileListComp extends SimpleComp { private void prepareColumnVisibility( TableView table, + TableColumn filenameCol, + TableColumn mtimeCol, + TableColumn modeCol, TableColumn ownerCol, - TableColumn filenameCol) { + TableColumn sizeCol) { var os = fileList.getFileSystemModel() .getFileSystem() .getShell() @@ -159,6 +163,15 @@ public final class BrowserFileListComp extends SimpleComp { if (os != OsType.WINDOWS && os != OsType.MACOS) { ownerCol.setVisible(newValue.doubleValue() > 1000); } + + var shell = fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow(); + if (!OsType.WINDOWS.equals(shell.getOsType()) && !OsType.MACOS.equals(shell.getOsType())) { + modeCol.setVisible(newValue.doubleValue() > 600); + } + + mtimeCol.setPrefWidth(newValue.doubleValue() == 0.0 || newValue.doubleValue() > 600 ? 150 : 100); + sizeCol.setPrefWidth(newValue.doubleValue() == 0.0 || newValue.doubleValue() > 600 ? 120 : 90); + var width = getFilenameWidth(table); filenameCol.setPrefWidth(width); }); @@ -171,7 +184,7 @@ public final class BrowserFileListComp extends SimpleComp { .mapToDouble(value -> value.getPrefWidth()) .sum() + 7; - return tableView.getWidth() - sum; + return Math.max(200, tableView.getWidth() - sum); } private String formatOwner(BrowserEntry param) { @@ -336,7 +349,7 @@ public final class BrowserFileListComp extends SimpleComp { } var selected = fileList.getSelection(); - var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream() + var action = BrowserMenuProviders.getFlattened(fileList.getFileSystemModel(), selected).stream() .filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected) && browserAction.isActive(fileList.getFileSystemModel(), selected)) .filter(browserAction -> browserAction.getShortcut() != null) @@ -345,9 +358,11 @@ public final class BrowserFileListComp extends SimpleComp { action.ifPresent(browserAction -> { // Prevent concurrent modification by creating copy on platform thread var selectionCopy = new ArrayList<>(selected); - ThreadHelper.runFailableAsync(() -> { + try { browserAction.execute(fileList.getFileSystemModel(), selectionCopy); - }); + } catch (Exception e) { + throw new RuntimeException(e); + } event.consume(); }); if (action.isPresent()) { @@ -479,8 +494,29 @@ public final class BrowserFileListComp extends SimpleComp { TableColumn modeCol, TableColumn ownerCol) { var lastDir = new SimpleObjectProperty(); - Runnable updateHandler = () -> { + BiConsumer, List> updateHandler = (o, n) -> { PlatformThread.runLaterIfNeeded(() -> { + // Optimization for single entry updates + if (o != null && n != null && o.size() == n.size()) { + var left = new HashSet<>(n); + o.forEach(left::remove); + if (left.size() == 1) { + var updatedEntry = left.iterator().next(); + var found = o.stream() + .filter(browserEntry -> browserEntry + .getRawFileEntry() + .getPath() + .equals(updatedEntry.getRawFileEntry().getPath())) + .findFirst(); + if (found.isPresent()) { + table.refresh(); + table.getItems().set(table.getItems().indexOf(found.get()), updatedEntry); + return; + } + } + } + + table.setDisable(true); var newItems = new ArrayList<>(fileList.getShown().getValue()); table.getItems().clear(); @@ -506,21 +542,17 @@ public final class BrowserFileListComp extends SimpleComp { ownerCol.setPrefWidth(0); } - if (fileList.getFileSystemModel().getFileSystem() != null) { - var shell = fileList.getFileSystemModel() - .getFileSystem() - .getShell() - .orElseThrow(); - if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) { - modeCol.setVisible(false); + var shell = + fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow(); + if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) { + modeCol.setVisible(false); + ownerCol.setVisible(false); + } else { + modeCol.setVisible(table.getWidth() > 600); + if (table.getWidth() > 1000) { + ownerCol.setVisible(hasOwner); + } else if (!hasOwner) { ownerCol.setVisible(false); - } else { - modeCol.setVisible(true); - if (table.getWidth() > 1000) { - ownerCol.setVisible(hasOwner); - } else if (!hasOwner) { - ownerCol.setVisible(false); - } } } @@ -541,17 +573,20 @@ public final class BrowserFileListComp extends SimpleComp { } } lastDir.setValue(currentDirectory); + table.setDisable(false); }); }; - updateHandler.run(); + updateHandler.accept(null, null); fileList.getShown().addListener((observable, oldValue, newValue) -> { // Delay to prevent internal tableview exceptions when sorting - Platform.runLater(updateHandler); + Platform.runLater(() -> { + updateHandler.accept(oldValue, newValue); + }); }); fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> { if (oldValue == null) { - updateHandler.run(); + updateHandler.accept(null, null); } }); } @@ -594,16 +629,15 @@ public final class BrowserFileListComp extends SimpleComp { if (empty || getTableRow() == null || getTableRow().getItem() == null) { setText(null); } else { - var path = getTableRow().getItem(); - if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { - setText(null); - } else if (fileSize != null) { + if (fileSize != null) { try { var l = Long.parseLong(fileSize); setText(byteCount(l)); } catch (NumberFormatException e) { setText(fileSize); } + } else { + setText(null); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java index cab84e76f..f888ffbc1 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListFilterComp.java @@ -6,9 +6,8 @@ import io.xpipe.app.comp.base.TextFieldComp; import io.xpipe.app.comp.base.TooltipHelper; import io.xpipe.app.core.AppI18n; import io.xpipe.app.util.InputHelper; - import io.xpipe.app.util.PlatformThread; -import javafx.application.Platform; + import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Pos; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java index 9160c743f..fd532dab1 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java @@ -1,10 +1,12 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.browser.action.impl.MoveFileActionProvider; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileEntry; import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FilePath; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; @@ -56,12 +58,31 @@ public final class BrowserFileListModel { } } + public void updateEntry(FilePath p, FileEntry n) { + var found = all.getValue().stream() + .filter(browserEntry -> browserEntry.getRawFileEntry().getPath().equals(p)) + .findFirst(); + if (found.isEmpty()) { + return; + } + + var index = all.getValue().indexOf(found.get()); + var l = new ArrayList<>(all.getValue()); + if (n != null) { + l.set(index, new BrowserEntry(n, this)); + } else { + l.remove(index); + } + all.setValue(l); + refreshShown(); + } + public void setComparator(Comparator comparator) { comparatorProperty.setValue(comparator); refreshShown(); } - private void refreshShown() { + void refreshShown() { List filtered = fileSystemModel.getFilter().getValue() != null ? all.getValue().stream() .filter(entry -> { @@ -90,7 +111,7 @@ public final class BrowserFileListModel { return us; } - public BrowserEntry rename(BrowserEntry old, String newName) { + public BrowserEntry rename(BrowserEntry old, String newName) throws Exception { if (old == null || newName == null || fileSystemModel == null @@ -99,7 +120,6 @@ public final class BrowserFileListModel { return old; } - var fullPath = fileSystemModel.getCurrentPath().get().join(old.getFileName()); var newFullPath = fileSystemModel.getCurrentPath().get().join(newName); // This check will fail on case-insensitive file systems when changing the case of the file @@ -113,22 +133,25 @@ public final class BrowserFileListModel { exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath); } catch (Exception e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); return old; } if (exists) { - ErrorEvent.fromMessage("Target " + newFullPath + " does already exist") + ErrorEventFactory.fromMessage("Target " + newFullPath + " does already exist") .expected() .handle(); - fileSystemModel.refresh(); + fileSystemModel.refreshSync(); return old; } } try { - fileSystemModel.getFileSystem().move(fullPath, newFullPath); - fileSystemModel.refresh(); + var builder = MoveFileActionProvider.Action.builder(); + builder.initEntries(fileSystemModel, List.of(old)); + builder.target(newFullPath); + builder.build().executeSync(); + var b = all.getValue().stream() .filter(browserEntry -> browserEntry.getRawFileEntry().getPath().equals(newFullPath)) @@ -136,7 +159,7 @@ public final class BrowserFileListModel { .orElse(old); return b; } catch (Exception e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); return old; } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java index dc18cb107..27309b700 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListNameCell.java @@ -8,6 +8,7 @@ import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.PlatformThread; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FilePath; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -58,7 +59,7 @@ class BrowserFileListNameCell extends TableCell { var textField = new LazyTextFieldComp(text) .minWidth(USE_PREF_SIZE) .createStructure() - .get(); + .getTextField(); var quickAccess = createQuickAccessButton(); setupShortcuts(tableView, (ButtonBase) quickAccess); setupRename(fileList, textField, editing); @@ -143,7 +144,7 @@ class BrowserFileListNameCell extends TableCell { getTableRow().requestFocus(); var it = getTableRow().getItem(); editing.setValue(null); - ThreadHelper.runAsync(() -> { + ThreadHelper.runFailableAsync(() -> { if (it == null) { return; } @@ -163,6 +164,21 @@ class BrowserFileListNameCell extends TableCell { PlatformThread.runLaterIfNeeded(() -> { textField.setDisable(false); textField.requestFocus(); + + var content = textField.getText(); + if (content != null && !content.isEmpty()) { + var name = FilePath.of(content); + var baseNameEnd = name.getBaseName().toString().length(); + textField.positionCaret(baseNameEnd); + } + }); + } + }); + + textField.disabledProperty().addListener((observable, oldValue, newValue) -> { + if (!oldValue && newValue) { + Platform.runLater(() -> { + editing.setValue(null); }); } }); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java index fd81572fe..f901da974 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java @@ -4,6 +4,7 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.ext.ConnectionFileSystem; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.FileOpener; @@ -20,58 +21,98 @@ import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Objects; +import java.util.Optional; public class BrowserFileOpener { - private static OutputStream openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes) + private static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes) throws Exception { var fileSystem = model.getFileSystem(); if (model.isClosed() || fileSystem.getShell().isEmpty()) { - return OutputStream.nullOutputStream(); + return BrowserFileOutput.none(); } if (totalBytes == 0) { var existingSize = model.getFileSystem().getFileSize(file.getPath()); if (existingSize != 0) { - var blank = AppDialog.confirm("fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath())); + var blank = AppDialog.confirm( + "fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath())); if (!blank) { - return OutputStream.nullOutputStream(); + return BrowserFileOutput.none(); } } } + var defOutput = new BrowserFileOutput() { + + @Override + public Optional target() { + return Optional.of(model.getEntry().get()); + } + + @Override + public boolean hasOutput() { + return true; + } + + @Override + public OutputStream open() throws Exception { + return fileSystem.openOutput(file.getPath(), totalBytes); + } + }; + var sc = fileSystem.getShell().get(); if (sc.getOsType() == OsType.WINDOWS) { - return fileSystem.openOutput(file.getPath(), totalBytes); + return defOutput; } var info = (FileInfo.Unix) file.getInfo(); var requiresSudo = requiresSudo(model, info, file.getPath()); if (!requiresSudo) { - return fileSystem.openOutput(file.getPath(), totalBytes); + return defOutput; } var elevate = AppDialog.confirm("fileWriteSudo"); if (!elevate) { - return fileSystem.openOutput(file.getPath(), totalBytes); + return defOutput; } var rootSc = sc.identicalDialectSubShell() .elevated(ElevationFunction.elevated(null)) .start(); var rootFs = new ConnectionFileSystem(rootSc); - try { - return new FilterOutputStream(rootFs.openOutput(file.getPath(), totalBytes)) { - @Override - public void close() throws IOException { - super.close(); + var rootOutput = new BrowserFileOutput() { + + @Override + public Optional target() { + return Optional.of(model.getEntry().get()); + } + + @Override + public boolean hasOutput() { + return true; + } + + @Override + public OutputStream open() throws Exception { + try { + return new FilterOutputStream(rootFs.openOutput(file.getPath(), totalBytes)) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + rootFs.close(); + } + } + }; + } catch (Exception ex) { rootFs.close(); + throw ex; } - }; - } catch (Exception ex) { - rootFs.close(); - throw ex; - } + } + }; + return rootOutput; } private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath) @@ -128,10 +169,25 @@ public class BrowserFileOpener { }, (size) -> { if (model.isClosed()) { - return OutputStream.nullOutputStream(); + return BrowserFileOutput.none(); } - return entry.getFileSystem().openOutput(file, size); + return new BrowserFileOutput() { + @Override + public boolean hasOutput() { + return true; + } + + @Override + public Optional target() { + return Optional.of(model.getEntry().get()); + } + + @Override + public OutputStream open() throws Exception { + return entry.getFileSystem().openOutput(file, size); + } + }; }, s -> FileOpener.openWithAnyApplication(s)); } @@ -154,10 +210,25 @@ public class BrowserFileOpener { }, (size) -> { if (model.isClosed()) { - return OutputStream.nullOutputStream(); + return BrowserFileOutput.none(); } - return entry.getFileSystem().openOutput(file, size); + return new BrowserFileOutput() { + @Override + public boolean hasOutput() { + return true; + } + + @Override + public Optional target() { + return Optional.of(model.getEntry().get()); + } + + @Override + public OutputStream open() throws Exception { + return entry.getFileSystem().openOutput(file, size); + } + }; }, s -> FileOpener.openInDefaultApplication(s)); } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java new file mode 100644 index 000000000..37b22b415 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOutput.java @@ -0,0 +1,35 @@ +package io.xpipe.app.browser.file; + +import io.xpipe.app.storage.DataStoreEntry; + +import java.io.OutputStream; +import java.util.Optional; + +public interface BrowserFileOutput { + + static BrowserFileOutput none() { + return new BrowserFileOutput() { + + @Override + public Optional target() { + return Optional.empty(); + } + + @Override + public boolean hasOutput() { + return false; + } + + @Override + public OutputStream open() { + return null; + } + }; + } + + Optional target(); + + boolean hasOutput(); + + OutputStream open() throws Exception; +} diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java index 25b63bf8b..e3d1156ef 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.core.process.OsType; import io.xpipe.core.store.*; @@ -26,9 +26,6 @@ public class BrowserFileSystemHelper { } // Handle special case when file system creation has failed - if (model.getFileSystem() == null) { - return path; - } var shell = model.getFileSystem().getShell(); if (shell.isEmpty()) { @@ -47,10 +44,6 @@ public class BrowserFileSystemHelper { return null; } - if (model.getFileSystem() == null) { - return path; - } - var shell = model.getFileSystem().getShell(); if (shell.isEmpty() || !shell.get().isRunning(true)) { return path; @@ -63,7 +56,7 @@ public class BrowserFileSystemHelper { .readStdoutOrThrow(); return !r.isBlank() ? r : null; } catch (Exception ex) { - ErrorEvent.expected(ex); + ErrorEventFactory.expected(ex); throw ex; } } @@ -74,10 +67,6 @@ public class BrowserFileSystemHelper { return null; } - if (model.getFileSystem() == null) { - return path; - } - var shell = model.getFileSystem().getShell(); if (shell.isEmpty()) { return path; @@ -105,24 +94,20 @@ public class BrowserFileSystemHelper { return; } - if (model.getFileSystem() == null) { - return; - } - var shell = model.getFileSystem().getShell(); if (shell.isEmpty()) { return; } if (verifyExists && !model.getFileSystem().directoryExists(path)) { - throw ErrorEvent.expected(new IllegalArgumentException( + throw ErrorEventFactory.expected(new IllegalArgumentException( String.format("Directory %s does not exist or is not accessible", path))); } try { model.getFileSystem().directoryAccessible(path); } catch (Exception ex) { - ErrorEvent.expected(ex); + ErrorEventFactory.expected(ex); throw ex; } } @@ -146,7 +131,7 @@ public class BrowserFileSystemHelper { try { file.getFileSystem().delete(file.getPath()); } catch (Throwable t) { - ErrorEvent.fromThrowable(t).handle(); + ErrorEventFactory.fromThrowable(t).handle(); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java index ca4c12c78..05a58b9b3 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabComp.java @@ -1,7 +1,7 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.BrowserFullSessionModel; -import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.menu.BrowserMenuProviders; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.comp.SimpleCompStructure; @@ -65,11 +65,12 @@ public class BrowserFileSystemTabComp extends SimpleComp { keyEvent.consume(); }); - var backBtn = BrowserAction.byId("back", model, List.of()).toButton(root, model, List.of()); - var forthBtn = BrowserAction.byId("forward", model, List.of()).toButton(root, model, List.of()); - var refreshBtn = BrowserAction.byId("refresh", model, List.of()).toButton(root, model, List.of()); + var backBtn = BrowserMenuProviders.byId("back", model, List.of()).toButton(root, model, List.of()); + var forthBtn = BrowserMenuProviders.byId("forward", model, List.of()).toButton(root, model, List.of()); + var refreshBtn = BrowserMenuProviders.byId("refresh", model, List.of()).toButton(root, model, List.of()); // Don't handle key events for this button, we also have that available as a menu item - var terminalBtn = BrowserAction.byId("openTerminal", model, List.of()).toButton(new Region(), model, List.of()); + var terminalBtn = + BrowserMenuProviders.byId("openTerminal", model, List.of()).toButton(new Region(), model, List.of()); var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open")); new ContextMenuAugment<>( @@ -80,7 +81,20 @@ public class BrowserFileSystemTabComp extends SimpleComp { menuButton.disableProperty().bind(model.getInOverview()); menuButton.setAccessibleText("Directory options"); - var filter = new BrowserFileListFilterComp(model, model.getFilter()).createStructure(); + var smallWidth = Bindings.createBooleanBinding( + () -> { + return root.getWidth() < 450; + }, + root.widthProperty()); + + refreshBtn.managedProperty().bind(smallWidth.not()); + refreshBtn.visibleProperty().bind(refreshBtn.managedProperty()); + terminalBtn.managedProperty().bind(smallWidth.not()); + terminalBtn.visibleProperty().bind(terminalBtn.managedProperty()); + + var filter = new BrowserFileListFilterComp(model, model.getFilter()) + .hide(smallWidth) + .createStructure(); var topBar = new HBox(); topBar.setAlignment(Pos.CENTER); @@ -101,6 +115,7 @@ public class BrowserFileSystemTabComp extends SimpleComp { refreshBtn, terminalBtn, menuButton); + topBar.setMinWidth(0); if (model.getBrowserModel() instanceof BrowserFullSessionModel fullSessionModel) { var pinButton = new Button(); 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 b7091b32b..298c9307e 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 @@ -1,14 +1,17 @@ package io.xpipe.app.browser.file; +import io.xpipe.app.action.ActionProvider; import io.xpipe.app.browser.BrowserAbstractSessionModel; import io.xpipe.app.browser.BrowserFullSessionModel; import io.xpipe.app.browser.BrowserStoreSessionTab; -import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.impl.TransferFilesActionProvider; +import io.xpipe.app.browser.menu.BrowserMenuItemProvider; import io.xpipe.app.comp.Comp; import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.ext.ShellStore; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.ext.WrapperFileSystem; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.terminal.*; @@ -17,7 +20,6 @@ import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.*; import io.xpipe.core.store.*; import io.xpipe.core.util.FailableConsumer; -import io.xpipe.core.util.FailableRunnable; import javafx.beans.binding.Bindings; import javafx.beans.property.*; @@ -26,7 +28,6 @@ import javafx.collections.ObservableList; import lombok.Getter; import lombok.NonNull; -import lombok.SneakyThrows; import java.io.IOException; import java.nio.file.Path; @@ -47,7 +48,9 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab progress = new SimpleObjectProperty<>(); private final ObservableList terminalRequests = FXCollections.observableArrayList(); private final BooleanProperty transferCancelled = new SimpleBooleanProperty(); + private FileSystem fileSystem; + private BrowserFileSystemSavedState savedState; private BrowserFileSystemCache cache; @@ -79,8 +82,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab originalFs.getShell().get().isRunning(true)); } fs.open(); // Listen to kill after init as the shell might get killed during init for certain reasons @@ -105,8 +110,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab { - if (fileSystem == null) { - return; - } - var current = getCurrentDirectory(); // We might close this after storage shutdown // If this entry does not exist, it's not that bad if we save it anyway @@ -134,17 +137,12 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab c, boolean refresh) { ThreadHelper.runFailableAsync(() -> { - if (fileSystem == null) { - return; - } - BooleanScope.executeExclusive(busy, () -> { if (entry.getStore() instanceof ShellStore s) { c.accept(fileSystem.getShell().orElseThrow()); @@ -176,21 +166,30 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab { - cdSyncWithoutCheck(currentPath.get()); - }); - } - - public void refreshSync() throws Exception { + public void refreshSync() { cdSyncWithoutCheck(currentPath.get()); } + public void refreshEntriesSync(List entries) throws Exception { + if (fileList.getAll().getValue().size() < 10) { + refreshSync(); + return; + } + + if (entries.size() > 10 && fileList.getAll().getValue().size() < 100) { + refreshSync(); + return; + } + + for (BrowserEntry browserEntry : entries) { + var refresh = fileSystem.getFileInfo(browserEntry.getRawFileEntry().getPath()); + fileList.updateEntry(browserEntry.getRawFileEntry().getPath(), refresh.orElse(null)); + } + } + public FileEntry getCurrentParentDirectory() { - var current = getCurrentDirectory(); - if (current == null) { - return null; + if (currentPath.get() == null) { + return FileEntry.ofDirectory(fileSystem, FilePath.of("?")); } var parent = currentPath.get().getParent(); @@ -202,12 +201,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab files) { ThreadHelper.runFailableAsync(() -> { BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - startIfNeeded(); var op = BrowserFileTransferOperation.ofLocal( entry, files, BrowserFileTransferMode.COPY, true, progress::setValue, transferCancelled); - op.execute(); + var action = TransferFilesActionProvider.Action.builder() + .operation(op) + .target(this.entry.asNeeded()) + .build(); + action.executeSync(); refreshSync(); }); }); @@ -440,137 +425,21 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab { BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - startIfNeeded(); var op = new BrowserFileTransferOperation( target, files, mode, true, progress::setValue, transferCancelled); - op.execute(); - refreshSync(); - }); - }); - } - - public void createDirectoryAsync(String name) { - if (name == null || name.isBlank()) { - return; - } - - if (getCurrentDirectory() == null) { - return; - } - - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - - startIfNeeded(); - var abs = getCurrentDirectory().getPath().join(name); - if (fileSystem.directoryExists(abs)) { - throw ErrorEvent.expected( - new IllegalStateException(String.format("Directory %s already exists", abs))); - } - - fileSystem.mkdirs(abs); - refreshSync(); - }); - }); - } - - public void createLinkAsync(String linkName, FilePath targetFile) { - if (linkName == null || linkName.isBlank() || targetFile == null) { - return; - } - - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - - if (getCurrentDirectory() == null) { - return; - } - - startIfNeeded(); - var abs = getCurrentDirectory().getPath().join(linkName); - fileSystem.symbolicLink(abs, targetFile); - refreshSync(); - }); - }); - } - - public void runCommandAsync(CommandBuilder command, boolean refresh) { - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - - if (getCurrentDirectory() == null) { - return; - } - - fileSystem - .getShell() - .orElseThrow() - .command(command) - .withWorkingDirectory(getCurrentDirectory().getPath()) - .execute(); - if (refresh) { - refreshSync(); - } - }); - }); - } - - public void runAsync(FailableRunnable r, boolean refresh) { - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - - if (getCurrentDirectory() == null) { - return; - } - - r.run(); - if (refresh) { - refreshSync(); - } - }); - }); - } - - public void createFileAsync(String name) { - if (name == null || name.isBlank()) { - return; - } - - ThreadHelper.runFailableAsync(() -> { - BooleanScope.executeExclusive(busy, () -> { - if (fileSystem == null) { - return; - } - - if (getCurrentDirectory() == null) { - return; - } - - var abs = getCurrentDirectory().getPath().join(name); - fileSystem.touch(abs); + var action = TransferFilesActionProvider.Action.builder() + .operation(op) + .target(entry.asNeeded()) + .build(); + action.executeSync(); refreshSync(); }); }); } public boolean isClosed() { - return fileSystem == null; + return false; } public void initWithGivenDirectory(FilePath dir) { @@ -585,30 +454,28 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab { - if (fileSystem == null) { - return; - } - BooleanScope.executeExclusive(busy, () -> { - if (fileSystem.getShell().isPresent()) { - var dock = shouldLaunchSplitTerminal() && dockIfPossible; - var uuid = UUID.randomUUID(); - terminalRequests.add(uuid); - if (dock - && browserModel instanceof BrowserFullSessionModel fullSessionModel - && !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) { - fullSessionModel.splitTab( - this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests)); - } - TerminalLauncher.open(entry.get(), name, directory, processControl, uuid, !dock); - - // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively - startIfNeeded(); - } + openTerminalSync(name, directory, processControl, dockIfPossible); }); }); } + public void openTerminalSync(String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible) + throws Exception { + var dock = shouldLaunchSplitTerminal() && dockIfPossible; + var uuid = UUID.randomUUID(); + terminalRequests.add(uuid); + if (dock + && browserModel instanceof BrowserFullSessionModel fullSessionModel + && !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) { + fullSessionModel.splitTab(this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests)); + } + TerminalLauncher.open(entry.get(), name, directory, processControl, uuid, !dock); + + // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively + startIfNeeded(); + } + public void backSync(int i) { var b = history.back(i); if (b != null) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java index 0608dc2f5..8bf483986 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -1,14 +1,18 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.*; import javafx.beans.property.BooleanProperty; +import javafx.beans.value.ChangeListener; + +import lombok.Getter; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; @@ -20,8 +24,12 @@ import java.util.regex.Pattern; public class BrowserFileTransferOperation { + @Getter private final FileEntry target; + + @Getter private final List files; + private final BrowserFileTransferMode transferMode; private final boolean checkConflicts; private final Consumer progress; @@ -184,7 +192,7 @@ public class BrowserFileTransferOperation { } if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) { - throw ErrorEvent.expected( + throw ErrorEventFactory.expected( new IllegalArgumentException("Target directory " + targetFile + " does already exist")); } @@ -232,8 +240,8 @@ public class BrowserFileTransferOperation { } } - var noExt = target.getFileName().equals(target.getExtension()); - return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + target.getExtension())); + var ext = target.getExtension(); + return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : "")); } private void handleSingleAcrossFileSystems(FileEntry source) throws Exception { @@ -287,59 +295,90 @@ public class BrowserFileTransferOperation { totalSize.addAndGet(source.getFileSizeLong().orElse(0)); } - var start = Instant.now(); - AtomicLong transferred = new AtomicLong(); - for (var e : flatFiles.entrySet()) { - if (cancelled()) { - return; - } + var originalSourceFs = flatFiles.keySet().iterator().next().getFileSystem(); + if (!flatFiles.keySet().stream() + .allMatch(fileEntry -> fileEntry.getFileSystem().equals(originalSourceFs))) { + throw new IllegalArgumentException("Mixed source file systems"); + } - var sourceFile = e.getKey(); - var fixedRelPath = FilePath.of(e.getValue()) - .fileSystemCompatible( - target.getFileSystem().getShell().orElseThrow().getOsType()); - var targetFile = target.getPath().join(fixedRelPath.toString()); - if (sourceFile.getFileSystem().equals(target.getFileSystem())) { - throw new IllegalStateException(); - } + var optimizedSourceFs = originalSourceFs.createTransferOptimizedFileSystem(); + var targetFs = target.getFileSystem().createTransferOptimizedFileSystem(); - if (sourceFile.getKind() == FileKind.DIRECTORY) { - target.getFileSystem().mkdirs(targetFile); - } else if (sourceFile.getKind() == FileKind.FILE) { - if (checkConflicts) { - var fileConflictChoice = - handleChoice(target.getFileSystem(), targetFile, files.size() > 1 || flatFiles.size() > 1); - if (fileConflictChoice == BrowserAlerts.FileConflictChoice.SKIP - || fileConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) { - continue; - } - - if (fileConflictChoice == BrowserAlerts.FileConflictChoice.RENAME) { - targetFile = renameFileLoop(target.getFileSystem(), targetFile, false); - } + try { + var start = Instant.now(); + AtomicLong transferred = new AtomicLong(); + for (var e : flatFiles.entrySet()) { + if (cancelled()) { + return; } - transfer(sourceFile, targetFile, transferred, totalSize, start); + var sourceFile = e.getKey(); + var fixedRelPath = FilePath.of(e.getValue()) + .fileSystemCompatible(targetFs.getShell().orElseThrow().getOsType()); + var targetFile = target.getPath().join(fixedRelPath.toString()); + if (sourceFile.getFileSystem().equals(targetFs)) { + throw new IllegalStateException(); + } + + if (sourceFile.getKind() == FileKind.DIRECTORY) { + targetFs.mkdirs(targetFile); + } else if (sourceFile.getKind() == FileKind.FILE) { + if (checkConflicts) { + var fileConflictChoice = + handleChoice(targetFs, targetFile, files.size() > 1 || flatFiles.size() > 1); + if (fileConflictChoice == BrowserAlerts.FileConflictChoice.SKIP + || fileConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) { + continue; + } + + if (fileConflictChoice == BrowserAlerts.FileConflictChoice.RENAME) { + targetFile = renameFileLoop(targetFs, targetFile, false); + } + } + + transfer( + sourceFile.getPath(), + optimizedSourceFs, + targetFile, + targetFs, + transferred, + totalSize, + start); + } + } + updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get())); + } finally { + if (optimizedSourceFs != originalSourceFs) { + optimizedSourceFs.close(); + } + if (target.getFileSystem() != targetFs) { + targetFs.close(); } } - updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get())); } private void transfer( - FileEntry sourceFile, FilePath targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start) + FilePath sourceFile, + FileSystem sourceFs, + FilePath targetFile, + FileSystem targetFs, + AtomicLong transferred, + AtomicLong totalSize, + Instant start) throws Exception { if (cancelled()) { return; } + var fileSize = sourceFs.getFileSize(sourceFile); + InputStream inputStream = null; OutputStream outputStream = null; try { - var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath()); // Read the first few bytes to figure out possible command failure early // before creating the output stream - inputStream = new BufferedInputStream(sourceFile.getFileSystem().openInput(sourceFile.getPath()), 1024); + inputStream = new BufferedInputStream(sourceFs.openInput(sourceFile), 1024); inputStream.mark(1024); var streamStart = new byte[1024]; var streamStartLength = inputStream.read(streamStart, 0, 1024); @@ -350,13 +389,11 @@ public class BrowserFileTransferOperation { inputStream.reset(); } - outputStream = target.getFileSystem().openOutput(targetFile, fileSize); + outputStream = targetFs.openOutput(targetFile, fileSize); transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start, fileSize); - outputStream.flush(); - inputStream.transferTo(OutputStream.nullOutputStream()); } catch (Exception ex) { // Mark progress as finished to reset any progress display - updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get())); + updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), transferred.get())); if (inputStream != null) { try { @@ -364,7 +401,7 @@ public class BrowserFileTransferOperation { } catch (Exception om) { // This is expected as the process control has to be killed // When calling close, it will throw an exception when it has to kill - // ErrorEvent.fromThrowable(om).handle(); + ErrorEventFactory.fromThrowable(om).expected().omit().handle(); } } if (outputStream != null) { @@ -373,12 +410,24 @@ public class BrowserFileTransferOperation { } catch (Exception om) { // This is expected as the process control has to be killed // When calling close, it will throw an exception when it has to kill - // ErrorEvent.fromThrowable(om).handle(); + ErrorEventFactory.fromThrowable(om).expected().omit().handle(); } } throw ex; } + // If we receive a cancel while we are closing, there's a good chance that the close is stuck + // Then, we just straight up kill the shells + ChangeListener closeCancelListener = (observableValue, oldValue, newValue) -> { + if (!newValue) { + return; + } + + sourceFs.getShell().orElseThrow().killExternal(); + targetFs.getShell().orElseThrow().killExternal(); + }; + cancelled.addListener(closeCancelListener); + Exception exception = null; try { inputStream.close(); @@ -389,12 +438,18 @@ public class BrowserFileTransferOperation { outputStream.close(); } catch (Exception om) { if (exception != null) { - ErrorEvent.fromThrowable(om).handle(); + exception.addSuppressed(om); } else { exception = om; } } + + cancelled.removeListener(closeCancelListener); + if (exception != null) { + ErrorEventFactory.preconfigure(ErrorEventFactory.fromThrowable(exception) + .reportable(!cancelled()) + .omitted(cancelled())); throw exception; } } @@ -406,7 +461,7 @@ public class BrowserFileTransferOperation { private static final int DEFAULT_BUFFER_SIZE = 1024; private void transferFile( - FileEntry sourceFile, + FilePath sourceFile, InputStream inputStream, OutputStream outputStream, AtomicLong transferred, @@ -415,13 +470,13 @@ public class BrowserFileTransferOperation { long expectedFileSize) throws Exception { // Initialize progress immediately prior to reading anything - updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start)); + updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get(), start)); var killStreams = new AtomicBoolean(false); var exception = new AtomicReference(); + var readCount = new AtomicLong(); var thread = ThreadHelper.createPlatformThread("transfer", true, () -> { try { - long readCount = 0; var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, expectedFileSize); byte[] buffer = new byte[bs]; int read; @@ -438,14 +493,17 @@ public class BrowserFileTransferOperation { outputStream.write(buffer, 0, read); transferred.addAndGet(read); - readCount += read; - updateProgress( - new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start)); + readCount.addAndGet(read); + updateProgress(new BrowserTransferProgress( + sourceFile.getFileName(), transferred.get(), total.get(), start)); } - var incomplete = readCount < expectedFileSize; + outputStream.flush(); + inputStream.transferTo(OutputStream.nullOutputStream()); + + var incomplete = readCount.get() < expectedFileSize; if (incomplete) { - throw new IOException("Source file " + sourceFile.getPath() + " input did end prematurely"); + throw new IOException("Source file " + sourceFile + " input did end prematurely"); } } catch (Exception ex) { exception.set(ex); @@ -459,9 +517,7 @@ public class BrowserFileTransferOperation { var cancelled = cancelled(); if (cancelled) { - // Assume that the transfer has stalled if it doesn't finish until then - thread.join(1000); - killStreams(); + killStreams(thread, readCount, false); break; } @@ -471,7 +527,7 @@ public class BrowserFileTransferOperation { } if (killStreams.get()) { - killStreams(); + killStreams(thread, readCount, true); } var ex = exception.get(); @@ -491,17 +547,33 @@ public class BrowserFileTransferOperation { var sourceShell = sourceFs.getShell().orElseThrow(); var targetShell = targetFs.getShell().orElseThrow(); // Check for null on shell reset - return sourceShell.getStdout() != null && !sourceShell.getStdout().isClosed() - && targetShell.getStdin() != null && !targetShell.getStdin().isClosed(); + return sourceShell.getStdout() != null + && !sourceShell.getStdout().isClosed() + && targetShell.getStdin() != null + && !targetShell.getStdin().isClosed(); } else { return true; } } - private void killStreams() throws Exception { + private void killStreams(Thread thread, AtomicLong transferred, boolean instant) throws Exception { var sourceFs = files.getFirst().getFileSystem(); var targetFs = target.getFileSystem(); var same = files.getFirst().getFileSystem().equals(target.getFileSystem()); + + if (!instant && !same && checkTransferValidity()) { + var initialTransferred = transferred.get(); + if (!thread.join(Duration.ofMillis(1000))) { + var nowTransferred = transferred.get(); + var stuck = initialTransferred == nowTransferred; + if (stuck) { + sourceFs.getShell().orElseThrow().killExternal(); + targetFs.getShell().orElseThrow().killExternal(); + return; + } + } + } + if (!same) { var sourceShell = sourceFs.getShell().orElseThrow(); var targetShell = targetFs.getShell().orElseThrow(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java index f21f54578..84a92ebba 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java @@ -82,7 +82,9 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState { if (ls == null) { ls = List.of(); } - var valid = ls.stream().filter(entry -> entry.getUuid() != null && entry.getPath() != null).toList(); + var valid = ls.stream() + .filter(entry -> entry.getUuid() != null && entry.getPath() != null) + .toList(); return new BrowserHistorySavedStateImpl(valid); } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java index 93b69839e..43747a824 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserOverviewComp.java @@ -4,7 +4,7 @@ import io.xpipe.app.comp.SimpleComp; import io.xpipe.app.comp.base.SimpleTitledPaneComp; import io.xpipe.app.comp.base.VerticalComp; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.util.DerivedObservableList; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.ShellControl; @@ -32,9 +32,6 @@ public class BrowserOverviewComp extends SimpleComp { @SneakyThrows protected Region createSimple() { // The open file system might have already been closed - if (model.getFileSystem() == null) { - return new Region(); - } ShellControl sc = model.getFileSystem().getShell().orElseThrow(); @@ -44,14 +41,11 @@ public class BrowserOverviewComp extends SimpleComp { .map(s -> FileEntry.ofDirectory(model.getFileSystem(), s)) .filter(entry -> { var fs = model.getFileSystem(); - if (fs == null) { - return false; - } try { return fs.directoryExists(entry.getPath()); } catch (Exception e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); return false; } }) diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index 8db966180..925f21387 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -64,8 +64,7 @@ public class BrowserStatusBarComp extends SimpleComp { var cancel = PlatformThread.sync(model.getTransferCancelled()); var hide = Bindings.createBooleanBinding( () -> { - if (model.getProgress().getValue() == null - || model.getProgress().getValue().done()) { + if (model.getProgress().getValue() == null) { return true; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java index adbdd224e..9bf89e2e6 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java @@ -71,7 +71,7 @@ public class BrowserTransferComp extends SimpleComp { var hideProgress = sourceItem.get().downloadFinished().get(); - var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0; + var share = p.getTransferred() * 100 / p.getTotal(); var progressSuffix = hideProgress ? "" : " " + share + "%"; return entry.getFileName() + progressSuffix; }, 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 138f76b16..cecebffc0 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 @@ -1,13 +1,15 @@ package io.xpipe.app.browser.file; import io.xpipe.app.browser.BrowserFullSessionModel; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.browser.action.impl.TransferFilesActionProvider; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.DesktopHelper; import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.ThreadHelper; - import io.xpipe.core.process.OsType; + import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; @@ -77,7 +79,7 @@ public class BrowserTransferModel { try { FileUtils.forceDelete(item.getLocalFile().toFile()); } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); } } @@ -118,7 +120,7 @@ public class BrowserTransferModel { try { FileUtils.forceMkdir(TEMP.toFile()); } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); return; } @@ -151,9 +153,14 @@ public class BrowserTransferModel { itemModel.getProgress().setValue(progress); }, itemModel.getTransferCancelled()); - op.execute(); + var action = TransferFilesActionProvider.Action.builder() + .operation(op) + .target(DataStorage.get().local().ref()) + .download(true) + .build(); + action.executeSync(); } catch (Throwable t) { - ErrorEvent.fromThrowable(t).handle(); + ErrorEventFactory.fromThrowable(t).handle(); synchronized (items) { items.remove(item); } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java index 7041fa414..faaa8552b 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser.icon; -import io.xpipe.app.resources.AppResources; +import io.xpipe.app.core.AppResources; import io.xpipe.core.store.FileEntry; import io.xpipe.core.store.FileKind; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java index 9bf645f4b..83b6cf778 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java @@ -1,6 +1,6 @@ package io.xpipe.app.browser.icon; -import io.xpipe.app.resources.AppResources; +import io.xpipe.app.core.AppResources; import io.xpipe.core.store.FileEntry; import io.xpipe.core.store.FileKind; @@ -85,7 +85,8 @@ public abstract class BrowserIconFileType { var name = entry.getPath().getFileName(); var ext = entry.getPath().getExtension(); - return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name); + return (ext.isPresent() && endings.contains("." + ext.get().toLowerCase(Locale.ROOT))) + || endings.contains(name); } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java index 1d771981d..db265bd81 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java @@ -14,8 +14,8 @@ public class BrowserIcons { return PrettyImageHelper.ofFixedSizeSquare("browser/default_folder.svg", 24); } - public static Comp createIcon(BrowserIconFileType type) { - return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 24); + public static Comp createContextMenuIcon(BrowserIconFileType type) { + return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 16); } public static Comp createIcon(FileEntry entry) { diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserApplicationPathAction.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java similarity index 81% rename from app/src/main/java/io/xpipe/app/browser/action/BrowserApplicationPathAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java index 5a5c9b572..7d53c6076 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserApplicationPathAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserApplicationPathMenuProvider.java @@ -1,11 +1,11 @@ -package io.xpipe.app.browser.action; +package io.xpipe.app.browser.menu; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import java.util.List; -public interface BrowserApplicationPathAction extends BrowserAction { +public interface BrowserApplicationPathMenuProvider extends BrowserMenuItemProvider { String getExecutable(); diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserBranchAction.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java similarity index 59% rename from app/src/main/java/io/xpipe/app/browser/action/BrowserBranchAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java index cf2900cd6..e556c7ed4 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserBranchAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java @@ -1,4 +1,4 @@ -package io.xpipe.app.browser.action; +package io.xpipe.app.browser.menu; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; @@ -11,7 +11,7 @@ import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; -public interface BrowserBranchAction extends BrowserAction { +public interface BrowserMenuBranchProvider extends BrowserMenuItemProvider { default MenuItem toMenuItem(BrowserFileSystemTabModel model, List selected) { var m = new Menu(getName(model, selected).getValue() + " ..."); @@ -20,16 +20,24 @@ public interface BrowserBranchAction extends BrowserAction { if (!sub.isApplicable(model, subselected)) { continue; } - m.getItems().add(sub.toMenuItem(model, subselected)); + var item = sub.toMenuItem(model, subselected); + if (item != null) { + m.getItems().add(item); + } } + + if (m.getItems().isEmpty()) { + return null; + } + var graphic = getIcon(model, selected); if (graphic != null) { - m.setGraphic(graphic); + m.setGraphic(graphic.createGraphicNode()); } m.setDisable(!isActive(model, selected)); - if (getProFeatureId() != null - && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { + if (getLicensedFeatureId() != null + && !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) { m.setDisable(true); m.setGraphic(new FontIcon("mdi2p-professional-hexagon")); } @@ -37,5 +45,6 @@ public interface BrowserBranchAction extends BrowserAction { return m; } - List getBranchingActions(BrowserFileSystemTabModel model, List entries); + List getBranchingActions( + BrowserFileSystemTabModel model, List entries); } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuCategory.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuCategory.java new file mode 100644 index 000000000..f9ec0601c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuCategory.java @@ -0,0 +1,9 @@ +package io.xpipe.app.browser.menu; + +public enum BrowserMenuCategory { + CUSTOM, + OPEN, + COPY_PASTE, + ACTION, + MUTATION +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java new file mode 100644 index 000000000..50c2e7a8f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuItemProvider.java @@ -0,0 +1,58 @@ +package io.xpipe.app.browser.menu; + +import io.xpipe.app.action.ActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.util.LabelGraphic; + +import javafx.beans.value.ObservableValue; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCombination; + +import java.util.List; + +public interface BrowserMenuItemProvider extends ActionProvider { + + MenuItem toMenuItem(BrowserFileSystemTabModel model, List selected); + + default void init(BrowserFileSystemTabModel model) throws Exception {} + + default boolean automaticallyResolveLinks() { + return true; + } + + default List resolveFilesIfNeeded(List selected) { + return automaticallyResolveLinks() + ? selected.stream() + .map(browserEntry -> + new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel())) + .toList() + : selected; + } + + default LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return null; + } + + default BrowserMenuCategory getCategory() { + return null; + } + + default KeyCombination getShortcut() { + return null; + } + + ObservableValue getName(BrowserFileSystemTabModel model, List entries); + + default boolean acceptsEmptySelection() { + return false; + } + + default boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return true; + } + + default boolean isActive(BrowserFileSystemTabModel model, List entries) { + return true; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java new file mode 100644 index 000000000..1c68e6471 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java @@ -0,0 +1,156 @@ +package io.xpipe.app.browser.menu; + +import io.xpipe.app.action.AbstractAction; +import io.xpipe.app.browser.action.BrowserAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.BrowserActionProviders; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.comp.base.TooltipHelper; +import io.xpipe.app.hub.action.StoreAction; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.BindingsHelper; +import io.xpipe.app.util.LicenseProvider; + +import javafx.scene.control.Button; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; + +import lombok.SneakyThrows; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider { + + default void execute(BrowserFileSystemTabModel model, List entries) throws Exception { + createAction(model, entries).executeAsync(); + } + + default Class getDelegateActionProvider() { + return null; + } + + @Override + default boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + if (getDelegateActionProvider() != null) { + var provider = BrowserActionProviders.forClass(getDelegateActionProvider()); + return provider.isApplicable(model, entries); + } else { + return true; + } + } + + @SneakyThrows + default AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var c = getDelegateActionProvider() != null + ? BrowserActionProviders.forClass(getDelegateActionProvider()) + .getActionClass() + .orElseThrow() + : getActionClass().orElseThrow(); + var bm = c.getDeclaredMethod("builder"); + bm.setAccessible(true); + var b = bm.invoke(null); + + if (StoreAction.class.isAssignableFrom(c)) { + var refMethod = b.getClass().getMethod("ref", DataStoreEntryRef.class); + refMethod.setAccessible(true); + refMethod.invoke(b, model.getEntry()); + } + + if (BrowserAction.class.isAssignableFrom(c)) { + var modelMethod = b.getClass().getMethod("model", BrowserFileSystemTabModel.class); + modelMethod.setAccessible(true); + modelMethod.invoke(b, model); + + var entriesMethod = b.getClass().getMethod("files", List.class); + entriesMethod.setAccessible(true); + entriesMethod.invoke( + b, + entries.stream() + .map(browserEntry -> browserEntry.getRawFileEntry().getPath()) + .toList()); + } + + var m = b.getClass().getDeclaredMethod("build"); + m.setAccessible(true); + var defValue = c.cast(m.invoke(b)); + return (AbstractAction) defValue; + } + + default Button toButton(Region root, BrowserFileSystemTabModel model, List selected) { + var b = new Button(); + b.setOnAction(event -> { + try { + execute(model, selected); + } catch (Exception e) { + throw new RuntimeException(e); + } + event.consume(); + }); + var name = getName(model, selected); + Tooltip.install(b, TooltipHelper.create(name, getShortcut())); + var graphic = getIcon(model, selected); + if (graphic != null) { + b.setGraphic(graphic.createGraphicNode()); + } + b.setMnemonicParsing(false); + b.accessibleTextProperty().bind(name); + root.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (getShortcut() != null && getShortcut().match(event)) { + b.fire(); + event.consume(); + } + }); + + b.setDisable(!isActive(model, selected)); + model.getCurrentPath().addListener((observable, oldValue, newValue) -> { + b.setDisable(!isActive(model, selected)); + }); + + if (getLicensedFeatureId() != null + && !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) { + b.setDisable(true); + b.setGraphic(new FontIcon("mdi2p-professional-hexagon")); + } + + return b; + } + + default MenuItem toMenuItem(BrowserFileSystemTabModel model, List selected) { + var name = getName(model, selected); + var mi = new MenuItem(); + mi.textProperty().bind(BindingsHelper.map(name, s -> { + if (getLicensedFeatureId() != null) { + return LicenseProvider.get().getFeature(getLicensedFeatureId()).suffix(s); + } + return s; + })); + mi.setOnAction(event -> { + try { + execute(model, selected); + } catch (Exception e) { + throw new RuntimeException(e); + } + event.consume(); + }); + if (getShortcut() != null) { + mi.setAccelerator(getShortcut()); + } + var graphic = getIcon(model, selected); + if (graphic != null) { + mi.setGraphic(graphic.createGraphicNode()); + } + mi.setMnemonicParsing(false); + mi.setDisable(!isActive(model, selected)); + + if (getLicensedFeatureId() != null + && !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) { + mi.setDisable(true); + } + + return mi; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java new file mode 100644 index 000000000..c97fa544c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuProviders.java @@ -0,0 +1,38 @@ +package io.xpipe.app.browser.menu; + +import io.xpipe.app.action.ActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; + +import java.util.List; + +public class BrowserMenuProviders { + + public static List getFlattened( + BrowserFileSystemTabModel model, List entries) { + return ActionProvider.ALL.stream() + .map(browserAction -> browserAction instanceof BrowserMenuItemProvider ba + ? getFlattened(ba, model, entries) + : List.of()) + .flatMap(List::stream) + .toList(); + } + + public static List getFlattened( + BrowserMenuItemProvider browserAction, BrowserFileSystemTabModel model, List entries) { + return browserAction instanceof BrowserMenuLeafProvider + ? List.of((BrowserMenuLeafProvider) browserAction) + : ((BrowserMenuBranchProvider) browserAction) + .getBranchingActions(model, entries).stream() + .map(action -> getFlattened(action, model, entries)) + .flatMap(List::stream) + .toList(); + } + + public static BrowserMenuLeafProvider byId(String id, BrowserFileSystemTabModel model, List entries) { + return getFlattened(model, entries).stream() + .filter(browserAction -> id.equals(browserAction.getId())) + .findAny() + .orElseThrow(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java b/app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java similarity index 61% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java index 8771bfb1e..489df88b1 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/FileTypeMenuProvider.java @@ -1,20 +1,18 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu; -import io.xpipe.app.browser.action.BrowserAction; 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 javafx.scene.Node; +import io.xpipe.app.util.LabelGraphic; import java.util.List; -public interface FileTypeAction extends BrowserAction { +public interface FileTypeMenuProvider extends BrowserMenuItemProvider { @Override - default Node getIcon(BrowserFileSystemTabModel model, List entries) { - return BrowserIcons.createIcon(getType()).createRegion(); + default LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(getType())); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java similarity index 91% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java index 8f11edf85..557164b44 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java @@ -1,7 +1,5 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu; -import io.xpipe.app.browser.action.BrowserBranchAction; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.core.AppI18n; @@ -14,15 +12,16 @@ import javafx.beans.value.ObservableValue; import java.util.List; -public abstract class MultiExecuteAction implements BrowserBranchAction { +public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvider { protected abstract CommandBuilder createCommand( ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry); @Override - public List getBranchingActions(BrowserFileSystemTabModel model, List entries) { + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { return List.of( - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -62,7 +61,7 @@ public abstract class MultiExecuteAction implements BrowserBranchAction { return AppPrefs.get().terminalType().getValue() != null; } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -87,7 +86,7 @@ public abstract class MultiExecuteAction implements BrowserBranchAction { return AppI18n.observable("runInFileBrowser"); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public void execute(BrowserFileSystemTabModel model, List entries) { diff --git a/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java new file mode 100644 index 000000000..ad2008e01 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java @@ -0,0 +1,83 @@ +package io.xpipe.app.browser.menu; + +import io.xpipe.app.browser.action.impl.RunCommandInBackgroundActionProvider; +import io.xpipe.app.browser.action.impl.RunCommandInBrowserActionProvider; +import io.xpipe.app.browser.action.impl.RunCommandInTerminalActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.prefs.AppPrefs; + +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public abstract class MultiExecuteSelectionMenuProvider implements BrowserMenuBranchProvider { + + protected abstract String createCommand(BrowserFileSystemTabModel model); + + protected abstract String getTerminalTitle(); + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return List.of( + new BrowserMenuLeafProvider() { + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var builder = RunCommandInTerminalActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.title(getTerminalTitle()); + builder.command(createCommand(model)); + builder.build().executeAsync(); + } + + @Override + public ObservableValue getName( + BrowserFileSystemTabModel model, List entries) { + var t = AppPrefs.get().terminalType().getValue(); + return AppI18n.observable( + "executeInTerminal", + t != null ? t.toTranslatedString().getValue() : "?"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return AppPrefs.get().terminalType().getValue() != null; + } + }, + new BrowserMenuLeafProvider() { + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var builder = RunCommandInBrowserActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.command(createCommand(model)); + builder.build().executeAsync(); + } + + @Override + public ObservableValue getName( + BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("runInFileBrowser"); + } + }, + new BrowserMenuLeafProvider() { + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var builder = RunCommandInBackgroundActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.command(createCommand(model)); + builder.build().executeAsync(); + } + + @Override + public ObservableValue getName( + BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("runSilent"); + } + }); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java similarity index 76% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java index d8bbd398b..1ddf5f3a0 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java @@ -1,21 +1,19 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class BackAction implements BrowserLeafAction { +public class BackMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -27,8 +25,8 @@ public class BackAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("fth-arrow-left"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("fth-arrow-left"); } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java new file mode 100644 index 000000000..3287cd36d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java @@ -0,0 +1,41 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.impl.BrowseInNativeManagerActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.core.process.OsType; + +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvider { + + @Override + public Class getDelegateActionProvider() { + return BrowseInNativeManagerActionProvider.class; + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return switch (OsType.getLocal()) { + case OsType.Windows windows -> AppI18n.observable("browseInWindowsExplorer"); + case OsType.Linux linux -> AppI18n.observable("browseInDefaultFileManager"); + case OsType.MacOs macOs -> AppI18n.observable("browseInFinder"); + }; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java new file mode 100644 index 000000000..e4e52b4a4 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java @@ -0,0 +1,175 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.impl.ChgrpActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuBranchProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuItemProvider; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileKind; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TextField; + +import java.util.List; +import java.util.stream.Stream; + +public class ChgrpMenuProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2a-account-group-outline"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.MUTATION; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("chgrp"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var os = model.getFileSystem().getShell().orElseThrow().getOsType(); + return os != OsType.WINDOWS && os != OsType.MACOS; + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + if (entries.stream() + .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) { + return List.of(new FlatProvider(), new RecursiveProvider()); + } else { + return getLeafActions(model, false); + } + } + + private static class FlatProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-file-outline"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("flat"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return getLeafActions(model, false); + } + } + + private static class RecursiveProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-file-tree"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("recursive"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return getLeafActions(model, true); + } + } + + private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + List actions = Stream.concat( + model.getCache().getGroups().entrySet().stream() + .filter(e -> !e.getValue().equals("nohome") + && !e.getValue().equals("nogroup") + && !e.getValue().equals("nobody") + && (e.getKey().equals(0) || e.getKey() >= 900)) + .map(e -> e.getValue()) + .map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)), + Stream.of(new CustomProvider(recursive))) + .toList(); + return actions; + } + + private static class FixedProvider implements BrowserMenuLeafProvider { + + private final String group; + private final boolean recursive; + + private FixedProvider(String group, boolean recursive) { + this.group = group; + this.recursive = recursive; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return new SimpleStringProperty(group); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var builder = ChgrpActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.group(group); + builder.recursive(recursive); + var action = builder.build(); + action.executeAsync(); + } + } + + private static class CustomProvider implements BrowserMenuLeafProvider { + + private final boolean recursive; + + private CustomProvider(boolean recursive) { + this.recursive = recursive; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("custom"); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var group = new SimpleStringProperty(); + var modal = ModalOverlay.of( + "groupName", + Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(group); + return creationName; + }) + .prefWidth(350)); + modal.withDefaultButtons(() -> { + if (group.getValue() == null) { + return; + } + + var builder = ChgrpActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.group(group.getValue()); + builder.recursive(recursive); + var action = builder.build(); + action.executeAsync(); + }); + modal.show(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java new file mode 100644 index 000000000..5964295f9 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java @@ -0,0 +1,173 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.impl.ChmodActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuBranchProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuItemProvider; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileKind; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TextField; + +import java.util.List; + +public class ChmodMenuProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2w-wrench-outline"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.MUTATION; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("chmod"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + if (entries.stream() + .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) { + return List.of(new FlatProvider(), new RecursiveProvider()); + } else { + return getLeafActions(model, false); + } + } + + private static class FlatProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-file-outline"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("flat"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return getLeafActions(model, false); + } + } + + private static class RecursiveProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-file-tree"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("recursive"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return getLeafActions(model, true); + } + } + + private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + var custom = new CustomProvider(recursive); + return List.of( + new FixedProvider("400", recursive), + new FixedProvider("600", recursive), + new FixedProvider("644", recursive), + new FixedProvider("700", recursive), + new FixedProvider("755", recursive), + new FixedProvider("777", recursive), + new FixedProvider("u+x", recursive), + new FixedProvider("a+x", recursive), + custom); + } + + private static class FixedProvider implements BrowserMenuLeafProvider { + + private final String permissions; + private final boolean recursive; + + private FixedProvider(String permissions, boolean recursive) { + this.permissions = permissions; + this.recursive = recursive; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return new SimpleStringProperty(permissions); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var builder = ChmodActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.permissions(permissions); + builder.recursive(recursive); + var action = builder.build(); + action.executeAsync(); + } + } + + private static class CustomProvider implements BrowserMenuLeafProvider { + + private final boolean recursive; + + private CustomProvider(boolean recursive) { + this.recursive = recursive; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("custom"); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var permissions = new SimpleStringProperty(); + var modal = ModalOverlay.of( + "chmodPermissions", + Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(permissions); + return creationName; + }) + .prefWidth(350)); + modal.withDefaultButtons(() -> { + if (permissions.getValue() == null) { + return; + } + + var builder = ChmodActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.permissions(permissions.getValue()); + builder.recursive(recursive); + var action = builder.build(); + action.executeAsync(); + }); + modal.show(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java new file mode 100644 index 000000000..a796e1288 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java @@ -0,0 +1,174 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.impl.ChownActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuBranchProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuItemProvider; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileKind; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TextField; + +import java.util.List; +import java.util.stream.Stream; + +public class ChownMenuProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2a-account-edit"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.MUTATION; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("chown"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var os = model.getFileSystem().getShell().orElseThrow().getOsType(); + return os != OsType.WINDOWS && os != OsType.MACOS; + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + if (entries.stream() + .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) { + return List.of(new FlatProvider(), new RecursiveProvider()); + } else { + return getLeafActions(model, false); + } + } + + private static class FlatProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-file-outline"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("flat"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return getLeafActions(model, false); + } + } + + private static class RecursiveProvider implements BrowserMenuBranchProvider { + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-file-tree"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("recursive"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return getLeafActions(model, true); + } + } + + private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + var actions = Stream.concat( + model.getCache().getUsers().entrySet().stream() + .filter(e -> !e.getValue().equals("nohome") + && !e.getValue().equals("nobody") + && (e.getKey().equals(0) || e.getKey() >= 900)) + .map(e -> e.getValue()) + .map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)), + Stream.of(new CustomProvider(recursive))) + .toList(); + return actions; + } + + private static class FixedProvider implements BrowserMenuLeafProvider { + + private final String owner; + private final boolean recursive; + + private FixedProvider(String owner, boolean recursive) { + this.owner = owner; + this.recursive = recursive; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return new SimpleStringProperty(owner); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var builder = ChownActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.owner(owner); + builder.recursive(recursive); + var action = builder.build(); + action.executeAsync(); + } + } + + private static class CustomProvider implements BrowserMenuLeafProvider { + + private final boolean recursive; + + private CustomProvider(boolean recursive) { + this.recursive = recursive; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("custom"); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var user = new SimpleStringProperty(); + var modal = ModalOverlay.of( + "userName", + Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(user); + return creationName; + }) + .prefWidth(350)); + modal.withDefaultButtons(() -> { + if (user.getValue() == null) { + return; + } + + var builder = ChownActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.owner(user.getValue()); + builder.recursive(recursive); + var action = builder.build(); + action.executeAsync(); + }); + modal.show(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java new file mode 100644 index 000000000..0ba0e4595 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java @@ -0,0 +1,57 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.action.AbstractAction; +import io.xpipe.app.browser.action.impl.ComputeDirectorySizesActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.store.FileKind; + +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvider { + + @Override + public AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var builder = ComputeDirectorySizesActionProvider.Action.builder(); + builder.initEntries(model, entries); + return builder.build(); + } + + public String getId() { + return "computeDirectorySizes"; + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-format-list-text"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + var topLevel = + entries.size() == 1 && entries.getFirst().getRawFileEntry().equals(model.getCurrentDirectory()); + return AppI18n.observable(topLevel ? "computeDirectorySizes" : "computeSize"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.stream() + .allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.ACTION; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java similarity index 66% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java index 7c714bd89..4adce0fa0 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java @@ -1,22 +1,21 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserClipboard; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class CopyAction implements BrowserLeafAction { +public class CopyMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -24,13 +23,13 @@ public class CopyAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2c-content-copy"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdoal-file_copy"); } @Override - public Category getCategory() { - return Category.COPY_PASTE; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.COPY_PASTE; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java similarity index 85% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java index d87d4058f..ba84d422c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java @@ -1,13 +1,13 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserAction; -import io.xpipe.app.browser.action.BrowserActionFormatter; -import io.xpipe.app.browser.action.BrowserBranchAction; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuBranchProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.util.ClipboardHelper; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import javafx.beans.property.SimpleObjectProperty; @@ -19,11 +19,11 @@ import javafx.scene.input.KeyCombination; import java.util.List; import java.util.stream.Collectors; -public class CopyPathAction implements BrowserAction, BrowserBranchAction { +public class CopyPathMenuProvider implements BrowserMenuBranchProvider { @Override - public Category getCategory() { - return Category.COPY_PASTE; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.COPY_PASTE; } @Override @@ -37,9 +37,15 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { } @Override - public List getBranchingActions(BrowserFileSystemTabModel model, List entries) { + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2c-content-copy"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { return List.of( - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public KeyCombination getShortcut() { return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN); @@ -49,7 +55,7 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { if (entries.size() == 1) { - return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis( + return new SimpleObjectProperty<>(centerEllipsis( entries.getFirst() .getRawFileEntry() .getPath() @@ -68,12 +74,12 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { ClipboardHelper.copyText(s); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { if (entries.size() == 1) { - return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis( + return new SimpleObjectProperty<>(centerEllipsis( entries.getFirst() .getRawFileEntry() .getPath() @@ -104,13 +110,13 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { ClipboardHelper.copyText(s); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { if (entries.size() == 1) { return new SimpleObjectProperty<>("\"" - + BrowserActionFormatter.centerEllipsis( + + centerEllipsis( entries.getFirst() .getRawFileEntry() .getPath() @@ -138,7 +144,7 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { ClipboardHelper.copyText(s); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public KeyCombination getShortcut() { return new KeyCodeCombination( @@ -149,7 +155,7 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { if (entries.size() == 1) { - return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis( + return new SimpleObjectProperty<>(centerEllipsis( entries.getFirst() .getRawFileEntry() .getPath() @@ -168,12 +174,12 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { ClipboardHelper.copyText(s); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { if (entries.size() == 1) { - return new SimpleObjectProperty<>(BrowserActionFormatter.centerEllipsis( + return new SimpleObjectProperty<>(centerEllipsis( entries.getFirst() .getRawFileEntry() .getPath() @@ -211,13 +217,13 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { ClipboardHelper.copyText(s); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { if (entries.size() == 1) { return new SimpleObjectProperty<>("\"" - + BrowserActionFormatter.centerEllipsis( + + centerEllipsis( entries.getFirst() .getRawFileEntry() .getPath() @@ -247,4 +253,17 @@ public class CopyPathAction implements BrowserAction, BrowserBranchAction { } }); } + + private static String centerEllipsis(String input, int length) { + if (input == null) { + return ""; + } + + if (input.length() <= length) { + return input; + } + + var half = (length / 2) - 5; + return input.substring(0, half) + " ... " + input.substring(input.length() - half); + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java new file mode 100644 index 000000000..c4dd10ee2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java @@ -0,0 +1,66 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.action.AbstractAction; +import io.xpipe.app.browser.action.impl.DeleteActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.store.FileKind; + +import javafx.beans.value.ObservableValue; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; + +import java.util.List; + +public class DeleteMenuProvider implements BrowserMenuLeafProvider { + + @Override + public AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var link = entries.stream() + .anyMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK); + var files = entries.stream() + .map(browserEntry -> !link + ? browserEntry.getRawFileEntry().resolved().getPath() + : browserEntry.getRawFileEntry().getPath()) + .toList(); + var builder = DeleteActionProvider.Action.builder(); + builder.initFiles(model, files); + return builder.build(); + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2d-delete"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.MUTATION; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.DELETE); + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable( + "deleteFile", + entries.stream() + .anyMatch(browserEntry -> + browserEntry.getRawFileEntry().getKind() == FileKind.LINK) + ? "link" + : ""); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/DownloadAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java similarity index 73% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/DownloadAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java index fdf26341a..0520bf92b 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/DownloadAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java @@ -1,22 +1,21 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; import io.xpipe.app.browser.BrowserFullSessionModel; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class DownloadAction implements BrowserLeafAction { +public class DownloadMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -33,13 +32,13 @@ public class DownloadAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2d-download"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2d-download"); } @Override - public Category getCategory() { - return Category.MUTATION; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.ACTION; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java similarity index 76% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java index 57d31d6b9..7e10f398f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/EditFileAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java @@ -1,24 +1,23 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileOpener; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class EditFileAction implements BrowserLeafAction { +public class EditFileMenuProvider implements BrowserMenuLeafProvider { @Override public KeyCombination getShortcut() { @@ -33,13 +32,13 @@ public class EditFileAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2p-pencil"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2p-pencil"); } @Override - public Category getCategory() { - return Category.OPEN; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java similarity index 69% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java index 4dbfdd3d2..bff4367c1 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java @@ -1,19 +1,18 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; - -import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; -public class FollowLinkAction implements BrowserLeafAction { +public class FollowLinkMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -22,13 +21,13 @@ public class FollowLinkAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2a-arrow-top-right-thick"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2a-arrow-top-right-thick"); } @Override - public Category getCategory() { - return Category.OPEN; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java similarity index 76% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java index bd157528c..415994cee 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java @@ -1,21 +1,19 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class ForwardAction implements BrowserLeafAction { +public class ForwardMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -27,8 +25,8 @@ public class ForwardAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("fth-arrow-right"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("fth-arrow-right"); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java similarity index 54% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java index b7ee10f6b..ca8e902df 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java @@ -1,9 +1,12 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserActionFormatter; 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.menu.BrowserApplicationPathMenuProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.FileTypeMenuProvider; +import io.xpipe.app.browser.menu.MultiExecuteMenuProvider; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; @@ -12,21 +15,23 @@ import javafx.beans.value.ObservableValue; import java.util.List; -public class JarAction extends MultiExecuteAction implements JavaAction, FileTypeAction { +public class JarMenuProvider extends MultiExecuteMenuProvider + implements BrowserApplicationPathMenuProvider, FileTypeMenuProvider { @Override - public Category getCategory() { - return Category.CUSTOM; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; } @Override public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { - return new SimpleStringProperty("java -jar " + BrowserActionFormatter.filesArgument(entries)); + var arg = entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")"; + return new SimpleStringProperty("java -jar " + arg); } @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries); + return super.isApplicable(model, entries) && FileTypeMenuProvider.super.isApplicable(model, entries); } @Override @@ -40,4 +45,9 @@ public class JarAction extends MultiExecuteAction implements JavaAction, FileTyp public BrowserIconFileType getType() { return BrowserIconFileType.byId("jar"); } + + @Override + public String getExecutable() { + return "java"; + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/JavapMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/JavapMenuProvider.java new file mode 100644 index 000000000..0f95058cc --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/JavapMenuProvider.java @@ -0,0 +1,61 @@ +package io.xpipe.app.browser.menu.impl; + +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.menu.BrowserApplicationPathMenuProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.browser.menu.FileTypeMenuProvider; +import io.xpipe.app.util.FileOpener; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.ShellControl; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class JavapMenuProvider + implements FileTypeMenuProvider, BrowserApplicationPathMenuProvider, BrowserMenuLeafProvider { + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + var arg = entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")"; + return new SimpleStringProperty("javap -c -p " + arg); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return FileTypeMenuProvider.super.isApplicable(model, entries); + } + + @Override + public BrowserIconFileType getType() { + return BrowserIconFileType.byId("class"); + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) throws Exception { + ShellControl sc = model.getFileSystem().getShell().orElseThrow(); + for (BrowserEntry entry : entries) { + var command = CommandBuilder.of() + .add("javap", "-c", "-p") + .addFile(entry.getRawFileEntry().getPath()); + var out = sc.command(command) + .withWorkingDirectory(model.getCurrentDirectory().getPath()) + .readStdoutOrThrow(); + FileOpener.openReadOnlyString(out); + } + } + + @Override + public String getExecutable() { + return "java"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java similarity index 59% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java index c2cb05190..920c15c19 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java @@ -1,37 +1,38 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserAction; -import io.xpipe.app.browser.action.BrowserBranchAction; -import io.xpipe.app.browser.action.BrowserLeafAction; +import io.xpipe.app.browser.action.impl.NewDirectoryActionProvider; +import io.xpipe.app.browser.action.impl.NewFileActionProvider; +import io.xpipe.app.browser.action.impl.NewLinkActionProvider; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.browser.icon.BrowserIcons; +import io.xpipe.app.browser.menu.BrowserMenuBranchProvider; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FilePath; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.control.TextField; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class NewItemAction implements BrowserAction, BrowserBranchAction { +public class NewItemMenuProvider implements BrowserMenuBranchProvider { @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2p-plus-box-outline"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2p-plus-box-outline"); } @Override - public Category getCategory() { - return Category.MUTATION; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.ACTION; } @Override @@ -45,9 +46,10 @@ public class NewItemAction implements BrowserAction, BrowserBranchAction { } @Override - public List getBranchingActions(BrowserFileSystemTabModel model, List entries) { + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { return List.of( - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public void execute(BrowserFileSystemTabModel model, List entries) { var name = new SimpleStringProperty(); @@ -60,14 +62,21 @@ public class NewItemAction implements BrowserAction, BrowserBranchAction { }) .prefWidth(350)); modal.withDefaultButtons(() -> { - model.createFileAsync(name.getValue()); + if (name.getValue() == null || name.getValue().isEmpty()) { + return; + } + + var builder = NewFileActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.name(name.getValue()); + builder.build().executeAsync(); }); modal.show(); } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return BrowserIcons.createDefaultFileIcon().createRegion(); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon()); } @Override @@ -76,7 +85,7 @@ public class NewItemAction implements BrowserAction, BrowserBranchAction { return AppI18n.observable("file"); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public void execute(BrowserFileSystemTabModel model, List entries) { var name = new SimpleStringProperty(); @@ -89,14 +98,21 @@ public class NewItemAction implements BrowserAction, BrowserBranchAction { }) .prefWidth(350)); modal.withDefaultButtons(() -> { - model.createDirectoryAsync(name.getValue()); + if (name.getValue() == null || name.getValue().isEmpty()) { + return; + } + + var builder = NewDirectoryActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.name(name.getValue()); + builder.build().executeAsync(); }); modal.show(); } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return BrowserIcons.createDefaultDirectoryIcon().createRegion(); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultDirectoryIcon()); } @Override @@ -105,7 +121,7 @@ public class NewItemAction implements BrowserAction, BrowserBranchAction { return AppI18n.observable("directory"); } }, - new BrowserLeafAction() { + new BrowserMenuLeafProvider() { @Override public void execute(BrowserFileSystemTabModel model, List entries) { var linkName = new SimpleStringProperty(); @@ -120,14 +136,25 @@ public class NewItemAction implements BrowserAction, BrowserBranchAction { .buildComp() .prefWidth(350)); modal.withDefaultButtons(() -> { - model.createLinkAsync(linkName.getValue(), FilePath.of(target.getValue())); + if (linkName.getValue() == null + || linkName.getValue().isEmpty() + || target.getValue() == null + || target.getValue().isEmpty()) { + return; + } + + var builder = NewLinkActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.name(linkName.getValue()); + builder.target(FilePath.of(target.getValue())); + builder.build().executeAsync(); }); modal.show(); } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return BrowserIcons.createDefaultFileIcon().createRegion(); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon()); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java similarity index 74% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java index df82f08f0..058e64643 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java @@ -1,23 +1,22 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; import io.xpipe.app.browser.BrowserFullSessionModel; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class OpenDirectoryInNewTabAction implements BrowserLeafAction { +public class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -28,13 +27,13 @@ public class OpenDirectoryInNewTabAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2f-folder-open-outline"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-folder-open-outline"); } @Override - public Category getCategory() { - return Category.OPEN; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java new file mode 100644 index 000000000..39504cf13 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryMenuProvider.java @@ -0,0 +1,45 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.impl.OpenDirectoryActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; + +import javafx.beans.value.ObservableValue; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; + +import java.util.List; + +public class OpenDirectoryMenuProvider implements BrowserMenuLeafProvider { + + @Override + public Class getDelegateActionProvider() { + return OpenDirectoryActionProvider.class; + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2f-folder-open"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("open"); + } +} 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 new file mode 100644 index 000000000..bd3c64087 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileDefaultMenuProvider.java @@ -0,0 +1,45 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.impl.OpenFileDefaultActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; + +import javafx.beans.value.ObservableValue; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; + +import java.util.List; + +public class OpenFileDefaultMenuProvider implements BrowserMenuLeafProvider { + + @Override + public Class getDelegateActionProvider() { + return OpenFileDefaultActionProvider.class; + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2b-book-open-variant"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("openWithDefaultApplication"); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java similarity index 58% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java index d6e68d406..070971483 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileWithAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java @@ -1,39 +1,38 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.impl.OpenFileWithActionProvider; import io.xpipe.app.browser.file.BrowserEntry; -import io.xpipe.app.browser.file.BrowserFileOpener; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class OpenFileWithAction implements BrowserLeafAction { +public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider { @Override - public void execute(BrowserFileSystemTabModel model, List entries) { - var e = entries.getFirst(); - BrowserFileOpener.openWithAnyApplication(model, e.getRawFileEntry()); + public Class getDelegateActionProvider() { + return OpenFileWithActionProvider.class; } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2b-book-open-page-variant-outline"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2b-book-open-page-variant-outline"); } @Override - public Category getCategory() { - return Category.OPEN; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; } @Override 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 new file mode 100644 index 000000000..a02575c63 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java @@ -0,0 +1,44 @@ +package io.xpipe.app.browser.menu.impl; + +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.impl.OpenFileNativeDetailsActionProvider; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; + +import javafx.beans.value.ObservableValue; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; + +import java.util.List; + +public class OpenNativeFileDetailsMenuProvider implements BrowserMenuLeafProvider { + + @Override + public Class getDelegateActionProvider() { + return OpenFileNativeDetailsActionProvider.class; + } + + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; + } + + @Override + public boolean acceptsEmptySelection() { + return true; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("showDetails"); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalMenuProvider.java similarity index 72% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalMenuProvider.java index c17cef8d0..b85e9efbc 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalMenuProvider.java @@ -1,25 +1,31 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; +import io.xpipe.app.browser.action.BrowserActionProvider; +import io.xpipe.app.browser.action.impl.OpenTerminalActionProvider; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FilePath; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.Collections; import java.util.List; -public class OpenTerminalAction implements BrowserLeafAction { +public class OpenTerminalMenuProvider implements BrowserMenuLeafProvider { + + @Override + public Class getDelegateActionProvider() { + return OpenTerminalActionProvider.class; + } @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -41,13 +47,13 @@ public class OpenTerminalAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2c-console"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2c-console"); } @Override - public Category getCategory() { - return Category.OPEN; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.OPEN; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java similarity index 81% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java index 1dd377a0a..9f6ae84a9 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java @@ -1,24 +1,23 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserClipboard; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; import io.xpipe.app.browser.file.BrowserFileTransferMode; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class PasteAction implements BrowserLeafAction { +public class PasteMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -44,13 +43,13 @@ public class PasteAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2c-content-paste"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2c-content-paste"); } @Override - public Category getCategory() { - return Category.COPY_PASTE; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.COPY_PASTE; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java similarity index 75% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java index 65190ee60..67963235d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java @@ -1,21 +1,19 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class RefreshDirectoryAction implements BrowserLeafAction { +public class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) throws Exception { @@ -27,8 +25,8 @@ public class RefreshDirectoryAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdmz-refresh"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdmz-refresh"); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java similarity index 67% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java index 4ed6250b5..bec71cf78 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java @@ -1,22 +1,21 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; -import io.xpipe.app.browser.action.BrowserLeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.List; -public class RenameAction implements BrowserLeafAction { +public class RenameMenuProvider implements BrowserMenuLeafProvider { @Override public void execute(BrowserFileSystemTabModel model, List entries) { @@ -24,13 +23,13 @@ public class RenameAction implements BrowserLeafAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2r-rename-box"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2r-rename-box"); } @Override - public Category getCategory() { - return Category.MUTATION; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.MUTATION; } @Override @@ -52,4 +51,9 @@ public class RenameAction implements BrowserLeafAction { public boolean automaticallyResolveLinks() { return false; } + + @Override + public String getId() { + return "renameFile"; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java similarity index 80% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java index 47f41190c..a4972080a 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java @@ -1,8 +1,11 @@ -package io.xpipe.ext.base.browser; +package io.xpipe.app.browser.menu.impl; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.BrowserMenuCategory; +import io.xpipe.app.browser.menu.MultiExecuteMenuProvider; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellControl; @@ -11,14 +14,11 @@ import io.xpipe.core.store.FileEntry; import io.xpipe.core.store.FileKind; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; - -import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; import java.util.stream.Stream; -public class RunAction extends MultiExecuteAction { +public class RunFileMenuProvider extends MultiExecuteMenuProvider { private boolean isExecutable(FileEntry e) { if (e.getKind() != FileKind.FILE) { @@ -54,13 +54,13 @@ public class RunAction extends MultiExecuteAction { } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return new FontIcon("mdi2p-play"); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2p-play"); } @Override - public Category getCategory() { - return Category.CUSTOM; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUntarAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java similarity index 52% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUntarAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java index 5120f29e6..14327bdc8 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUntarAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java @@ -1,34 +1,34 @@ -package io.xpipe.ext.base.browser.compress; +package io.xpipe.app.browser.menu.impl.compress; -import io.xpipe.app.browser.action.BrowserApplicationPathAction; -import io.xpipe.app.browser.action.BrowserLeafAction; +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.core.process.CommandBuilder; -import io.xpipe.core.process.ShellControl; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.store.FilePath; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import java.util.List; -public class BaseUntarAction implements BrowserApplicationPathAction, BrowserLeafAction { +public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider, BrowserMenuLeafProvider { private final boolean gz; private final boolean toDirectory; - public BaseUntarAction(boolean gz, boolean toDirectory) { + public BaseUntarMenuProvider(boolean gz, boolean toDirectory) { this.gz = gz; this.toDirectory = toDirectory; } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return BrowserIcons.createIcon(BrowserIconFileType.byId("zip")).createRegion(); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); } @Override @@ -37,33 +37,17 @@ public class BaseUntarAction implements BrowserApplicationPathAction, BrowserLea } @Override - public void execute(BrowserFileSystemTabModel model, List entries) { - model.runAsync( - () -> { - ShellControl sc = model.getFileSystem().getShell().orElseThrow(); - for (BrowserEntry entry : entries) { - var target = getTarget(entry.getRawFileEntry().getPath()); - var c = CommandBuilder.of().add("tar"); - if (toDirectory) { - c.add("-C").addFile(target); - } - c.add("-x").addIf(gz, "-z").add("-f"); - c.addFile(entry.getRawFileEntry().getPath()); - if (toDirectory) { - model.getFileSystem().mkdirs(target); - } - sc.command(c) - .withWorkingDirectory( - model.getCurrentDirectory().getPath()) - .execute(); - } - }, - true); + public AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var builder = UntarActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.gz(gz); + builder.toDirectory(toDirectory); + return builder.build(); } @Override - public Category getCategory() { - return Category.CUSTOM; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUnzipUnixAction.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java similarity index 51% rename from ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUnzipUnixAction.java rename to app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java index b309f83b4..60e4ef1f3 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/compress/BaseUnzipUnixAction.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java @@ -1,31 +1,32 @@ -package io.xpipe.ext.base.browser.compress; +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.core.process.CommandBuilder; +import io.xpipe.app.util.LabelGraphic; import io.xpipe.core.process.OsType; -import io.xpipe.core.store.FilePath; -import io.xpipe.ext.base.browser.ExecuteApplicationAction; import javafx.beans.value.ObservableValue; -import javafx.scene.Node; import java.util.List; -public abstract class BaseUnzipUnixAction extends ExecuteApplicationAction { +public abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvider, BrowserApplicationPathMenuProvider { private final boolean toDirectory; - public BaseUnzipUnixAction(boolean toDirectory) { + public BaseUnzipUnixMenuProvider(boolean toDirectory) { this.toDirectory = toDirectory; } @Override - public Node getIcon(BrowserFileSystemTabModel model, List entries) { - return BrowserIcons.createIcon(BrowserIconFileType.byId("zip")).createRegion(); + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); } @Override @@ -34,24 +35,16 @@ public abstract class BaseUnzipUnixAction extends ExecuteApplicationAction { } @Override - protected boolean refresh() { - return true; + public AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var builder = UnzipActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.toDirectory(toDirectory); + return builder.build(); } @Override - protected CommandBuilder createCommand(BrowserFileSystemTabModel model, BrowserEntry entry) { - var command = CommandBuilder.of() - .add("unzip", "-o") - .addFile(entry.getRawFileEntry().getPath()); - if (toDirectory) { - command.add("-d").addFile(getTarget(entry.getRawFileEntry().getPath())); - } - return command; - } - - @Override - public Category getCategory() { - return Category.CUSTOM; + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; } @Override @@ -59,14 +52,13 @@ public abstract class BaseUnzipUnixAction extends ExecuteApplicationAction { var sep = model.getFileSystem().getShell().orElseThrow().getOsType().getFileSystemSeparator(); var dir = entries.size() > 1 ? "[...]" - : getTarget(entries.getFirst().getRawFileEntry().getPath()).getFileName() + sep; + : UnzipActionProvider.getTarget( + entries.getFirst().getRawFileEntry().getPath()) + .getFileName() + + sep; return toDirectory ? AppI18n.observable("unzipDirectory", dir) : AppI18n.observable("unzipHere"); } - private FilePath getTarget(FilePath name) { - return FilePath.of(name.toString().replaceAll("\\.zip$", "")); - } - @Override public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { return entries.stream() diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java new file mode 100644 index 000000000..7fae06e57 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java @@ -0,0 +1,63 @@ +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.BrowserMenuCategory; +import io.xpipe.app.browser.menu.BrowserMenuLeafProvider; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.process.OsType; + +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafProvider { + + private final boolean toDirectory; + + public BaseUnzipWindowsActionProvider(boolean toDirectory) { + this.toDirectory = toDirectory; + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip"))); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.CUSTOM; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + var sep = model.getFileSystem().getShell().orElseThrow().getOsType().getFileSystemSeparator(); + var dir = entries.size() > 1 + ? "[...]" + : UnzipActionProvider.getTarget( + entries.getFirst().getRawFileEntry().getPath()) + .getFileName() + + sep; + return toDirectory ? AppI18n.observable("unzipDirectory", dir) : AppI18n.observable("unzipHere"); + } + + @Override + public AbstractAction createAction(BrowserFileSystemTabModel model, List entries) { + var builder = UnzipActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.toDirectory(toDirectory); + return builder.build(); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.stream() + .allMatch(entry -> + entry.getRawFileEntry().getPath().toString().endsWith(".zip")) + && model.getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS); + } +} 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 new file mode 100644 index 000000000..fa1c5a1b2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java @@ -0,0 +1,226 @@ +package io.xpipe.app.browser.menu.impl.compress; + +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileSystemTabModel; +import io.xpipe.app.browser.menu.*; +import io.xpipe.app.comp.Comp; +import io.xpipe.app.comp.base.ModalOverlay; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.util.CommandSupport; +import io.xpipe.app.util.LabelGraphic; +import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileKind; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TextField; + +import java.util.List; + +public class CompressMenuProvider implements BrowserMenuBranchProvider { + + @Override + public void init(BrowserFileSystemTabModel model) throws Exception { + var sc = model.getFileSystem().getShell().orElseThrow(); + + var foundTar = CommandSupport.findProgram(sc, "tar"); + model.getCache().getInstalledApplications().put("tar", foundTar.isPresent()); + + if (sc.getOsType() != OsType.WINDOWS) { + var found = CommandSupport.findProgram(sc, "zip"); + model.getCache().getInstalledApplications().put("zip", found.isPresent()); + } + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return new LabelGraphic.IconGraphic("mdi2a-archive"); + } + + @Override + public BrowserMenuCategory getCategory() { + return BrowserMenuCategory.ACTION; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("compress"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + var ext = List.of("zip", "tar", "tar.gz", "tgz", "rar", "xar"); + if (entries.stream().anyMatch(browserEntry -> ext.stream().anyMatch(s -> browserEntry + .getRawFileEntry() + .getPath() + .toString() + .toLowerCase() + .endsWith("." + s)))) { + return false; + } + + return true; + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + var contentsOptions = + entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() == FileKind.DIRECTORY; + if (contentsOptions) { + return List.of(new BranchProvider(false), new BranchProvider(true)); + } + + return List.of( + new ZipActionProvider(false), + new TarBasedActionProvider(false, false) { + @Override + protected String getExtension() { + return "tar"; + } + }, + new TarBasedActionProvider(false, true) { + + @Override + protected String getExtension() { + return "tar.gz"; + } + }); + } + + private class BranchProvider implements BrowserMenuBranchProvider { + + private final boolean directory; + + private BranchProvider(boolean directory) { + this.directory = directory; + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable(directory ? "excludeRoot" : "includeRoot"); + } + + @Override + public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { + return directory + ? new LabelGraphic.IconGraphic("mdi2f-file-tree") + : new LabelGraphic.IconGraphic("mdi2f-file-outline"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List entries) { + return List.of( + new ZipActionProvider(directory), + new TarBasedActionProvider(directory, false) { + @Override + protected String getExtension() { + return "tar"; + } + }, + new TarBasedActionProvider(directory, true) { + + @Override + protected String getExtension() { + return "tar.gz"; + } + }); + } + } + + private abstract static class LeafProvider implements BrowserMenuLeafProvider { + + protected final boolean directory; + + private LeafProvider(boolean directory) { + this.directory = directory; + } + + @Override + public void execute(BrowserFileSystemTabModel model, List entries) { + var name = new SimpleStringProperty(directory ? entries.getFirst().getFileName() : null); + var modal = ModalOverlay.of( + "base.archiveName", + Comp.of(() -> { + var creationName = new TextField(); + creationName.textProperty().bindBidirectional(name); + return creationName; + }) + .prefWidth(350)); + modal.withDefaultButtons(() -> { + var fixedName = name.getValue(); + if (fixedName == null) { + return; + } + + if (!fixedName.endsWith(getExtension())) { + fixedName = fixedName + "." + getExtension(); + } + + create(fixedName, model, entries); + }); + modal.show(); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return new SimpleStringProperty("." + getExtension()); + } + + protected abstract void create(String fileName, BrowserFileSystemTabModel model, List entries); + + protected abstract String getExtension(); + } + + private class ZipActionProvider extends LeafProvider { + + private ZipActionProvider(boolean directory) { + super(directory); + } + + @Override + protected void create(String fileName, BrowserFileSystemTabModel model, List entries) { + var builder = io.xpipe.app.browser.menu.impl.compress.ZipActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.target(model.getCurrentDirectory().getPath().join(fileName)); + builder.directoryContentOnly(directory); + builder.build().executeAsync(); + } + + @Override + protected String getExtension() { + return "zip"; + } + } + + private abstract class TarBasedActionProvider extends LeafProvider { + + private final boolean gz; + + private TarBasedActionProvider(boolean directory, boolean gz) { + super(directory); + this.gz = gz; + } + + @Override + protected void create(String fileName, BrowserFileSystemTabModel model, List entries) { + var builder = TarActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.target(model.getCurrentDirectory().getPath().join(fileName)); + builder.directoryContentOnly(directory); + builder.gz(gz); + builder.build().executeAsync(); + } + + @Override + public boolean isActive(BrowserFileSystemTabModel model, List entries) { + return model.getCache().getInstalledApplications().get("tar"); + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS || !directory; + } + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java new file mode 100644 index 000000000..d2a0d292b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java @@ -0,0 +1,68 @@ +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.core.process.CommandBuilder; +import io.xpipe.core.store.FilePath; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class TarActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + private final FilePath target; + + private final boolean directoryContentOnly; + + private final boolean gz; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + var sc = model.getFileSystem().getShell().orElseThrow(); + var tar = CommandBuilder.of() + .add("tar", "-c") + .addIf(gz, "-z") + .add("-f") + .addFile(target); + var base = model.getCurrentDirectory().getPath(); + + if (directoryContentOnly) { + var dir = getEntries().getFirst().getRawFileEntry().getPath(); + // Fix for bsd find, remove / + var command = CommandBuilder.of() + .add("find") + .addFile(dir.removeTrailingSlash().toUnix()) + .add("|", "sed") + .addLiteral("s,^" + dir.toDirectory().toUnix() + "*,,") + .add("|"); + command.add(tar).add("-C").addFile(dir.toDirectory().toUnix()).add("-T", "-"); + sc.command(command).execute(); + } else { + var command = CommandBuilder.of().add(tar); + for (BrowserEntry entry : getEntries()) { + var rel = entry.getRawFileEntry().getPath().relativize(base); + command.addFile(rel); + } + sc.command(command).execute(); + } + model.refreshSync(); + } + } + + @Override + public String getId() { + return "tar"; + } +} 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 new file mode 100644 index 000000000..2f77e3421 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java @@ -0,0 +1,59 @@ +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.core.process.CommandBuilder; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FilePath; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class UntarActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + private final boolean gz; + private final boolean toDirectory; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + ShellControl sc = model.getFileSystem().getShell().orElseThrow(); + for (BrowserEntry entry : getEntries()) { + var target = getTarget(entry.getRawFileEntry().getPath()); + var c = CommandBuilder.of().add("tar"); + if (toDirectory) { + c.add("-C").addFile(target); + } + c.add("-x").addIf(gz, "-z").add("-f"); + c.addFile(entry.getRawFileEntry().getPath()); + if (toDirectory) { + model.getFileSystem().mkdirs(target); + } + sc.command(c) + .withWorkingDirectory(model.getCurrentDirectory().getPath()) + .execute(); + } + } + + private FilePath getTarget(FilePath name) { + return FilePath.of(name.toString() + .replaceAll("\\.tar$", "") + .replaceAll("\\.tar.gz$", "") + .replaceAll("\\.tgz$", "")); + } + } + + @Override + public String getId() { + return "untar"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarDirectoryMenuProvider.java new file mode 100644 index 000000000..d10e90348 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarDirectoryMenuProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UntarDirectoryMenuProvider extends BaseUntarMenuProvider { + + public UntarDirectoryMenuProvider() { + super(false, true); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzDirectoryMenuProvider.java new file mode 100644 index 000000000..2ad443fd3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzDirectoryMenuProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UntarGzDirectoryMenuProvider extends BaseUntarMenuProvider { + + public UntarGzDirectoryMenuProvider() { + super(true, true); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzHereMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzHereMenuProvider.java new file mode 100644 index 000000000..ffb565f88 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarGzHereMenuProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UntarGzHereMenuProvider extends BaseUntarMenuProvider { + + public UntarGzHereMenuProvider() { + super(true, false); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarHereMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarHereMenuProvider.java new file mode 100644 index 000000000..625c5c0b2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarHereMenuProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UntarHereMenuProvider extends BaseUntarMenuProvider { + + public UntarHereMenuProvider() { + super(false, false); + } +} 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 new file mode 100644 index 000000000..cc680d1dd --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java @@ -0,0 +1,85 @@ +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.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.process.ShellDialects; +import io.xpipe.core.store.FilePath; + +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class UnzipActionProvider implements BrowserActionProvider { + + public static FilePath getTarget(FilePath name) { + return FilePath.of(name.toString().replaceAll("\\.zip$", "")); + } + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + private final boolean toDirectory; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + var sc = model.getFileSystem().getShell().orElseThrow(); + if (sc.getOsType() == OsType.WINDOWS) { + if (ShellDialects.isPowershell(sc)) { + for (BrowserEntry entry : getEntries()) { + runPowershellCommand(sc, model, entry); + } + } else { + try (var sub = sc.subShell(ShellDialects.POWERSHELL)) { + for (BrowserEntry entry : getEntries()) { + runPowershellCommand(sub, model, entry); + } + } + } + } else { + for (BrowserEntry entry : getEntries()) { + var command = CommandBuilder.of() + .add("unzip", "-o") + .addFile(entry.getRawFileEntry().getPath()); + if (toDirectory) { + command.add("-d") + .addFile(getTarget(entry.getRawFileEntry().getPath())); + } + try (var cc = sc.command(command) + .withWorkingDirectory(model.getCurrentDirectory().getPath()) + .start()) { + cc.discardOrThrow(); + } + } + } + model.refreshSync(); + } + + private void runPowershellCommand(ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry) + throws Exception { + var command = CommandBuilder.of().add("Expand-Archive", "-Force"); + if (toDirectory) { + var target = getTarget(entry.getRawFileEntry().getPath()); + command.add("-DestinationPath").addFile(target); + } + command.add("-Path").addFile(entry.getRawFileEntry().getPath()); + sc.command(command) + .withWorkingDirectory(model.getCurrentDirectory().getPath()) + .execute(); + } + } + + @Override + public String getId() { + return "unzip"; + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryUnixMenuProvider.java new file mode 100644 index 000000000..4c5854889 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryUnixMenuProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UnzipDirectoryUnixMenuProvider extends BaseUnzipUnixMenuProvider { + + public UnzipDirectoryUnixMenuProvider() { + super(true); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryWindowsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryWindowsActionProvider.java new file mode 100644 index 000000000..b34ee0e9e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipDirectoryWindowsActionProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UnzipDirectoryWindowsActionProvider extends BaseUnzipWindowsActionProvider { + + public UnzipDirectoryWindowsActionProvider() { + super(true); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereUnixMenuProvider.java new file mode 100644 index 000000000..68a47617b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereUnixMenuProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UnzipHereUnixMenuProvider extends BaseUnzipUnixMenuProvider { + + public UnzipHereUnixMenuProvider() { + super(false); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereWindowsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereWindowsActionProvider.java new file mode 100644 index 000000000..124de3a47 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipHereWindowsActionProvider.java @@ -0,0 +1,8 @@ +package io.xpipe.app.browser.menu.impl.compress; + +public class UnzipHereWindowsActionProvider extends BaseUnzipWindowsActionProvider { + + public UnzipHereWindowsActionProvider() { + super(false); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java new file mode 100644 index 000000000..7e3b2ef2e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java @@ -0,0 +1,90 @@ +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.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellDialects; +import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FilePath; + +import lombok.NonNull; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +public class ZipActionProvider implements BrowserActionProvider { + + @Jacksonized + @SuperBuilder + public static class Action extends BrowserAction { + + @NonNull + private final FilePath target; + + private final boolean directoryContentOnly; + + @Override + public boolean isMutation() { + return true; + } + + @Override + public void executeImpl() throws Exception { + var sc = model.getFileSystem().getShell().orElseThrow(); + if (sc.getOsType() == OsType.WINDOWS) { + var base = model.getCurrentDirectory().getPath(); + var command = CommandBuilder.of() + .add("Compress-Archive", "-Force", "-DestinationPath") + .addFile(target) + .add("-Path"); + for (int i = 0; i < getEntries().size(); i++) { + var rel = getEntries().get(i).getRawFileEntry().getPath().relativize(base); + if (getEntries().get(i).getRawFileEntry().getKind() == FileKind.DIRECTORY && directoryContentOnly) { + command.addQuoted(rel.toDirectory().toWindows() + "*"); + } else { + command.addFile(rel.toWindows()); + } + if (i != getEntries().size() - 1) { + command.add(","); + } + } + + if (ShellDialects.isPowershell(sc)) { + sc.command(command).withWorkingDirectory(base).execute(); + } else { + try (var sub = sc.subShell(ShellDialects.POWERSHELL)) { + sub.command(command).withWorkingDirectory(base).execute(); + } + } + } else { + var command = CommandBuilder.of().add("zip", "-r", "-"); + for (BrowserEntry entry : getEntries()) { + var base = target.getParent(); + var rel = entry.getRawFileEntry().getPath().relativize(base).toUnix(); + if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY && directoryContentOnly) { + command.add("."); + } else { + command.addFile(rel); + } + } + command.add(">").addFile(target); + + if (directoryContentOnly) { + sc.command(command) + .withWorkingDirectory( + getEntries().getFirst().getRawFileEntry().getPath()) + .execute(); + } else { + sc.command(command).execute(); + } + } + model.refreshSync(); + } + } + + @Override + public String getId() { + return "zip"; + } +} 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 0a27668a5..e4340270d 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 @@ -2,8 +2,8 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; -import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.PlatformThread; diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java index f0b529e16..76a1e7a73 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java @@ -2,28 +2,27 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.SimpleComp; -import io.xpipe.app.core.AppFontSizes; -import io.xpipe.app.core.AppProperties; +import io.xpipe.app.core.*; import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.core.window.AppMainWindow; import io.xpipe.app.issue.TrackEvent; -import io.xpipe.app.resources.AppImages; -import io.xpipe.app.resources.AppResources; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.PlatformThread; -import io.xpipe.core.process.OsType; -import javafx.animation.Animation; +import javafx.animation.*; +import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.ListChangeListener; +import javafx.css.PseudoClass; import javafx.geometry.Pos; +import javafx.scene.effect.DropShadow; import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.stage.Window; -import atlantafx.base.util.Animations; - public class AppMainWindowContentComp extends SimpleComp { private final Stage stage; @@ -35,30 +34,58 @@ public class AppMainWindowContentComp extends SimpleComp { @Override protected Region createSimple() { var overlay = AppDialog.getModalOverlays(); - var loaded = AppMainWindow.getLoadedContent(); + var loaded = AppMainWindow.getInstance().getLoadedContent(); + var sidebarPresent = new SimpleBooleanProperty(); var bg = Comp.of(() -> { var loadingIcon = new ImageView(); - loadingIcon.setFitWidth(64); - loadingIcon.setFitHeight(64); + loadingIcon.setFitWidth(80); + loadingIcon.setFitHeight(80); - var anim = Animations.pulse(loadingIcon, 1.1); - if (OsType.getLocal() != OsType.LINUX) { - anim.setRate(0.85); - anim.setCycleCount(Animation.INDEFINITE); - anim.play(); - } + var color = + AppPrefs.get() != null && AppPrefs.get().theme().getValue().isDark() + ? Color.web("#0b898aff").darker() + : Color.web("#0b898aff"); + DropShadow shadow = new DropShadow(); + shadow.setRadius(10); + shadow.setColor(color); + + var loadingAnimation = new AnimationTimer() { + + long offset; + + @Override + public void handle(long now) { + // Increment offset as we are always having 60fps + // Prevents animation jumps when the animation timer isn't called for a long time + offset += 1000 / 60; + + // Move shadow in a circle + var rad = -(offset % 1000.0) / 1000.0 * 2 * Math.PI; + var x = Math.sin(rad); + var y = Math.cos(rad); + shadow.setOffsetX(x * 3); + shadow.setOffsetY(y * 3); + } + }; + + loadingIcon.setEffect(shadow); + loadingAnimation.start(); // This allows for assigning logos even if AppImages has not been initialized yet var dir = "img/logo/"; AppResources.with(AppResources.XPIPE_MODULE, dir, path -> { - loadingIcon.setImage(AppImages.loadImage(path.resolve("loading.png"))); + var image = AppPrefs.get() != null + && AppPrefs.get().theme().getValue().isDark() + ? path.resolve("loading-dark.png") + : path.resolve("loading.png"); + loadingIcon.setImage(AppImages.loadImage(image)); }); var version = new LabelComp((AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " " + AppProperties.get().getVersion()); version.apply(struc -> { - AppFontSizes.xxl(struc.get()); - struc.get().setOpacity(0.6); + AppFontSizes.apply(struc.get(), appFontSizes -> "15"); + struc.get().setOpacity(0.65); }); var text = new LabelComp(AppMainWindow.getLoadingText()); @@ -84,12 +111,13 @@ public class AppMainWindowContentComp extends SimpleComp { if (struc != null) { TrackEvent.info("Window content node set"); PlatformThread.runNestedLoopIteration(); - anim.stop(); struc.prepareAddition(); pane.getChildren().add(struc.get()); + sidebarPresent.set(true); PlatformThread.runNestedLoopIteration(); pane.getStyleClass().remove("background"); pane.getChildren().remove(vbox); + loadingAnimation.stop(); struc.show(); TrackEvent.info("Window content node shown"); } @@ -111,7 +139,18 @@ public class AppMainWindowContentComp extends SimpleComp { return pane; }); + var modal = new ModalOverlayStackComp(bg, overlay); - return modal.createRegion(); + var r = modal.createRegion(); + var p = r.lookupAll(".modal-overlay-stack-element"); + sidebarPresent.subscribe(v -> { + if (v) { + p.forEach(node -> { + node.pseudoClassStateChanged(PseudoClass.getPseudoClass("loaded"), true); + }); + } + }); + + return r; } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java index 5c44efe63..4983c22f7 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java @@ -35,12 +35,6 @@ public class ChoiceComp extends Comp>> { this.includeNone = includeNone; } - public ChoiceComp(Property value, ObservableValue>> range, boolean includeNone) { - this.value = value; - this.range = PlatformThread.sync(range); - this.includeNone = includeNone; - } - public static ChoiceComp ofTranslatable( Property value, List range, boolean includeNone) { var map = range.stream() 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 0838fec7a..1b191945e 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 @@ -61,6 +61,12 @@ public class ChoicePaneComp extends Comp> { var vbox = new VBox(transformer.apply(cb)); vbox.setFillWidth(true); + vbox.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + vbox.getChildren().getFirst().requestFocus(); + } + }); + cb.prefWidthProperty().bind(vbox.widthProperty()); cb.valueProperty().subscribe(n -> { if (n == null) { 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 629e1fc99..0f8569619 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,10 +5,9 @@ import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.comp.SimpleCompStructure; import io.xpipe.app.core.AppLayoutModel; -import io.xpipe.app.core.window.AppWindowHelper; +import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.ext.ProcessControlProvider; -import io.xpipe.app.ext.ShellStore; -import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.ContextualFileReference; import io.xpipe.app.storage.DataStorageSyncHandler; @@ -73,7 +72,9 @@ public class ContextualFileReferenceChoiceComp extends Comp> var replacement = ProcessControlProvider.get().replace(fileSystem.getValue()); BrowserFileChooserSessionComp.openSingleFile( () -> replacement, - () -> filePath.getValue() != null ? filePath.getValue().getParent() : null, + () -> filePath.getValue() != null + ? filePath.getValue().getParent() + : null, fileStore -> { if (fileStore != null) { filePath.setValue(fileStore.getPath()); @@ -104,34 +105,33 @@ public class ContextualFileReferenceChoiceComp extends Comp> try { var source = currentPath.asLocalPath(); if (!Files.exists(source)) { - ErrorEvent.fromMessage("Unable to resolve local file path " + source).expected().handle(); + ErrorEventFactory.fromMessage("Unable to resolve local file path " + source) + .expected() + .handle(); return; } var target = sync.getTargetLocation().apply(source); - var shouldCopy = AppWindowHelper.showConfirmationAlert( - "confirmGitShareTitle", "confirmGitShareHeader", "confirmGitShareContent"); + var shouldCopy = AppDialog.confirm("confirmGitShare"); if (!shouldCopy) { return; } var handler = DataStorageSyncHandler.getInstance(); - var syncedTarget = handler.addDataFile( - source, target, sync.getPerUser().test(source)); + var syncedTarget = + handler.addDataFile(source, target, sync.getPerUser().test(source)); var pubSource = Path.of(source + ".pub"); if (Files.exists(pubSource)) { var pubTarget = sync.getTargetLocation().apply(pubSource); - handler - .addDataFile( - pubSource, pubTarget, sync.getPerUser().test(pubSource)); + handler.addDataFile(pubSource, pubTarget, sync.getPerUser().test(pubSource)); } Platform.runLater(() -> { filePath.setValue(FilePath.of(syncedTarget)); }); } catch (Exception e) { - ErrorEvent.fromThrowable(e).handle(); + ErrorEventFactory.fromThrowable(e).handle(); } }); gitShareButton.tooltipKey("gitShareFileTooltip"); diff --git a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java deleted file mode 100644 index 524c5ac0e..000000000 --- a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.xpipe.app.comp.base; - -import io.xpipe.app.comp.Comp; -import io.xpipe.app.comp.CompStructure; -import io.xpipe.app.comp.SimpleCompStructure; -import io.xpipe.app.comp.augment.ContextMenuAugment; -import io.xpipe.app.util.ContextMenuHelper; - -import javafx.beans.binding.Bindings; -import javafx.beans.value.ObservableValue; -import javafx.css.Size; -import javafx.css.SizeUnits; -import javafx.scene.control.Button; -import javafx.scene.control.MenuItem; - -import org.kordamp.ikonli.javafx.FontIcon; - -import java.util.List; - -public class DropdownComp extends Comp> { - - private final List> items; - - public DropdownComp(List> items) { - this.items = items; - } - - @Override - public CompStructure