From 227bcb80150e695f3195e9f3a9d40ca03c8865d0 Mon Sep 17 00:00:00 2001 From: crschnick Date: Wed, 27 Sep 2023 00:47:51 +0000 Subject: [PATCH] Merge branch acc into master --- CONTRIBUTING.md | 37 + DEVELOPMENT.md | 15 +- FAQ.md | 94 +- PRIVACY.md | 287 +- README.md | 25 +- .../api/connector/XPipeApiConnection.java | 2 +- app/build.gradle | 7 +- app/gradle_scripts/flexmark.gradle | 24 +- app/gradle_scripts/github-api.gradle | 4 +- app/gradle_scripts/richtextfx.gradle | 12 +- app/gradle_scripts/sentry.gradle | 6 +- .../app/browser/BrowserBookmarkList.java | 245 +- .../xpipe/app/browser/BrowserClipboard.java | 42 + .../io/xpipe/app/browser/BrowserComp.java | 66 +- .../app/browser/BrowserFileListComp.java | 9 +- .../app/browser/BrowserFileListModel.java | 2 +- .../io/xpipe/app/browser/BrowserModel.java | 7 +- .../io/xpipe/app/browser/BrowserNavBar.java | 11 +- .../app/browser/BrowserSelectionListComp.java | 10 +- .../app/browser/BrowserTransferComp.java | 4 +- .../app/browser/BrowserTransferModel.java | 4 +- .../xpipe/app/browser/BrowserWelcomeComp.java | 56 +- .../app/browser/OpenFileSystemModel.java | 26 +- .../xpipe/app/browser/action/LeafAction.java | 6 +- .../xpipe/app/browser/icon/BrowserIcons.java | 21 +- .../app/browser/icon/FileIconManager.java | 27 - .../java/io/xpipe/app/comp/AppLayoutComp.java | 15 +- .../io/xpipe/app/comp/DeveloperTabComp.java | 7 +- .../io/xpipe/app/comp/base/CountComp.java | 12 +- .../io/xpipe/app/comp/base/DropdownComp.java | 68 + .../app/comp/base/FileDropOverlayComp.java | 2 +- .../app/comp/base/LazyTextFieldComp.java | 3 +- .../app/comp/base/LoadingOverlayComp.java | 2 + .../io/xpipe/app/comp/base/MarkdownComp.java | 13 +- .../io/xpipe/app/comp/base/OsLogoComp.java | 6 +- .../xpipe/app/comp/base/SideMenuBarComp.java | 32 +- .../app/comp/storage/DataStoreTypeComp.java | 28 - .../xpipe/app/comp/storage/StorageFilter.java | 56 - .../storage/store/DenseStoreEntryComp.java | 40 +- .../storage/store/StandardStoreEntryComp.java | 2 +- .../storage/store/StoreCategoryWrapper.java | 145 + .../storage/store/StoreCreationBarComp.java | 74 - .../comp/storage/store/StoreCreationMenu.java | 85 + .../comp/storage/store/StoreEntryComp.java | 131 +- .../store/StoreEntryFlatMiniSectionComp.java | 70 - .../storage/store/StoreEntryListComp.java | 17 +- ...rComp.java => StoreEntryListSideComp.java} | 33 +- .../comp/storage/store/StoreEntryTree.java | 44 - .../comp/storage/store/StoreEntryWrapper.java | 57 +- .../comp/storage/store/StoreIntroComp.java | 14 +- .../app/comp/storage/store/StoreSection.java | 161 +- .../comp/storage/store/StoreSectionComp.java | 28 +- .../storage/store/StoreSectionMiniComp.java | 109 + .../comp/storage/store/StoreSidebarComp.java | 12 +- ...ganizationComp.java => StoreSortComp.java} | 20 +- .../app/comp/storage/store/StoreSortMode.java | 2 +- .../comp/storage/store/StoreViewState.java | 172 +- .../app/comp/store/DataStoreSelectorComp.java | 73 - .../app/comp/store/DsFileHistoryComp.java | 61 - .../store/DsLocalDirectoryBrowseComp.java | 67 - .../app/comp/store/DsLocalFileBrowseComp.java | 77 - .../comp/store/DsRemoteFileChoiceComp.java | 42 - .../comp/store/DsStoreProviderChoiceComp.java | 8 +- .../comp/store/DsStreamStoreChoiceComp.java | 187 - .../app/comp/store/GuiDsStoreCreator.java | 30 +- .../app/comp/store/NamedStoreChoiceComp.java | 162 - app/src/main/java/io/xpipe/app/core/App.java | 2 +- .../io/xpipe/app/core/AppAntivirusAlert.java | 3 +- .../xpipe/app/core/AppExtensionManager.java | 29 +- .../java/io/xpipe/app/core/AppGreetings.java | 96 +- .../main/java/io/xpipe/app/core/AppI18n.java | 12 + .../java/io/xpipe/app/core/AppImages.java | 2 +- .../io/xpipe/app/core/AppLayoutModel.java | 17 +- .../java/io/xpipe/app/core/AppMainWindow.java | 2 + .../java/io/xpipe/app/core/AppProperties.java | 21 +- .../java/io/xpipe/app/core/AppResources.java | 4 +- .../main/java/io/xpipe/app/core/AppState.java | 9 + .../main/java/io/xpipe/app/core/AppTheme.java | 50 +- .../main/java/io/xpipe/app/core/AppTray.java | 5 + .../io/xpipe/app/core/AppWindowHelper.java | 22 +- .../java/io/xpipe/app/core/mode/BaseMode.java | 15 +- .../java/io/xpipe/app/core/mode/GuiMode.java | 19 +- .../io/xpipe/app/core/mode/OperationMode.java | 60 +- .../app/exchange/DialogExchangeImpl.java | 2 +- .../app/exchange/LaunchExchangeImpl.java | 16 +- .../java/io/xpipe/app/ext/ActionProvider.java | 27 + .../io/xpipe/app/ext/DataStoreProvider.java | 27 +- .../java/io/xpipe/app/ext/ScanProvider.java | 2 +- .../main/java/io/xpipe/app/fxcomps/Comp.java | 10 +- .../fxcomps/augment/ContextMenuAugment.java | 1 - ...t.java => DragOverPseudoClassAugment.java} | 6 +- .../app/fxcomps/augment/DraggableAugment.java | 61 + .../app/fxcomps/augment/GrowAugment.java | 18 +- .../app/fxcomps/impl/DataStoreChoiceComp.java | 215 +- .../impl/FileSystemStoreChoiceComp.java | 5 +- .../io/xpipe/app/fxcomps/impl/FilterComp.java | 2 +- .../app/fxcomps/impl/IconButtonComp.java | 7 +- .../app/fxcomps/impl/PrettyImageComp.java | 120 +- .../app/fxcomps/impl/PrettyImageHelper.java | 42 + .../xpipe/app/fxcomps/impl/PrettySvgComp.java | 106 + .../io/xpipe/app/fxcomps/impl/SvgCache.java | 12 - .../xpipe/app/fxcomps/impl/SvgCacheComp.java | 134 - .../io/xpipe/app/fxcomps/impl/SvgView.java | 8 +- .../app/fxcomps/util/BindingsHelper.java | 81 + .../java/io/xpipe/app/issue/ErrorAction.java | 7 +- .../io/xpipe/app/issue/ErrorDetailsComp.java | 27 - .../java/io/xpipe/app/issue/ErrorEvent.java | 19 +- .../io/xpipe/app/issue/ErrorHandlerComp.java | 8 +- .../java/io/xpipe/app/issue/EventHandler.java | 50 +- .../io/xpipe/app/issue/EventHandlerImpl.java | 26 +- .../xpipe/app/issue/ExceptionConverter.java | 21 +- .../io/xpipe/app/issue/GuiErrorHandler.java | 31 + .../xpipe/app/issue/GuiErrorHandlerBase.java | 54 + .../xpipe/app/issue/SentryErrorHandler.java | 136 +- .../xpipe/app/issue/TerminalErrorHandler.java | 68 +- .../io/xpipe/app/issue/UserReportComp.java | 43 +- .../io/xpipe/app/launcher/LauncherInput.java | 33 +- .../io/xpipe/app/prefs/AppPreferencesFx.java | 9 +- .../java/io/xpipe/app/prefs/AppPrefs.java | 64 +- .../xpipe/app/prefs/CustomFormRenderer.java | 24 +- .../xpipe/app/prefs/ExternalTerminalType.java | 6 +- .../io/xpipe/app/prefs/UpdateCheckComp.java | 83 +- .../io/xpipe/app/prefs/VaultCategory.java | 65 + .../io/xpipe/app/storage/DataStorage.java | 250 +- .../xpipe/app/storage/DataStoreCategory.java | 132 + .../io/xpipe/app/storage/DataStoreEntry.java | 128 +- .../xpipe/app/storage/GitStorageHandler.java | 28 + .../app/storage/ImpersistentStorage.java | 5 + .../io/xpipe/app/storage/StandardStorage.java | 197 +- .../io/xpipe/app/storage/StorageElement.java | 31 +- .../io/xpipe/app/storage/StorageListener.java | 5 + .../io/xpipe/app/update/AppDownloads.java | 2 +- .../io/xpipe/app/update/AppInstaller.java | 8 +- .../app/update/CommercializationAlert.java | 43 + .../io/xpipe/app/update/UpdateHandler.java | 6 +- .../app/update/XPipeDistributionType.java | 28 +- .../java/io/xpipe/app/util/BooleanScope.java | 62 + .../java/io/xpipe/app/util/BusyProperty.java | 29 - .../xpipe/app/util/CustomComboBoxBuilder.java | 7 + .../app/util/DataStoreCategoryChoiceComp.java | 57 + .../io/xpipe/app/util/DataStoreFormatter.java | 7 +- .../io/xpipe/app/util/DataTypeParser.java | 38 - .../app/util/DataTypeParserInternal.java | 104 - .../java/io/xpipe/app/util/DesktopHelper.java | 8 + .../io/xpipe/app/util/DesktopShortcuts.java | 2 +- .../io/xpipe/app/util/FeatureProvider.java | 46 + .../java/io/xpipe/app/util/FileBridge.java | 17 +- .../java/io/xpipe/app/util/FileOpener.java | 2 +- .../java/io/xpipe/app/util/JfxHelper.java | 5 +- .../java/io/xpipe/app/util/LicenseType.java | 20 + .../io/xpipe/app/util/MarkdownHelper.java | 21 + .../xpipe/app/util/ObservableDataStore.java | 50 + .../java/io/xpipe/app/util/PlatformState.java | 10 + .../app/util/ProxyManagerProviderImpl.java | 2 +- .../java/io/xpipe/app/util/ScanAlert.java | 221 +- .../util/SecretRetrievalStrategyHelper.java | 2 +- .../io/xpipe/app/util/StatefulDataStore.java | 13 + .../java/io/xpipe/app/util/ThreadHelper.java | 14 +- .../java/io/xpipe/app/util/TypeConverter.java | 133 - .../java/io/xpipe/app/util/UserConfig.java | 4 + .../java/io/xpipe/app/util/Validator.java | 2 +- .../java/io/xpipe/app/util/Validators.java | 18 +- app/src/main/java/module-info.java | 5 +- .../io/xpipe/app/resources/img/Hips.svg | 69 + .../io/xpipe/app/resources/img/Wave.svg | 128 + .../io/xpipe/app/resources/img/bg.png | Bin 0 -> 661687 bytes .../resources/lang/dscreation_en.properties | 15 +- .../resources/lang/preferences_en.properties | 12 +- .../resources/lang/translations_en.properties | 26 +- .../app/resources/misc/commercialization.md | 21 + .../io/xpipe/app/resources/misc/eula.md | 95 - .../resources/misc/report_privacy_policy.md | 270 + .../app/resources/misc/storage_readme.md | 13 + .../io/xpipe/app/resources/misc/tos.md | 149 + .../io/xpipe/app/resources/misc/welcome.md | 22 +- .../io/xpipe/app/resources/style/about.css | 20 +- .../io/xpipe/app/resources/style/bookmark.css | 28 + .../io/xpipe/app/resources/style/browser.css | 37 +- .../io/xpipe/app/resources/style/category.css | 31 + .../xpipe/app/resources/style/choice-comp.css | 14 + .../resources/style/error-handler-comp.css | 1 - .../xpipe/app/resources/style/filter-comp.css | 2 +- .../xpipe/app/resources/style/header-bars.css | 89 +- .../xpipe/app/resources/style/popup-menu.css | 37 +- .../io/xpipe/app/resources/style/prefs.css | 9 +- .../app/resources/style/sidebar-comp.css | 4 +- .../style/storage-group-list-comp.css | 1 + .../app/resources/style/store-creator.css | 2 +- .../app/resources/style/store-entry-comp.css | 52 +- .../resources/style/store-mini-section.css | 89 + .../io/xpipe/app/resources/style/style.css | 27 +- .../java/io/xpipe/beacon/BeaconConfig.java | 4 +- .../xpipe/beacon/BeaconDaemonController.java | 2 +- build.gradle | 45 +- .../java/io/xpipe/core/process/OsType.java | 15 - .../io/xpipe/core/process/ShellControl.java | 18 +- .../io/xpipe/core/process/ShellDialect.java | 12 +- .../io/xpipe/core/process/ShellDialects.java | 2 + .../core/store/ConnectionFileSystem.java | 15 +- .../java/io/xpipe/core/store/GroupStore.java | 16 + ...Store.java => InternalCacheDataStore.java} | 2 +- .../java/io/xpipe/core/store/ShellStore.java | 2 +- .../io/xpipe/core/util/CoreJacksonModule.java | 22 + .../core/util/XPipeExecTempDirectory.java | 3 +- .../io/xpipe/core/util/XPipeInstallation.java | 86 +- .../io/xpipe/core/util/XPipeSystemId.java | 8 +- dist/base.gradle | 32 +- dist/build.gradle | 5 +- dist/changelogs/1.5.4.md | 11 - dist/changelogs/1.6.0.md | 31 + dist/jpackage.gradle | 28 +- .../io/xpipe/ext/base/FileStoreProvider.java | 11 - .../java/io/xpipe/ext/base/HttpStore.java | 10 +- .../xpipe/ext/base/action/AddStoreAction.java | 53 - .../action/DeleteStoreChildrenAction.java | 5 + .../ext/base/action/EditStoreAction.java | 5 + .../ext/base/action/GroupToggleAction.java | 42 + .../xpipe/ext/base/action/LaunchAction.java | 87 +- .../ext/base/action/ObserveStoreAction.java | 52 + .../xpipe/ext/base/action/XPipeUrlAction.java | 119 + ext/base/src/main/java/module-info.java | 6 +- .../resources/lang/translations_en.properties | 5 +- ext/collections/build.gradle | 17 - .../collections/ArchiveEntryDataStore.java | 39 - .../ext/collections/ArchiveEntryStore.java | 31 - .../io/xpipe/ext/collections/ArchiveFile.java | 24 - .../ext/collections/ArchiveFileProvider.java | 18 - .../collections/ArchiveReadConnection.java | 91 - .../ext/collections/DirectoryProvider.java | 124 - .../ext/collections/ZipFileProvider.java | 49 - .../src/main/java/module-info.java | 14 - .../resources/extension.properties | 1 - .../ext/collections/resources/img/icon.png | Bin 23849 -> 0 bytes .../collections/resources/img/zip_icon.png | Bin 1899 -> 0 bytes .../resources/lang/translations_de.properties | 3 - .../resources/lang/translations_en.properties | 3 - ext/csv/build.gradle | 15 - ext/csv/docs/index.rst | 13 - .../java/io/xpipe/ext/csv/CsvDelimiter.java | 80 - .../java/io/xpipe/ext/csv/CsvDetector.java | 82 - .../java/io/xpipe/ext/csv/CsvHeaderState.java | 125 - .../java/io/xpipe/ext/csv/CsvQuoteChar.java | 61 - .../java/io/xpipe/ext/csv/CsvQuoteState.java | 67 - .../io/xpipe/ext/csv/CsvReadConnection.java | 127 - .../main/java/io/xpipe/ext/csv/CsvSource.java | 71 - .../io/xpipe/ext/csv/CsvSourceProvider.java | 147 - .../java/io/xpipe/ext/csv/CsvSplitter.java | 57 - .../io/xpipe/ext/csv/CsvWriteConnection.java | 173 - ext/csv/src/main/java/module-info.java | 21 - .../ext/csv/resources/extension.properties | 1 - .../xpipe/ext/csv/resources/img/csv_icon.png | Bin 19591 -> 0 bytes .../resources/lang/translations_en.properties | 18 - .../xpipe/ext/csv/test/CsvDetectorTest.java | 192 - ext/csv/src/test/java/module-info.java | 12 - ext/csv/src/test/resources/airtravel.csv | 14 - ...ey-2020-financial-year-provisional-csv.csv | 37081 ---------------- ext/csv/src/test/resources/empty.csv | 0 ext/csv/src/test/resources/hw_25000.csv | 25001 ----------- ext/csv/src/test/resources/job_status.csv | 9 - .../username-password-recovery-code.csv | 6 - ext/jackson/build.gradle | 24 - .../xpipe/ext/jackson/JacksonConverter.java | 105 - .../xpipe/ext/jackson/json/JsonProvider.java | 152 - .../ext/jackson/json/JsonReadConnection.java | 22 - .../ext/jackson/json/JsonWriteConnection.java | 33 - .../jackson/json_table/JsonTableProvider.java | 171 - .../json_table/JsonTableReadConnection.java | 79 - .../json_table/JsonTableWriteConnection.java | 58 - .../io/xpipe/ext/jackson/xml/XmlProvider.java | 143 - .../ext/jackson/xml/XmlReadConnection.java | 21 - .../ext/jackson/xml/XmlWriteConnection.java | 35 - .../jackson/xml_table/XmlTableProvider.java | 184 - .../xml_table/XmlTableReadConnection.java | 109 - .../xml_table/XmlTableWriteConnection.java | 92 - ext/jackson/src/main/java/module-info.java | 23 - .../jackson/resources/extension.properties | 1 - .../ext/jackson/resources/img/json_icon.png | Bin 7531 -> 0 bytes .../ext/jackson/resources/img/xml_icon.png | Bin 7540 -> 0 bytes .../resources/lang/translations_de.properties | 3 - .../resources/lang/translations_en.properties | 8 - ext/jdbcx/src/main/java/module-info.java | 1 - ext/office/build.gradle | 5 - ext/office/src/main/java/module-info.java | 1 - ext/pdx/build.gradle | 16 - .../io/xpipe/ext/pdx/Eu4FileProvider.java | 61 - .../io/xpipe/ext/pdx/PdxFileProvider.java | 104 - .../io/xpipe/ext/pdx/PdxTextFileProvider.java | 94 - .../ext/pdx/parser/ContextTupleNode.java | 204 - .../ext/pdx/parser/ContextValueNode.java | 44 - .../io/xpipe/ext/pdx/parser/NodeContext.java | 73 - .../xpipe/ext/pdx/parser/ParseException.java | 89 - .../io/xpipe/ext/pdx/parser/StringValues.java | 32 - .../io/xpipe/ext/pdx/parser/TaggedNodes.java | 78 - .../ext/pdx/parser/TextFormatParser.java | 288 - .../ext/pdx/parser/TextFormatTokenizer.java | 363 - .../ext/pdx/parser/TupleNodeBuilder.java | 91 - .../Ck3CompressedSavegameStructure.java | 91 - .../io/xpipe/ext/pdx/savegame/Ck3Header.java | 68 - .../Ck3PlaintextSavegameStructure.java | 58 - .../io/xpipe/ext/pdx/savegame/NodeWriter.java | 55 - .../ext/pdx/savegame/NodeWriterImpl.java | 92 - .../savegame/PlaintextSavegameStructure.java | 51 - .../ext/pdx/savegame/SavegameContent.java | 32 - .../ext/pdx/savegame/SavegameParseResult.java | 109 - .../ext/pdx/savegame/SavegameStructure.java | 91 - .../xpipe/ext/pdx/savegame/SavegameType.java | 282 - .../pdx/savegame/ZipSavegameStructure.java | 127 - ext/pdx/src/main/java/module-info.java | 16 - .../ext/pdx/resources/extension.properties | 1 - .../xpipe/ext/pdx/resources/img/eu4_icon.png | Bin 112188 -> 0 bytes .../ext/pdx/resources/img/pdxText_icon.png | Bin 49947 -> 0 bytes .../resources/lang/translations_de.properties | 3 - .../resources/lang/translations_en.properties | 6 - ext/procx/build.gradle | 5 - ext/procx/src/main/java/module-info.java | 1 - ext/{jdbcx => uacc}/build.gradle | 0 ext/uacc/src/main/java/module-info.java | 1 + get-xpipe.ps1 | 6 +- get-xpipe.sh | 0 gradle/gradle_scripts/commons.gradle | 34 +- gradle/gradle_scripts/extension_test.gradle | 8 +- gradle/gradle_scripts/java.gradle | 9 +- gradle/gradle_scripts/javafx.gradle | 12 +- gradle/gradle_scripts/junit.gradle | 2 +- gradle/gradle_scripts/lombok.gradle | 8 +- gradle/gradle_scripts/prettytime.gradle | 4 +- gradle/gradle_scripts/versioncompare.gradle | 4 +- settings.gradle | 8 + setup.sh | 6 +- version | 2 +- 330 files changed, 5406 insertions(+), 71419 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/storage/DataStoreTypeComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/storage/StorageFilter.java create mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationBarComp.java create mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationMenu.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryFlatMiniSectionComp.java rename app/src/main/java/io/xpipe/app/comp/storage/store/{StoreEntryListHeaderComp.java => StoreEntryListSideComp.java} (60%) delete mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java create mode 100644 app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionMiniComp.java rename app/src/main/java/io/xpipe/app/comp/storage/store/{StoreOrganizationComp.java => StoreSortComp.java} (85%) delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/DataStoreSelectorComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/DsFileHistoryComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/DsLocalDirectoryBrowseComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/DsLocalFileBrowseComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/DsRemoteFileChoiceComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/DsStreamStoreChoiceComp.java delete mode 100644 app/src/main/java/io/xpipe/app/comp/store/NamedStoreChoiceComp.java rename app/src/main/java/io/xpipe/app/fxcomps/augment/{DragPseudoClassAugment.java => DragOverPseudoClassAugment.java} (73%) create mode 100644 app/src/main/java/io/xpipe/app/fxcomps/augment/DraggableAugment.java create mode 100644 app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageHelper.java create mode 100644 app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java delete mode 100644 app/src/main/java/io/xpipe/app/fxcomps/impl/SvgCache.java delete mode 100644 app/src/main/java/io/xpipe/app/fxcomps/impl/SvgCacheComp.java create mode 100644 app/src/main/java/io/xpipe/app/issue/GuiErrorHandler.java create mode 100644 app/src/main/java/io/xpipe/app/issue/GuiErrorHandlerBase.java create mode 100644 app/src/main/java/io/xpipe/app/prefs/VaultCategory.java create mode 100644 app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java create mode 100644 app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java create mode 100644 app/src/main/java/io/xpipe/app/update/CommercializationAlert.java create mode 100644 app/src/main/java/io/xpipe/app/util/BooleanScope.java delete mode 100644 app/src/main/java/io/xpipe/app/util/BusyProperty.java create mode 100644 app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java delete mode 100644 app/src/main/java/io/xpipe/app/util/DataTypeParser.java delete mode 100644 app/src/main/java/io/xpipe/app/util/DataTypeParserInternal.java create mode 100644 app/src/main/java/io/xpipe/app/util/FeatureProvider.java create mode 100644 app/src/main/java/io/xpipe/app/util/LicenseType.java create mode 100644 app/src/main/java/io/xpipe/app/util/MarkdownHelper.java create mode 100644 app/src/main/java/io/xpipe/app/util/ObservableDataStore.java create mode 100644 app/src/main/java/io/xpipe/app/util/StatefulDataStore.java delete mode 100644 app/src/main/java/io/xpipe/app/util/TypeConverter.java create mode 100644 app/src/main/java/io/xpipe/app/util/UserConfig.java create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/Hips.svg create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/Wave.svg create mode 100644 app/src/main/resources/io/xpipe/app/resources/img/bg.png create mode 100644 app/src/main/resources/io/xpipe/app/resources/misc/commercialization.md delete mode 100644 app/src/main/resources/io/xpipe/app/resources/misc/eula.md create mode 100644 app/src/main/resources/io/xpipe/app/resources/misc/report_privacy_policy.md create mode 100644 app/src/main/resources/io/xpipe/app/resources/misc/storage_readme.md create mode 100644 app/src/main/resources/io/xpipe/app/resources/misc/tos.md create mode 100644 app/src/main/resources/io/xpipe/app/resources/style/bookmark.css create mode 100644 app/src/main/resources/io/xpipe/app/resources/style/category.css create mode 100644 app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css create mode 100644 app/src/main/resources/io/xpipe/app/resources/style/store-mini-section.css create mode 100644 core/src/main/java/io/xpipe/core/store/GroupStore.java rename core/src/main/java/io/xpipe/core/store/{StatefulDataStore.java => InternalCacheDataStore.java} (91%) delete mode 100644 dist/changelogs/1.5.4.md create mode 100644 dist/changelogs/1.6.0.md delete mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/AddStoreAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/GroupToggleAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/ObserveStoreAction.java create mode 100644 ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java delete mode 100644 ext/collections/build.gradle delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/ArchiveEntryDataStore.java delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/ArchiveEntryStore.java delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/ArchiveFile.java delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/ArchiveFileProvider.java delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/ArchiveReadConnection.java delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/DirectoryProvider.java delete mode 100644 ext/collections/src/main/java/io/xpipe/ext/collections/ZipFileProvider.java delete mode 100644 ext/collections/src/main/java/module-info.java delete mode 100644 ext/collections/src/main/resources/io/xpipe/ext/collections/resources/extension.properties delete mode 100644 ext/collections/src/main/resources/io/xpipe/ext/collections/resources/img/icon.png delete mode 100644 ext/collections/src/main/resources/io/xpipe/ext/collections/resources/img/zip_icon.png delete mode 100644 ext/collections/src/main/resources/io/xpipe/ext/collections/resources/lang/translations_de.properties delete mode 100644 ext/collections/src/main/resources/io/xpipe/ext/collections/resources/lang/translations_en.properties delete mode 100644 ext/csv/build.gradle delete mode 100644 ext/csv/docs/index.rst delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvDelimiter.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvDetector.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvHeaderState.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvQuoteChar.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvQuoteState.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvReadConnection.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvSource.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvSourceProvider.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvSplitter.java delete mode 100644 ext/csv/src/main/java/io/xpipe/ext/csv/CsvWriteConnection.java delete mode 100644 ext/csv/src/main/java/module-info.java delete mode 100644 ext/csv/src/main/resources/io/xpipe/ext/csv/resources/extension.properties delete mode 100644 ext/csv/src/main/resources/io/xpipe/ext/csv/resources/img/csv_icon.png delete mode 100644 ext/csv/src/main/resources/io/xpipe/ext/csv/resources/lang/translations_en.properties delete mode 100644 ext/csv/src/test/java/io/xpipe/ext/csv/test/CsvDetectorTest.java delete mode 100644 ext/csv/src/test/java/module-info.java delete mode 100644 ext/csv/src/test/resources/airtravel.csv delete mode 100644 ext/csv/src/test/resources/annual-enterprise-survey-2020-financial-year-provisional-csv.csv delete mode 100644 ext/csv/src/test/resources/empty.csv delete mode 100644 ext/csv/src/test/resources/hw_25000.csv delete mode 100644 ext/csv/src/test/resources/job_status.csv delete mode 100644 ext/csv/src/test/resources/username-password-recovery-code.csv delete mode 100644 ext/jackson/build.gradle delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/JacksonConverter.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/json/JsonProvider.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/json/JsonReadConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/json/JsonWriteConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/json_table/JsonTableProvider.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/json_table/JsonTableReadConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/json_table/JsonTableWriteConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/xml/XmlProvider.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/xml/XmlReadConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/xml/XmlWriteConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/xml_table/XmlTableProvider.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/xml_table/XmlTableReadConnection.java delete mode 100644 ext/jackson/src/main/java/io/xpipe/ext/jackson/xml_table/XmlTableWriteConnection.java delete mode 100644 ext/jackson/src/main/java/module-info.java delete mode 100644 ext/jackson/src/main/resources/io/xpipe/ext/jackson/resources/extension.properties delete mode 100644 ext/jackson/src/main/resources/io/xpipe/ext/jackson/resources/img/json_icon.png delete mode 100644 ext/jackson/src/main/resources/io/xpipe/ext/jackson/resources/img/xml_icon.png delete mode 100644 ext/jackson/src/main/resources/io/xpipe/ext/jackson/resources/lang/translations_de.properties delete mode 100644 ext/jackson/src/main/resources/io/xpipe/ext/jackson/resources/lang/translations_en.properties delete mode 100644 ext/jdbcx/src/main/java/module-info.java delete mode 100644 ext/office/build.gradle delete mode 100644 ext/office/src/main/java/module-info.java delete mode 100644 ext/pdx/build.gradle delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/Eu4FileProvider.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/PdxFileProvider.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/PdxTextFileProvider.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/ContextTupleNode.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/ContextValueNode.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/NodeContext.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/ParseException.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/StringValues.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TaggedNodes.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TextFormatParser.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TextFormatTokenizer.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/parser/TupleNodeBuilder.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/Ck3CompressedSavegameStructure.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/Ck3Header.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/Ck3PlaintextSavegameStructure.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/NodeWriter.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/NodeWriterImpl.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/PlaintextSavegameStructure.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/SavegameContent.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/SavegameParseResult.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/SavegameStructure.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/SavegameType.java delete mode 100644 ext/pdx/src/main/java/io/xpipe/ext/pdx/savegame/ZipSavegameStructure.java delete mode 100644 ext/pdx/src/main/java/module-info.java delete mode 100644 ext/pdx/src/main/resources/io/xpipe/ext/pdx/resources/extension.properties delete mode 100644 ext/pdx/src/main/resources/io/xpipe/ext/pdx/resources/img/eu4_icon.png delete mode 100644 ext/pdx/src/main/resources/io/xpipe/ext/pdx/resources/img/pdxText_icon.png delete mode 100644 ext/pdx/src/main/resources/io/xpipe/ext/pdx/resources/lang/translations_de.properties delete mode 100644 ext/pdx/src/main/resources/io/xpipe/ext/pdx/resources/lang/translations_en.properties delete mode 100644 ext/procx/build.gradle delete mode 100644 ext/procx/src/main/java/module-info.java rename ext/{jdbcx => uacc}/build.gradle (100%) create mode 100644 ext/uacc/src/main/java/module-info.java mode change 100755 => 100644 get-xpipe.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..c1b324af --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributors guide + +If you're interested in contributing to XPipe, you can easily do so! Just submit a pull request with your changes. + +In terms of development environment setup, be sure to read the [development page](https://github.com/xpipe-io/xpipe/blob/master/DEVELOPMENT.md) first. +Especially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement: + +### Implementing support for a new editor + +All code for handling external editors can be found [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java). There you will find plenty of working examples that you can use as a base for your own implementation. + +### Implementing support for a new terminal + +All code for handling external terminals can be found [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java). There you will find plenty of working examples that you can use as a base for your own implementation. + +### Adding more file icons for specific types + +You can register file types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/resources/io/xpipe/app/resources/file_list.txt) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/browser_icons). + +The existing file list and icons are taken from the [vscode-icons](https://github.com/vscode-icons/vscode-icons) project. Due to limitations in the file definition list compatibility, some file types might not be listed by their proper extension and are therefore not being applied correctly even though the images and definitions exist already. + +### Adding more context menu actions in the file browser + +In case you want to implement your own actions for certain file types in the file browser, you can easily do so. You can find most existing actions [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/browser) to get some inspiration. +Once you created your custom classes, you have to register them in your module info, just like [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/module-info.java). + +### Implementing custom actions for the connection hub + +All actions that you can perform for certain connections in the connection overview tab are implemented using an [Action API](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/ext/ActionProvider.java). You can find a sample implementation [here](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java) and many common action implementations [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/action). + +### Familiarising yourself with the shell and command API + +The [sample action](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java) shows the basics of working with shells and executing commands in them. For more references, just look for the usages of the [API classes](https://github.com/xpipe-io/xpipe/tree/master/core/src/main/java/io/xpipe/core/process). + +### Implementing something else + +if you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7f4cb8f5..aee8b5e8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -5,14 +5,14 @@ There are no real formal contribution guidelines right now, they will maybe come ## Repository Structure -- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation +- [core](core) - Shared core classes of the XPipe Java API, XPipe extensions, and the XPipe daemon implementation. + This mainly concerns API classes not a lot of implementation. - [beacon](beacon) - The XPipe beacon component is responsible for handling all communications between the XPipe - daemon - and the client applications, for example the various programming language APIs and the CLI + daemon and the client applications, for example APIs and the CLI - [app](app) - Contains the XPipe daemon implementation, the XPipe desktop application, and an API to create all different kinds of extensions for the XPipe platform - [dist](dist) - Tools to create a distributable package of XPipe -- [ext](ext) - Available XPipe extensions. Essentially every feature is implemented as an extension +- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension ## Modularity @@ -21,8 +21,11 @@ All components are modularized, including all their dependencies. In case a dependency is (sadly) not modularized yet, module information is manually added using [moditect](https://github.com/moditect/moditect-gradle-plugin). Further, note that as this is a pretty complicated Java project that fully utilizes modularity, many IDEs still have problems building this project properly. + For example, you can't build this project in eclipse or vscode as it will complain about missing modules. The tested and recommended IDE is IntelliJ. +When setting up the project in IntelliJ, make sure that the correct JDK (Java 20) +is selected both for the project and for gradle itself. ## Setup @@ -44,8 +47,8 @@ You can use the gradle wrapper to build and run the project: - `gradlew :test` will run the tests of the specified project. You are also able to properly debug the built production application through two different methods: -- The `app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it -- The `app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme). +- The `dist/build/dist/base/app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it +- The `dist/build/dist/base/app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme). Just make sure that the attachme process is running within IntelliJ, and the debugger should launch automatically once you start up the application. Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt diff --git a/FAQ.md b/FAQ.md index 0b65b563..3a1426d2 100644 --- a/FAQ.md +++ b/FAQ.md @@ -5,18 +5,16 @@ Compared to other existing tools, the fundamental approach of how to connect and how to communicate with remote systems differs. Other tools utilize the established protocol-based approach, i.e. connect and communicate with a -server via a certain protocol like SSH, SFTP, and many more using an integrated library for that purpose. -XPipe utilizes a shell-based approach that works on top of command-line programs. -It interacts with your installed command-line programs via their stdout, stderr, +server via a certain protocol like SSH, SFTP, and many more using a bundled library for that purpose. +XPipe instead utilizes a shell-based approach that works on top of command-line programs. +It exclusively interacts with your installed command-line programs via their stdout, stderr, and stdin to handle local and remote shell connections. This approach makes it much more flexible as it doesn't have to deal with any file system APIs, remote file handling protocols, or libraries at all as that part is delegated to your existing programs. - Let's use the example of SSH. Protocol-based programs come with an included SSH library that allows them to interact with a remote system via SSH. -This requires an SSH server implementation to be running on the remote system. XPipe does not ship with any sort of SSH library or similar. -Instead, XPipe creates a new process using your local `ssh` executable, which is usually the OpenSSH client. +Instead, XPipe starts a new process using your local `ssh` executable, which is usually the OpenSSH client. I.e. it launches the process `ssh user@host` in the background and communicates with the opened remote shell through the stdout, stderr, stdin of the process. From there, it detects what kind of server and environment, @@ -25,8 +23,7 @@ and adjusts how it talks to the remote system from there. It effectively delegates everything protocol and connection related to your external programs. As a result of this approach, you can do stuff with XPipe that you can't do with other tools. -One example would be connecting and accessing files on a -docker container as there's no real protocol to formally connect here by default. +One example would be connecting and accessing files on a docker container as there's no real protocol to formally connect here by default. XPipe can simply execute `docker exec -i sh` to open a shell into the container and handle the file management through this opened shell by sending commands like `ls`, `touch`, and more. @@ -39,40 +36,33 @@ you can read the [introduction article](https://foojay.io/today/presenting-xpipe ## Does it run on my system? -The desktop application should run on any reasonably up-to-date -Windows/Linux/macOS system that has been released in the last ten years. +The desktop application should run on any reasonably up-to-date Windows/Linux/macOS system that has been released in the last ten years. -## What else do I need to use this? - -As mentioned previously, XPipe itself does not ship with any sort of libraries for connection handling -and instead delegates this to your existing command-line tools. -For this approach to work however, you need to have the required tools installed. - -For example, if you want to connect to a remote system via SSH with XPipe, -you need to have an `ssh` client installed and added to your PATH. -The exact vendor and version of this `ssh` command-line -tool doesn't matter as long as the standard options are supported. - -If a required program is attempted to be used but can not be found, XPipe will notify you. +In case you are running this on a very slow system, there is also a performance mode available in the settings menu to reduce the visual fidelity and make the application more responsive. ## Is this secure / Can I entrust my sensitive information to this? -Due to its nature, XPipe has to handle a lot of sensitive information like passwords, keys, and more. -As security plays a very important role here, there exists a dedicated [security page](/SECURITY.md) -that should contain all relevant information for you to make your decision. +Due to its nature, XPipe has to handle a lot of sensitive information like passwords, keys, and more. As security plays a very important role here, there exists a dedicated [security details page](/SECURITY.md) that should contain all relevant information for you to make your decision. + +In short, all is transferred in an encrypted manner to other programs. You can choose whether you want to store sensitive information within XPipe or source it from other sources such as password managers. If you store that sensitive data in XPipe, it is also stored encrypted on your local machine. You can also set a custom master password to improve the encryption security of your data further. ## How does XPipe handle privacy? -XPipe does not collect any sort of data like usage or tracking data. -The only case in which some sort of data is collected is when you choose to -use the built-in error reporter to submit a report. +XPipe does not collect any personal data. +The only case in which some sort of data is collected is when the built-in error reporter is used to submit a report. This report data is limited to general system and error information, no sensitive information is submitted. For those people who like to read legalese, there's the [privacy policy](/PRIVACY.md). -## How does XPipe handle updates? +## Do I have to pay to use this effectively? -Especially in its early development stage, it can be pretty important to frequently distribute new releases. -How exactly the update process is handled depends on your distribution: +I recently decided to develop XPipe full time and hope to finance this by providing plans for professional and commercial users. +The commercialization model is designed to be very generous for personal users. If you don't use XPipe for commercial purposes, you can use it basically without any limitations for free. If you intend to use it for commercial purposes or want to support the development, you can check out the [available tiers](https://buy.xpipe.io/checkout/buy/dbcd37b8-be94-40a5-8c1c-af61979e6537). + +## Which release type should I choose? + +You are able to essentially get the same feature set regardless which way you choose. There are a few small exceptions, such as desktop environment integrations for your operating system that are only available with installers, however these features are not crucial to XPipe. + +Especially in its early development stage, it can be pretty important to frequently distribute new releases. How exactly the update process is handled depends on your distribution: - Installers (msi/deb/rpm/pkg): They come with the ability to automatically check for updates, download them, and install them if you provide your confirmation. @@ -83,17 +73,47 @@ How exactly the update process is handled depends on your distribution: Note that you can choose to disable this update check functionality entirely in the settings menu. +## Does it matter which type of release I choose initially? + +Not really, they all share the same configuration data locations. You can switch between different release types, e.g. from the portable version to an installer without any issues if you just want to try it out without installing. + +There also exists a separate PTB (Public Test Build) release that is meant for testing out new features early on. You can find them at https://github.com/xpipe-io/xpipe-ptb if you're interested. The regular releases and PTB releases are designed to not interfere with each other and can therefore be installed and used side by side. + +## How can I save/export my configuration data? + +If you want to export or share the whole connection list, you can find all the data at ~/.xpipe/storage. You can also change that directory in the settings menu. + +A simple solution is to change the storage directory to be in a cloud directory like OneDrive or Dropbox so it automatically synchronizes the data across all systems. + +The professional version also comes with a feature to synchronize your storage with a remote git repository that you can host yourself wherever you like. This comes with the advantage of a commit history for individual connections and the ability to share this repository data with other team members using the access management of your git platform. + +## Can I contribute to this project? + +Yes, check out the [development page](/DEVELOPMENT.md) for details on how to set up a development environment and the [contributing page](/CONTRIBUTING.md) on how to get started. + ## Why are there no GitHub actions workflows in this repository? -There are several test workflows run in a private environment as they use private test connections -such as remote server connections and database connections. -Other private workflows are responsible for packaging, signing, and distributing the releases. -So you can assume that the code is tested and the release is automated! +There are several test workflows run in a private environment as they use private test connections such as remote server connections and database connections. Other private workflows are responsible for packaging, signing, and distributing the releases and are also kept private due to them handling a lot of passwords and API keys. So you can assume that the code is tested and the release is automated! ## What is the best way to reach out to the developers and other users? -You can always open a GitHub issue in this repository in case you encounter a problem. -There are also several other ways to reach out, so you can choose whatever you like best: +You can always open a GitHub issue in this repository in case you encounter a problem. There are also several other ways to reach out, so you can choose whatever you like best: - [XPipe Discord Server](https://discord.gg/8y89vS8cRb) - [XPipe Slack Server](https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg) + +## What is XPipe not? + +XPipe is not: + +- a backup tool: It is not designed to copy large masses of files across systems reliably. +- a system management tool: While it allows you to access any remote system, it does not come with a fancy management dashboard and overview for your server infrastructure. +- a terminal emulator: XPipe is designed around integrating with your own favorite terminal and will allow you to launch any preconfigured shell connection in it. It does not come with any integrated terminal functionality itself. +- a separate protocol handling implementation: XPipe does not come with its own libraries to handle protocols, so it is not able to connect via SSH without a locally installed SSH client like OpenSSH +- an RDP/VNC client: It does not support these protocols (yet) + +## What will definitely not be implemented? + +While the general development direction is still very open, there are a few things that definitely won't be implemented: + +- A mobile version, an app store version, and a flatpak version: The concept of integrating with your local CLI tools is incompatible with most sandboxes. diff --git a/PRIVACY.md b/PRIVACY.md index df82c77a..9350ab1c 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,293 +1,22 @@ -**PRIVACY NOTICE (Created with termly.io)** +**PRIVACY NOTICE** -**Last updated April 21, 2023** +**Last updated September 17, 2023** -This privacy notice for XPipe, in which ("**we**," "**us**," or "**our**") refers to Christopher Schnick, describes how +This privacy notice for XPipe, in which ("**we**," "**us**," or "**our**") refers to XPipe UG (haftungsbeschränkt), describes how and why we might collect, store, use, and/or share ("**process**") your information when you use our services ("**Services**"), such as when you: -* Download and use our application (XPipe), or any other application of ours that links to this privacy notice +* Download and use our application (XPipe) **Questions or concerns?** Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with our policies and practices, please do not use our Services. If you still have any questions or concerns, please contact us at hello@xpipe.io. - **TABLE OF CONTENTS** +**1\. WHAT INFORMATION DO WE COLLECT?** -[1\. WHAT INFORMATION DO WE COLLECT?](#infocollect) +We do not process personal or sensitive information. -[2\. HOW DO WE PROCESS YOUR INFORMATION?](#infouse) +**2\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?** -[3\. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR PERSONAL INFORMATION?](#legalbases) - -[4\. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?](#whoshare) - -[5\. IS YOUR INFORMATION TRANSFERRED INTERNATIONALLY?](#intltransfers) - -[6\. HOW LONG DO WE KEEP YOUR INFORMATION?](#inforetain) - -[7\. HOW DO WE KEEP YOUR INFORMATION SAFE?](#infosafe) - -[8\. WHAT ARE YOUR PRIVACY RIGHTS?](#privacyrights) - -[9\. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?](#caresidents) - -[10\. DO WE MAKE UPDATES TO THIS NOTICE?](#policyupdates) - -[11\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact) - -[12\. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?](#request) - - **1\. WHAT INFORMATION DO WE COLLECT?** - -**Personal information you disclose to us** - -We collect personal information that you voluntarily provide to us when you express an interest in obtaining information -about us or our products and Services, or otherwise when you contact us, for example by emailing us. - -**Sensitive Information.** We do not process sensitive information. - -**Error reports.** If you encounter and report application errors using the issue reporter, -we also may collect the following information if you choose to -provide us with access or permission: - -* _Device Data._ Device information (such as your device ID, model, and - manufacturer), operating system, version information, and application identification numbers. - -* _Log and Usage Data. (Optional choice)_ Log and usage data is service-related, diagnostic, usage, and performance information our - servers automatically collect when you access or use our Services and which we record in log files. Depending on how - you interact with us, this log data may include your IP address, device information, and settings and - information about your activity in the Services (such as the date/time stamps associated with your usage, pages and - files viewed, searches, and other actions you take such as which features you use), device event information (such as - system activity, error reports (sometimes called "crash dumps"), and hardware settings). - -* _Error Diagnostics Information. (Optional choice)_ Information relating to a specific error that occurred in the application. This may - include application logs, event data, and any other kind of data that is used to diagnose the error. - -This information is primarily needed to maintain the security and operation of our application(s), for troubleshooting, -and for our internal analytics and reporting purposes. - - **2\. HOW DO WE PROCESS YOUR INFORMATION?** - -**We process your personal information for a variety of reasons, depending on how you interact with our Services, -including:** - -* **To protect our Services.** We may process your information as part of our efforts to keep our Services safe and - secure, including error and exploit monitoring and prevention. - -* **To save or protect an individual's vital interest.** We may process your information when necessary to save or - protect an individual’s vital interest, such as to prevent harm. - - **3\. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR INFORMATION?** - -_**In Short:** We only process your personal information when we believe it is necessary and we have a valid legal -reason (i.e., legal basis) to do so under applicable law, like with your consent, to comply with laws, to provide you -with services to enter into or fulfill our contractual obligations, to protect your rights, or to fulfill our legitimate -business interests._ - -_**If you are located in the EU or UK, this section applies to you.**_ - -The General Data Protection Regulation (GDPR) and UK GDPR require us to explain the valid legal bases we rely on in -order to process your personal information. As such, we may rely on the following legal bases to process your personal -information: - -* **Consent.** We may process your information if you have given us permission (i.e., consent) to use your personal - information for a specific purpose. You can withdraw your consent at any time. Click [here](#request) to learn - more. - -* **Legitimate Interests.** We may process your information when we believe it is reasonably necessary to achieve our - legitimate business interests and those interests do not outweigh your interests and fundamental rights and freedoms. - For example, we may process your personal information for some of the purposes described in order to: - -* Diagnose problems and/or prevent errors and exploits - -* Understand how our users use our products and services so we can improve user experience - -* **Legal Obligations.** We may process your information where we believe it is necessary for compliance with our legal - obligations, such as to cooperate with a law enforcement body or regulatory agency, exercise or defend our legal - rights, or disclose your information as evidence in litigation in which we are involved. - - -* **Vital Interests.** We may process your information where we believe it is necessary to protect your vital interests - or the vital interests of a third party, such as situations involving potential threats to the safety of any person. - -In legal terms, we are generally the "data controller" under European data protection laws of the personal information -described in this privacy notice, since we determine the means and/or purposes of the data processing we perform. This -privacy notice does not apply to the personal information we process as a "data processor" on behalf of our customers. -In those situations, the customer that we provide services to and with whom we have entered into a data processing -agreement is the "data controller" responsible for your personal information, and we merely process your information on -their behalf in accordance with your instructions. If you want to know more about our customers' privacy practices, you -should read their privacy policies and direct any questions you have to them. - -**_If you are located in Canada, this section applies to you._** - -We may process your information if you have given us specific permission (i.e., express consent) to use your personal -information for a specific purpose, or in situations where your permission can be inferred (i.e., implied consent). You -can withdraw your consent at any time. Click [here](#request) to learn more. - -In some exceptional cases, we may be legally permitted under applicable law to process your information without your -consent, including, for example: - -* If collection is clearly in the interests of an individual and consent cannot be obtained in a timely way - -* For investigations and fraud detection and prevention - -* For business transactions provided certain conditions are met - -* If it is contained in a witness statement and the collection is necessary to assess, process, or settle an insurance - claim - -* For identifying injured, ill, or deceased persons and communicating with next of kin - -* If we have reasonable grounds to believe an individual has been, is, or may be victim of financial abuse - -* If it is reasonable to expect collection and use with consent would compromise the availability or the accuracy of the - information and the collection is reasonable for purposes related to investigating a breach of an agreement or a - contravention of the laws of Canada or a province - -* If disclosure is required to comply with a subpoena, warrant, court order, or rules of the court relating to the - production of records - -* If it was produced by an individual in the course of their employment, business, or profession and the collection is - consistent with the purposes for which the information was produced - -* If the collection is solely for journalistic, artistic, or literary purposes - -* If the information is publicly available and is specified by the regulations - - **4\. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?** - -**_In Short:_** _We may share information in specific situations described in this section and/or with the following -third parties._ - -* **Error reporting and usage tracking** - -**_Sentry_**: -Functional Software, Inc. dba Sentry, 45 Fremont Street, 8th Floor, San Francisco, CA 94105. -You can find their privacy policy here: [https://sentry.io/privacy/](https://sentry.io/privacy/) - - **5\. IS YOUR INFORMATION TRANSFERRED INTERNATIONALLY?** - -**_In Short:_** _We may transfer, store, and process your information in countries other than your own._ - -Please be aware that your information may be transferred to, stored, and processed by us in our -facilities and by those third parties with whom we may share your personal information ( -see "[WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?](#whoshare)" above), in the United States, and other -countries. - -If you are a resident in the European Economic Area (EEA) or United Kingdom (UK), then these countries may not -necessarily have data protection laws or other similar laws as comprehensive as those in your country. However, we will -take all necessary measures to protect your personal information in accordance with this privacy notice and applicable -law. - -European Commission's Standard Contractual Clauses: - -We have implemented measures to protect your personal information, including by using the European Commission's Standard -Contractual Clauses for transfers of personal information between our group companies and between us and our third-party -providers. These clauses require all recipients to protect all personal information that they process originating from -the EEA or UK in accordance with European data protection laws and regulations. Our Data Processing Agreements that -include Standard Contractual Clauses are available here: [https://sentry.io/legal/dpa/](https://sentry.io/legal/dpa/). -We have implemented similar appropriate safeguards with our third-party service providers and partners and further -details can be provided upon request. - - **6\. HOW LONG DO WE KEEP YOUR INFORMATION?** - -**_In Short:_** _We keep your information for as long as necessary to fulfill the purposes outlined in this privacy -notice unless otherwise required by law._ - -We will only keep your personal information for as long as it is necessary for the purposes set out in this privacy -notice, unless a longer retention period is required or permitted by law (such as tax, accounting, or other legal -requirements). - -When we have no ongoing legitimate business need to process your personal information, we will either delete or -anonymize such information, or, if this is not possible (for example, because your personal information has been stored -in backup archives), then we will securely store your personal information and isolate it from any further processing -until deletion is possible. - - **7\. HOW DO WE KEEP YOUR INFORMATION SAFE?** - -**_In Short:_** _We aim to protect your personal information through a system of organizational and technical security -measures._ - -We have implemented appropriate and reasonable technical and organizational security measures designed to protect the -security of any personal information we process. However, despite our safeguards and efforts to secure your information, -no electronic transmission over the Internet or information storage technology can be guaranteed to be 100% secure, so -we cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to -defeat our security and improperly collect, access, steal, or modify your information. Although we will do our best to -protect your personal information, transmission of personal information to and from our Services is at your own risk. -You should only access the Services within a secure environment. - - **8\. WHAT ARE YOUR PRIVACY RIGHTS?** - -**_In Short:_** _In some regions, such as the European Economic Area (EEA), United Kingdom (UK), and Canada, you have -rights that allow you greater access to and control over your personal information. You may review, change, or terminate -your account at any time._ - -In some regions (like the EEA, UK, and Canada), you have certain rights under applicable data protection laws. These may -include the right (i) to request access and obtain a copy of your personal information, (ii) to request rectification or -erasure; (iii) to restrict the processing of your personal information; and (iv) if applicable, to data portability. In -certain circumstances, you may also have the right to object to the processing of your personal information. You can -make such a request by contacting us by using the contact details provided in the -section "[HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)" below. - -We will consider and act upon any request in accordance with applicable data protection laws. - -If you are located in the EEA or UK and you believe we are unlawfully processing your personal information, you also -have the right to complain to your local data protection supervisory authority. You can find their contact details -here: [https://ec.europa.eu/justice/data-protection/bodies/authorities/index\_en.htm](https://ec.europa.eu/justice/data-protection/bodies/authorities/index_en.htm) -. - -If you are located in Switzerland, the contact details for the data protection authorities are available -here: [https://www.edoeb.admin.ch/edoeb/en/home.html](https://www.edoeb.admin.ch/edoeb/en/home.html). - -**Withdrawing your consent:** If we are relying on your consent to process your personal information, which may be -express and/or implied consent depending on the applicable law, you have the right to withdraw your consent at any time. -You can withdraw your consent at any time by contacting us by using the contact details provided in the -section "[HOW CAN YOU CONTACT US ABOUT THIS NOTICE?](#contact)" below. - -However, please note that this will not affect the lawfulness of the processing before its withdrawal nor, when -applicable law allows, will it affect the processing of your personal information conducted in reliance on lawful -processing grounds other than consent. - -If you have questions or comments about your privacy rights, you may email us at hello@xpipe.io. - - **9\. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS?** - -**_In Short:_** _Yes, if you are a resident of California, you are granted specific rights regarding access to your -personal information._ - -California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users who are California -residents to request and obtain from us, once a year and free of charge, information about categories of personal -information (if any) we disclosed to third parties for direct marketing purposes and the names and addresses of all -third parties with which we shared personal information in the immediately preceding calendar year. If you are a -California resident and would like to make such a request, please submit your request in writing to us using the contact -information provided below. - -If you are under 18 years of age, reside in California, and have a registered account with Services, you have the right -to request removal of unwanted data that you publicly post on the Services. To request removal of such data, please -contact us using the contact information provided below and include the email address associated with your account and a -statement that you reside in California. We will make sure the data is not publicly displayed on the Services, but -please be aware that the data may not be completely or comprehensively removed from all our systems (e.g., backups, -etc.). - - **10\. DO WE MAKE UPDATES TO THIS NOTICE?** - -_**In Short:** Yes, we will update this notice as necessary to stay compliant with relevant laws._ - -We may update this privacy notice from time to time. The updated version will be indicated by an updated "Revised" date -and the updated version will be effective as soon as it is accessible. If we make material changes to this privacy -notice, we may notify you either by prominently posting a notice of such changes or by directly sending you a -notification. We encourage you to review this privacy notice frequently to be informed of how we are protecting your -information. - - **11\. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?** - -If you have questions or comments about this notice, you may contact Christopher -Schnick by email at crschnick@xpipe.io. - - **12\. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?** - -Based on the applicable laws of your country, you may have the right to request access to the personal information we -collect from you, change that information, or delete it. To request to review, update, or delete your personal -information, please submit a request form by writing to hello@xpipe.io. +If you have questions or comments, you may contact us by email at hello@xpipe.io. diff --git a/README.md b/README.md index 5d942f89..f839a03a 100644 --- a/README.md +++ b/README.md @@ -9,23 +9,18 @@ XPipe fully integrates with your tools such as your favourite text/code editors, It currently supports: - [Kubernetes](https://kubernetes.io/) clusters, pods, and containers - [Docker](https://www.docker.com/), [Podman](https://podman.io/), and [LXD](https://linuxcontainers.org/lxd/introduction/) container instances located on any host -- [SSH](https://www.ssh.com/academy/ssh/protocol) connections, config file connections, and tunnels +- [SSH](https://www.ssh.com/academy/ssh/protocol) connections, config files, and tunnels - [Windows Subsystem for Linux](https://ubuntu.com/wsl), [Cygwin](https://www.cygwin.com/), and [MSYS2](https://www.msys2.org/) instances - [Powershell Remote Sessions](https://learn.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.3) - Any other custom remote connection methods that work through the command-line -Furthermore, you can also use any remote shell connection as a proxy when establishing new connections, allowing full flexibility to set up connection routes. - -The project is still in a relatively early stage and will benefit massively from your feedback, issue reports, feature request, and more. There are also a lot more features to come in the future. - -You have more questions? Then check out the new [FAQ](/FAQ.md). - ## Connection Hub - Easily connect to and access all kinds of remote connections in one place - Securely stores all information exclusively on your computer and encrypts all secret information. See the [security page](/SECURITY.md) for more information -- Allows you to fully customize the init environment of the launched shell sessions with custom scripts +- Allows you to create specific login environments on any system to instantly jump into proper environment for every use case of yours - Can create desktop shortcuts that automatically open remote connections in your terminal +- Group all your connections into hierarchical categories ![connections](https://github.com/xpipe-io/xpipe/assets/72509152/ef19aa85-1b66-45e0-a051-5a4658758626) @@ -34,10 +29,9 @@ You have more questions? Then check out the new [FAQ](/FAQ.md). - Interact with the file system of any remote system using a workflow optimized for professionals - Quickly open a terminal into any directory - Utilize your favourite local programs to open and edit remote files -- Has the same feature set for all supported connection types - Dynamically elevate sessions with sudo when required -The feature set is the same for all supported connection types. It of course also supports browsing the file system on your local machine. +The feature set is the same for all supported connection types. It also supports browsing the file system on your local machine. ![browser](https://github.com/xpipe-io/xpipe/assets/72509152/5631fe50-58b4-4847-a5f4-ad3898a02a9f) @@ -100,12 +94,21 @@ bash <(curl -sL https://raw.githubusercontent.com/xpipe-io/xpipe/master/get-xpip powershell -ExecutionPolicy Bypass -Command iwr "https://raw.githubusercontent.com/xpipe-io/xpipe/master/get-xpipe.ps1" -OutFile "$env:TEMP\get-xpipe.ps1" ";" "&" "$env:TEMP\get-xpipe.ps1" ``` +## Tiers + +Recently I decided to try to develop XPipe full-time. To finance this, there now is a professional tier intended for commercial users. +The free community tier comes with the full feature set and no restrictions on anything as long you are using it for non-commercial purposes. For commercial usage, I would like to ask you to purchase an [XPipe professional license](https://buy.xpipe.io/checkout/buy/dbcd37b8-be94-40a5-8c1c-af61979e6537). You can try out XPipe as much as you want in a non-commercial setting or start a free trial if you want to test it in your commercial environments prior to purchasing a license. + ## Open source model -XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future. This mainly concerns the shell handling library implementation and extensions for configuring and handling shell connections. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. Finally, scripts and workflows to create and publish installers and packages are also not included to prevent attackers from easily impersonating the XPipe application. +XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future. + +This mainly concerns the features only available in the professional tier and the shell handling library implementation and extensions for configuring and handling shell connections. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. Finally, scripts and workflows to create and publish installers and packages are also not included to prevent attackers from easily impersonating the XPipe application. ## Further information +You have more questions? Then check out the new [FAQ](/FAQ.md). + For information about the security model of XPipe, see the [security page](/SECURITY.md). For information about the privacy policy of XPipe, see the [privacy page](/PRIVACY.md). diff --git a/api/src/main/java/io/xpipe/api/connector/XPipeApiConnection.java b/api/src/main/java/io/xpipe/api/connector/XPipeApiConnection.java index 6cbddc49..0645d941 100644 --- a/api/src/main/java/io/xpipe/api/connector/XPipeApiConnection.java +++ b/api/src/main/java/io/xpipe/api/connector/XPipeApiConnection.java @@ -132,7 +132,7 @@ public final class XPipeApiConnection extends BeaconConnection { } private void start() throws Exception { - var installation = XPipeInstallation.getLocalDefaultInstallationBasePath(true); + var installation = XPipeInstallation.getLocalDefaultInstallationBasePath(); BeaconServer.start(installation, XPipeDaemonMode.TRAY); } diff --git a/app/build.gradle b/app/build.gradle index 47004c53..9abeddd9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,7 +74,7 @@ apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" sourceSets { main { - output.resourcesDir("$buildDir/classes/java/main") + output.resourcesDir("${project.layout.buildDirectory.get()}/classes/java/main") } } @@ -100,6 +100,7 @@ List jvmRunArgs = [ "--add-exports", "javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-exports", "javafx.graphics/com.sun.javafx.scene=org.controlsfx.controls", "--add-exports", "org.apache.commons.lang3/org.apache.commons.lang3.math=io.xpipe.app", + "--add-opens", "java.base/java.lang=io.xpipe.app", "--add-opens", "java.base/java.lang.reflect=com.jfoenix", "--add-opens", "java.base/java.lang.reflect=com.jfoenix", "--add-opens", "java.base/java.lang=io.xpipe.core", @@ -110,9 +111,9 @@ List jvmRunArgs = [ "--add-opens", 'com.dlsc.preferencesfx/com.dlsc.preferencesfx.model=io.xpipe.app', "-Xmx8g", "-Dio.xpipe.app.arch=$rootProject.arch", - "--enable-preview", // "-XX:+ExitOnOutOfMemoryError", "-Dfile.encoding=UTF-8", + '-XX:+UseZGC', "-Dvisualvm.display.name=XPipe" ] @@ -137,7 +138,7 @@ application { run { systemProperty 'io.xpipe.app.useVirtualThreads', 'false' systemProperty 'io.xpipe.app.mode', 'gui' - systemProperty 'io.xpipe.app.dataDir', "$projectDir/local7/" + systemProperty 'io.xpipe.app.dataDir', "$projectDir/local_git2/" systemProperty 'io.xpipe.app.writeLogs', "true" systemProperty 'io.xpipe.app.writeSysOut', "true" systemProperty 'io.xpipe.app.developerMode', "true" diff --git a/app/gradle_scripts/flexmark.gradle b/app/gradle_scripts/flexmark.gradle index d2011c42..39c8debb 100644 --- a/app/gradle_scripts/flexmark.gradle +++ b/app/gradle_scripts/flexmark.gradle @@ -1,21 +1,21 @@ dependencies { - implementation files("$buildDir/generated-modules/flexmark-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-data-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-ast-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-builder-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-sequence-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-misc-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-dependency-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-collection-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-format-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-html-0.64.0.jar") - implementation files("$buildDir/generated-modules/flexmark-util-visitor-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-data-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-ast-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-builder-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-sequence-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-misc-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-dependency-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-collection-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-format-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-html-0.64.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flexmark-util-visitor-0.64.0.jar") } addDependenciesModuleInfo { overwriteExistingFiles = true jdepsExtraArgs = ['-q'] - outputDirectory = file("$buildDir/generated-modules") + outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") modules { module { artifact 'com.vladsch.flexmark:flexmark:0.64.0' diff --git a/app/gradle_scripts/github-api.gradle b/app/gradle_scripts/github-api.gradle index 2382bc0b..1d54161b 100644 --- a/app/gradle_scripts/github-api.gradle +++ b/app/gradle_scripts/github-api.gradle @@ -1,11 +1,11 @@ dependencies { - implementation files("$buildDir/generated-modules/github-api-1.301.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/github-api-1.301.jar") } addDependenciesModuleInfo { overwriteExistingFiles = true jdepsExtraArgs = ['-q'] - outputDirectory = file("$buildDir/generated-modules") + outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") modules { module { artifact 'org.kohsuke:github-api:1.301' diff --git a/app/gradle_scripts/richtextfx.gradle b/app/gradle_scripts/richtextfx.gradle index 470e083a..a5dc4a61 100644 --- a/app/gradle_scripts/richtextfx.gradle +++ b/app/gradle_scripts/richtextfx.gradle @@ -1,15 +1,15 @@ dependencies { - implementation files("$buildDir/generated-modules/richtextfx-0.10.6.jar") - implementation files("$buildDir/generated-modules/flowless-0.6.6.jar") - implementation files("$buildDir/generated-modules/undofx-2.1.1.jar") - implementation files("$buildDir/generated-modules/wellbehavedfx-0.3.3.jar") - implementation files("$buildDir/generated-modules/reactfx-2.0-M5.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/richtextfx-0.10.6.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/flowless-0.6.6.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/undofx-2.1.1.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/wellbehavedfx-0.3.3.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/reactfx-2.0-M5.jar") } addDependenciesModuleInfo { overwriteExistingFiles = true jdepsExtraArgs = ['-q'] - outputDirectory = file("$buildDir/generated-modules") + outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") modules { module { artifact group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6.6' diff --git a/app/gradle_scripts/sentry.gradle b/app/gradle_scripts/sentry.gradle index 12b74596..0d647055 100644 --- a/app/gradle_scripts/sentry.gradle +++ b/app/gradle_scripts/sentry.gradle @@ -1,14 +1,14 @@ dependencies { - implementation files("$buildDir/generated-modules/sentry-6.16.0.jar") + implementation files("${project.layout.buildDirectory.get()}/generated-modules/sentry-6.29.0.jar") } addDependenciesModuleInfo { overwriteExistingFiles = true jdepsExtraArgs = ['-q'] - outputDirectory = file("$buildDir/generated-modules") + outputDirectory = file("${project.layout.buildDirectory.get()}/generated-modules") modules { module { - artifact 'io.sentry:sentry:6.16.0' + artifact 'io.sentry:sentry:6.29.0' moduleInfoSource = ''' module io.sentry { exports io.sentry; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java index 23821528..8f55b06f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java @@ -1,39 +1,41 @@ package io.xpipe.app.browser; -import io.xpipe.app.comp.storage.store.StoreEntryTree; +import atlantafx.base.theme.Styles; import io.xpipe.app.comp.storage.store.StoreEntryWrapper; +import io.xpipe.app.comp.storage.store.StoreSection; +import io.xpipe.app.comp.storage.store.StoreSectionMiniComp; import io.xpipe.app.comp.storage.store.StoreViewState; +import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.IconButtonComp; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; +import io.xpipe.app.fxcomps.impl.FilterComp; +import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.DataStoreCategoryChoiceComp; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.FixedHierarchyStore; import io.xpipe.core.store.ShellStore; import javafx.application.Platform; -import javafx.beans.property.*; -import javafx.collections.SetChangeListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; import javafx.css.PseudoClass; import javafx.geometry.Point2D; -import javafx.scene.AccessibleRole; -import javafx.scene.Node; -import javafx.scene.control.OverrunStyle; -import javafx.scene.control.TreeCell; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; import javafx.scene.input.DragEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import java.util.List; import java.util.Timer; import java.util.TimerTask; +import java.util.function.Predicate; final class BrowserBookmarkList extends SimpleComp { + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + public static final Timer DROP_TIMER = new Timer("dnd", true); private Point2D lastOver = new Point2D(-1, -1); private TimerTask activeTask; @@ -46,167 +48,68 @@ final class BrowserBookmarkList extends SimpleComp { @Override protected Region createSimple() { - var root = StoreEntryTree.createTree(); - var view = new TreeView<>(root); - view.setShowRoot(false); - view.getStyleClass().add("bookmark-list"); - view.setCellFactory(param -> { - return new StoreCell(view); - }); - - PlatformThread.sync(model.getSelected()).addListener((observable, oldValue, newValue) -> { - if (newValue == null) { - view.getSelectionModel().clearSelection(); - return; - } - - view.getSelectionModel() - .select(getTreeViewItem( - root, - StoreViewState.get().getAllEntries().stream() - .filter(storeEntryWrapper -> storeEntryWrapper - .getState() - .getValue() - .isUsable() - && storeEntryWrapper - .getEntry() - .getStore() - .equals(newValue.getStore())) - .findAny() - .orElse(null))); - }); - - return view; - } - - private static TreeItem getTreeViewItem( - TreeItem item, StoreEntryWrapper value) { - if (item.getValue() != null && item.getValue().equals(value)) { - return item; - } - - for (TreeItem child : item.getChildren()) { - TreeItem s = getTreeViewItem(child, value); - if (s != null) { - return s; - } - } - return null; - } - - private final class StoreCell extends TreeCell { - - private final StringProperty img = new SimpleStringProperty(); - private final Node imageView = new PrettyImageComp(img, 20, 20).createRegion(); - private final BooleanProperty busy = new SimpleBooleanProperty(false); - - @Override - protected double computePrefWidth(double height) { - // This makes the cell always properly cut of any overflow of text - return 1; - } - - private StoreCell(TreeView t) { - disableProperty().bind(busy); - setAccessibleRole(AccessibleRole.BUTTON); - setGraphic(imageView); - setTextOverrun(OverrunStyle.ELLIPSIS); - addEventHandler(DragEvent.DRAG_OVER, mouseEvent -> { - if (getItem() == null) { - return; - } - - // Disable this for now - // handleHoverTimer(getItem().getEntry().getStore(), mouseEvent); - - mouseEvent.consume(); - }); - addEventHandler(DragEvent.DRAG_EXITED, mouseEvent -> { - activeTask = null; - mouseEvent.consume(); - }); - addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { - if (getItem() == null - || event.getButton() != MouseButton.PRIMARY - || (!getItem().getState().getValue().isUsable())) { - return; - } - - ThreadHelper.runFailableAsync(() -> { - if (getItem().getEntry().getStore() instanceof ShellStore fileSystem) { - BusyProperty.execute(busy, () -> { - getItem().refreshIfNeeded(); + var filterText = new SimpleStringProperty(); + var open = PlatformThread.sync(model.getSelected()); + Predicate applicable = storeEntryWrapper -> { + return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore + || storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore) + && storeEntryWrapper.getEntry().getState().isUsable(); + }; + var section = StoreSectionMiniComp.createList( + StoreSection.createTopLevel( + StoreViewState.get().getAllEntries(), applicable, filterText, StoreViewState.get().getActiveCategory()), + (s, comp) -> { + BooleanProperty busy = new SimpleBooleanProperty(false); + comp.disable(Bindings.createBooleanBinding(() -> { + return busy.get() || !applicable.test(s.getWrapper()); + }, busy)).apply(struc -> { + open.addListener((observable, oldValue, newValue) -> { + struc.get() + .pseudoClassStateChanged( + SELECTED, + newValue != null + && newValue.getStore() + .equals(s.getWrapper() + .getEntry() + .getStore())); }); - model.openFileSystemAsync(null, fileSystem, null, busy); - } else if (getItem().getEntry().getStore() instanceof FixedHierarchyStore) { - BusyProperty.execute(busy, () -> { - getItem().refreshWithChildren(); + struc.get().setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + var entry = s.getWrapper().getEntry(); + if (entry.getStore() instanceof ShellStore fileSystem) { + BooleanScope.execute(busy, () -> { + s.getWrapper().refreshIfNeeded(); + }); + model.openFileSystemAsync(null, fileSystem, null, busy); + } else if (entry.getStore() instanceof FixedHierarchyStore) { + BooleanScope.execute(busy, () -> { + s.getWrapper().refreshWithChildren(); + }); + } + }); + event.consume(); }); - } + }); }); - event.consume(); - }); - var icon = new SimpleObjectProperty<>("mdal-keyboard_arrow_right"); - getPseudoClassStates().addListener((SetChangeListener) change -> { - if (change.getSet().contains(PseudoClass.getPseudoClass("expanded"))) { - icon.set("mdal-keyboard_arrow_down"); - } else { - icon.set("mdal-keyboard_arrow_right"); - } - }); - var button = new IconButtonComp(icon, () -> { - getTreeItem().setExpanded(!getTreeItem().isExpanded()); - }) - .apply(struc -> struc.get().setPrefWidth(25)) - .grow(false, true) - .styleClass("expand-button") - .apply(struc -> struc.get().setFocusTraversable(false)); - setDisclosureNode(button.createRegion()); + var category = new DataStoreCategoryChoiceComp(StoreViewState.get().getActiveCategory()) + .styleClass(Styles.LEFT_PILL) + .grow(false, true); + var filter = + new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {}); - indexProperty().addListener((observable, oldValue, newValue) -> { - if (newValue.intValue() == 0) { - getStyleClass().add("first"); - } else { - getStyleClass().remove("first"); - } - }); - } + var top = new HorizontalComp(List.of(category, filter.hgrow())) + .styleClass("top") + .apply(struc -> { + AppFont.medium(struc.get()); + struc.get().setFillHeight(true); + }) + .createRegion(); + var r = section.vgrow().createRegion(); + var content = new VBox(top, r); + content.setFillWidth(true); - @Override - public void updateItem(StoreEntryWrapper item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - - // Don't set image as that would trigger image comp update - // and cells are emptied on each change, leading to unnecessary changes - // img.set(null); - - // Use opacity instead of visibility as visibility is kinda bugged with web views - setOpacity(0.0); - - setFocusTraversable(false); - setAccessibleText(null); - } else { - setText(item.getName()); - - // Check if store is in failed state - if (item.getEntry().getState() == DataStoreEntry.State.LOAD_FAILED) { - setGraphic(null); - setFocusTraversable(false); - setAccessibleText(null); - return; - } - - img.set(item.getEntry() - .getProvider() - .getDisplayIconFileName(item.getEntry().getStore())); - setOpacity(1.0); - setFocusTraversable(true); - setAccessibleText( - item.getName() + " " + item.getEntry().getProvider().getDisplayName()); - } - } + content.getStyleClass().add("bookmark-list"); + return content; } private void handleHoverTimer(DataStore store, DragEvent event) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java index e99f50f1..b5b0de3e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java @@ -1,6 +1,8 @@ package io.xpipe.app.browser; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileSystem; +import io.xpipe.core.util.FailableRunnable; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.input.ClipboardContent; @@ -8,6 +10,11 @@ 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; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -24,6 +31,41 @@ public class BrowserClipboard { public static final Property currentCopyClipboard = new SimpleObjectProperty<>(); public static Instance currentDragClipboard; + static { + Toolkit.getDefaultToolkit() + .getSystemClipboard() + .addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable() { + @Override + @SuppressWarnings("unchecked") + public void run() throws Throwable { + Clipboard clipboard = (Clipboard) e.getSource(); + try { + if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { + return; + } + + List data = (List) clipboard.getData(DataFlavor.javaFileListFlavor); + var files = data.stream().map(string -> string.toPath()).toList(); + if (files.size() == 0) { + return; + } + + var entries = new ArrayList(); + for (Path file : files) { + entries.add(FileSystemHelper.getLocal(file)); + } + + currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries)); + } catch (IllegalStateException ex) { + // Handle awt bug + if (!"cannot open system clipboard".equals(ex.getMessage())) { + throw ex; + } + } + } + })); + } + @SneakyThrows public static ClipboardContent startDrag(FileSystem.FileEntry base, List selected) { var content = new ClipboardContent(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java index 6e022963..d940b836 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -10,13 +10,12 @@ import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -34,6 +33,7 @@ import javafx.scene.layout.*; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import static atlantafx.base.theme.Styles.DENSE; import static atlantafx.base.theme.Styles.toggleStyleClass; @@ -88,7 +88,7 @@ public class BrowserComp extends SimpleComp { .widthProperty() .addListener( // set sidebar width in pixels depending on split pane width - (obs, old, val) -> splitPane.setDividerPosition(0, 320 / splitPane.getWidth())); + (obs, old, val) -> splitPane.setDividerPosition(0, 360 / splitPane.getWidth())); var r = addBottomBar(splitPane); r.getStyleClass().add("browser"); @@ -149,7 +149,8 @@ public class BrowserComp extends SimpleComp { private TabPane createTabPane() { var tabs = new TabPane(); tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); - tabs.setTabMinWidth(Region.USE_COMPUTED_SIZE); + tabs.setTabMinWidth(Region.USE_PREF_SIZE); + tabs.setTabMaxWidth(400); tabs.setTabClosingPolicy(ALL_TABS); Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); toggleStyleClass(tabs, DENSE); @@ -200,7 +201,7 @@ public class BrowserComp extends SimpleComp { while (c.next()) { for (var r : c.getRemoved()) { PlatformThread.runLaterIfNeeded(() -> { - try (var b = new BusyProperty(modifying)) { + try (var b = new BooleanScope(modifying).start()) { var t = map.remove(r); tabs.getTabs().remove(t); } @@ -209,7 +210,7 @@ public class BrowserComp extends SimpleComp { for (var a : c.getAddedSubList()) { PlatformThread.runLaterIfNeeded(() -> { - try (var b = new BusyProperty(modifying)) { + try (var b = new BooleanScope(modifying).start()) { var t = createTab(tabs, a); map.put(a, t); tabs.getTabs().add(t); @@ -244,34 +245,51 @@ public class BrowserComp extends SimpleComp { var tab = new Tab(); var ring = new RingProgressIndicator(0, false); - ring.setMinSize(14, 14); - ring.setPrefSize(14, 14); - ring.setMaxSize(14, 14); + ring.setMinSize(16, 16); + ring.setPrefSize(16, 16); + ring.setMaxSize(16, 16); ring.progressProperty() .bind(Bindings.createDoubleBinding( () -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy()))); var image = DataStoreProviders.byStore(model.getStore()).getDisplayIconFileName(model.getStore()); - var logo = new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion(); + var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion(); - var label = new Label(model.getName()); - label.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - label.addEventHandler( - DragEvent.DRAG_ENTERED, - mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab))); - - label.graphicProperty() + tab.graphicProperty() .bind(Bindings.createObjectBinding( () -> { return model.getBusy().get() ? ring : logo; }, PlatformThread.sync(model.getBusy()))); - - tab.setGraphic(label); - new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(label); - GrowAugment.create(true, false).augment(new SimpleCompStructure<>(label)); - tab.setContent(new OpenFileSystemComp(model).createSimple()); tab.setText(model.getName()); + + // new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(tab); + tab.setContent(new OpenFileSystemComp(model).createSimple()); + + var id = UUID.randomUUID().toString(); + tab.setId(id); + + var found = tabs.lookupAll("tab-header-area"); + SimpleChangeListener.apply(tabs.skinProperty(), newValue -> { + if (newValue != null) { + Platform.runLater(() -> { + Label l = (Label) tabs.lookup("#" + id + " .tab-label"); + var w = l.maxWidthProperty(); + l.minWidthProperty().bind(w); + l.prefWidthProperty().bind(w); + + var close = (StackPane) tabs.lookup("#" + id + " .tab-close-button"); + close.setPrefWidth(30); + + StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); + new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); + c.addEventHandler( + DragEvent.DRAG_ENTERED, + mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab))); + }); + } + }); + return tab; } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index 311404ff..6f995e40 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -7,9 +7,9 @@ import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; -import io.xpipe.app.fxcomps.impl.SvgCacheComp; +import io.xpipe.app.fxcomps.impl.PrettySvgComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.impl.FileNames; @@ -423,8 +423,7 @@ final class BrowserFileListComp extends SimpleComp { private final StringProperty img = new SimpleStringProperty(); private final StringProperty text = new SimpleStringProperty(); - private final Node imageView = new SvgCacheComp( - new SimpleDoubleProperty(24), new SimpleDoubleProperty(24), img, FileIconManager.getSvgCache()) + private final Node imageView = new PrettySvgComp(img, 24, 24) .createRegion(); private final StackPane textField = new LazyTextFieldComp(text).createStructure().get(); @@ -473,7 +472,7 @@ final class BrowserFileListComp extends SimpleComp { return; } - try (var ignored = new BusyProperty(updating)) { + try (var ignored = new BooleanScope(updating).start()) { super.updateItem(newName, empty); if (empty || newName == null || getTableRow().getItem() == null) { // Don't set image as that would trigger image comp update diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java index 7781f627..bce6612e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java @@ -120,7 +120,7 @@ public final class BrowserFileListModel { } if (exists) { - ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").unreportable().handle(); + ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle(); fileSystemModel.refresh(); return false; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java index 7ee5abd8..e876e07e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -2,7 +2,7 @@ package io.xpipe.app.browser; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.impl.FileStore; import io.xpipe.core.store.ShellStore; @@ -78,8 +78,7 @@ public class BrowserModel { public void restoreState(BrowserSavedState.Entry e, BooleanProperty busy) { var storageEntry = DataStorage.get().getStoreEntry(e.getUuid()); storageEntry.ifPresent(entry -> { - openFileSystemAsync( - entry.getName(), entry.getStore().asNeeded(), e.getPath(), busy); + openFileSystemAsync(null, entry.getStore().asNeeded(), e.getPath(), busy); }); } @@ -177,7 +176,7 @@ public class BrowserModel { // Prevent multiple calls from interfering with each other synchronized (BrowserModel.this) { - try (var b = new BusyProperty(externalBusy != null ? externalBusy : new SimpleBooleanProperty())) { + try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) { model = new OpenFileSystemModel(name, this, store); model.initFileSystem(); model.initSavedState(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index 9a16b8c1..4cc62d6f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -6,12 +6,9 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; -import io.xpipe.app.fxcomps.impl.HorizontalComp; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; -import io.xpipe.app.fxcomps.impl.StackComp; -import io.xpipe.app.fxcomps.impl.TextFieldComp; +import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.util.SimpleChangeListener; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -46,7 +43,7 @@ public class BrowserNavBar extends SimpleComp { }); path.addListener((observable, oldValue, newValue) -> { ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(model.getBusy(), () -> { + BooleanScope.execute(model.getBusy(), () -> { var changed = model.cdSyncOrRetry(newValue, true); changed.ifPresent(s -> Platform.runLater(() -> path.set(s))); }); @@ -94,7 +91,7 @@ public class BrowserNavBar extends SimpleComp { : "home_icon.svg"; }, model.getCurrentPath()); - var breadcrumbsGraphic = new PrettyImageComp(graphic, 22, 22) + var breadcrumbsGraphic = PrettyImageHelper.ofSvg(graphic, 22, 22) .padding(new Insets(0, 0, 1, 0)) .styleClass("path-graphic") .createRegion(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java index 9df516a6..ab5f45ce 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java @@ -6,11 +6,10 @@ import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.SvgCacheComp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileSystem; -import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; @@ -19,7 +18,6 @@ import javafx.scene.SnapshotParameters; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; import javafx.scene.image.Image; -import javafx.scene.image.WritableImage; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import lombok.AllArgsConstructor; @@ -54,11 +52,7 @@ public class BrowserSelectionListComp extends SimpleComp { protected Region createSimple() { var c = new ListBoxViewComp<>(list, list, entry -> { return Comp.of(() -> { - var wv = new SvgCacheComp( - new SimpleDoubleProperty(20), - new SimpleDoubleProperty(20), - new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)), - FileIconManager.getSvgCache()) + var wv = PrettyImageHelper.ofFixedSquare(FileIconManager.getFileIcon(entry, false), 20) .createRegion(); var l = new Label(null, wv); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index 1f424031..11a2f0f5 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -4,7 +4,7 @@ import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.augment.DragPseudoClassAugment; +import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; @@ -82,7 +82,7 @@ public class BrowserTransferComp extends SimpleComp { var listBox = new VerticalComp(List.of(list, dragNotice)).padding(new Insets(10, 10, 5, 10)); var stack = new LoadingOverlayComp( new StackComp(List.of(backgroundStack, listBox, clearPane)) - .apply(DragPseudoClassAugment.create()) + .apply(DragOverPseudoClassAugment.create()) .apply(struc -> { struc.get().setOnDragOver(event -> { // Accept drops from inside the app window diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java index be8a71c9..df32461a 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java @@ -1,7 +1,7 @@ package io.xpipe.app.browser; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileSystem; import javafx.beans.property.BooleanProperty; @@ -82,7 +82,7 @@ public class BrowserTransferModel { } try { - try (var b = new BusyProperty(downloading)) { + try (var b = new BooleanScope(downloading).start()) { FileSystemHelper.dropFilesInto( FileSystemHelper.getLocal(TEMP), List.of(item.getFileEntry()), diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java index 38a2b253..1167800f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -1,23 +1,22 @@ package io.xpipe.app.browser; +import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Tile; import atlantafx.base.theme.Styles; import io.xpipe.app.comp.base.TileButtonComp; import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.storage.DataStorage; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Orientation; -import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; +import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -import org.kordamp.ikonli.javafx.FontIcon; public class BrowserWelcomeComp extends SimpleComp { @@ -33,15 +32,18 @@ public class BrowserWelcomeComp extends SimpleComp { var welcome = new BrowserGreetingComp().createSimple(); - var vbox = new VBox(welcome); - vbox.setMaxWidth(700); - vbox.setPadding(new Insets(40, 40, 40, 50)); - vbox.setSpacing(18); + var vbox = new VBox(welcome, new Spacer(Orientation.VERTICAL)); + + var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75).padding(new Insets(5, 0, 0, 0)).createRegion(); + var hbox = new HBox(img, vbox); + hbox.setSpacing(15); + if (state == null) { - var header = new Label("Here you will be able to see were you left off last time you exited XPipe."); + var header = new Label("Here you will be able to see where you left off last time you exited XPipe."); AppFont.header(header); vbox.getChildren().add(header); - return vbox; + hbox.setPadding(new Insets(40, 40, 40, 50)); + return new VBox(hbox); } var header = new Label("Last time you were connected to the following systems:"); @@ -51,7 +53,7 @@ public class BrowserWelcomeComp extends SimpleComp { var storeList = new VBox(); storeList.setSpacing(8); - state.getLastSystems().forEach(e-> { + state.getLastSystems().forEach(e -> { var entry = DataStorage.get().getStoreEntry(e.getUuid()); if (entry.isEmpty()) { return; @@ -63,30 +65,36 @@ public class BrowserWelcomeComp extends SimpleComp { var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore()); - var view = new PrettyImageComp(new SimpleStringProperty(graphic), 45, 45); - var openButton = new Button(null, new FontIcon("mdmz-restore")); - new FancyTooltipAugment<>("restore").augment(openButton); - openButton.getStyleClass().addAll(Styles.FLAT, Styles.BUTTON_CIRCLE); - openButton.setOnAction(event -> { - model.restoreState(e, openButton.disableProperty()); - event.consume(); + var view = PrettyImageHelper.ofFixedSquare(graphic, 45); + view.padding(new Insets(2, 8, 2, 8)); + var tile = new Tile( + DataStorage.get().getStoreBrowserDisplayName(entry.get().getStore()), + e.getPath(), + view.createRegion()); + tile.setActionHandler(() -> { + model.restoreState(e, tile.disableProperty()); }); - var tile = new Tile(entry.get().getName(), e.getPath(), view.createRegion()); - tile.setAction(openButton); storeList.getChildren().add(tile); }); var sp = new ScrollPane(storeList); sp.setFitToWidth(true); - vbox.getChildren().add(sp); - vbox.getChildren().add(new Separator(Orientation.HORIZONTAL)); + + var layout = new VBox(); + layout.setMaxWidth(700); + layout.setPadding(new Insets(40, 40, 40, 50)); + layout.setSpacing(18); + layout.getChildren().add(hbox); + layout.getChildren().add(new Separator(Orientation.HORIZONTAL)); + layout.getChildren().add(sp); + layout.getChildren().add(new Separator(Orientation.HORIZONTAL)); var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> { model.restoreState(state); actionEvent.consume(); }).grow(true, false); - vbox.getChildren().add(tile.createRegion()); + layout.getChildren().add(tile.createRegion()); - return vbox; + return layout; } } diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java index e1b1efb3..e87a779f 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -3,18 +3,18 @@ package io.xpipe.app.browser; import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.impl.FileNames; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.store.*; +import io.xpipe.core.util.FailableConsumer; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import lombok.Getter; import lombok.SneakyThrows; -import org.apache.commons.lang3.function.FailableConsumer; import java.io.IOException; import java.nio.file.Path; @@ -46,7 +46,7 @@ public final class OpenFileSystemModel { this.browserModel = browserModel; this.store = store; var e = DataStorage.get().getStoreEntryIfPresent(store); - this.name = name != null ? name : e.isPresent() ? e.get().getName() : "?"; + this.name = name != null ? name : e.isPresent() ? DataStorage.get().getStoreBrowserDisplayName(store) : "?"; this.tooltip = e.isPresent() ? DataStorage.get().getId(e.get()).toString() : name; this.inOverview.bind(Bindings.createBooleanBinding( () -> { @@ -62,7 +62,7 @@ public final class OpenFileSystemModel { return; } - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (store instanceof ShellStore s) { c.accept(fileSystem.getShell().orElseThrow()); if (refresh) { @@ -75,7 +75,7 @@ public final class OpenFileSystemModel { @SneakyThrows public void refresh() { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { cdSyncWithoutCheck(currentPath.get()); }); } @@ -112,7 +112,7 @@ public final class OpenFileSystemModel { public void cdAsync(String path) { ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { cdSync(path); }); }); @@ -238,7 +238,7 @@ public final class OpenFileSystemModel { public void dropLocalFilesIntoAsync(FileSystem.FileEntry entry, List files) { ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (fileSystem == null) { return; } @@ -257,7 +257,7 @@ public final class OpenFileSystemModel { } ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (fileSystem == null) { return; } @@ -285,7 +285,7 @@ public final class OpenFileSystemModel { } ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (fileSystem == null) { return; } @@ -307,7 +307,7 @@ public final class OpenFileSystemModel { } ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (fileSystem == null) { return; } @@ -329,7 +329,7 @@ public final class OpenFileSystemModel { } ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (fileSystem == null) { return; } @@ -363,7 +363,7 @@ public final class OpenFileSystemModel { } public void initFileSystem() throws Exception { - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { var fs = store.createFileSystem(); fs.open(); this.fileSystem = fs; @@ -392,7 +392,7 @@ public final class OpenFileSystemModel { return; } - BusyProperty.execute(busy, () -> { + BooleanScope.execute(busy, () -> { if (store instanceof ShellStore s) { var connection = ((ConnectionFileSystem) fileSystem).getShellControl(); var command = s.control() diff --git a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java index 31ae618d..20ee866c 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -4,7 +4,7 @@ import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.util.Shortcuts; -import io.xpipe.app.util.BusyProperty; +import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import javafx.beans.property.SimpleStringProperty; import javafx.scene.control.Button; @@ -21,7 +21,7 @@ public interface LeafAction extends BrowserAction { var b = new Button(); b.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(model.getBusy(), () -> { + BooleanScope.execute(model.getBusy(), () -> { execute(model, selected); }); }); @@ -50,7 +50,7 @@ public interface LeafAction extends BrowserAction { var mi = new MenuItem(nameFunc.apply(getName(model, selected))); mi.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - BusyProperty.execute(model.getBusy(), () -> { + BooleanScope.execute(model.getBusy(), () -> { execute(model, selected); }); }); 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 40dda213..b17b035e 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 @@ -1,23 +1,24 @@ package io.xpipe.app.browser.icon; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.core.store.FileSystem; -import javafx.beans.property.SimpleStringProperty; public class BrowserIcons { - public static PrettyImageComp createDefaultFileIcon() { - return new PrettyImageComp(new SimpleStringProperty("default_file.svg"), 22, 22); + + public static Comp createDefaultFileIcon() { + return PrettyImageHelper.ofFixedSquare("default_file.svg", 22); } - public static PrettyImageComp createDefaultDirectoryIcon() { - return new PrettyImageComp(new SimpleStringProperty("default_folder.svg"), 22, 22); + public static Comp createDefaultDirectoryIcon() { + return PrettyImageHelper.ofFixedSquare("default_folder.svg", 22); } - public static PrettyImageComp createIcon(FileType type) { - return new PrettyImageComp(new SimpleStringProperty(type.getIcon()), 22, 22); + public static Comp createIcon(FileType type) { + return PrettyImageHelper.ofFixedSquare(type.getIcon(), 22); } - public static PrettyImageComp createIcon(FileSystem.FileEntry entry) { - return new PrettyImageComp(new SimpleStringProperty(FileIconManager.getFileIcon(entry, false)), 22, 22); + public static Comp createIcon(FileSystem.FileEntry entry) { + return PrettyImageHelper.ofFixedSquare(FileIconManager.getFileIcon(entry, false), 22); } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java index ccb7f46f..fd5c74b1 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java @@ -2,40 +2,13 @@ package io.xpipe.app.browser.icon; import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppResources; -import io.xpipe.app.fxcomps.impl.SvgCache; import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileSystem; -import javafx.scene.image.Image; -import lombok.Getter; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; public class FileIconManager { - @Getter - private static final SvgCache svgCache = createCache(); - private static boolean loaded; - private static SvgCache createCache() { - return new SvgCache() { - - private final Map images = new HashMap<>(); - - @Override - public synchronized void put(String image, Image value) { - images.put(image, value); - } - - @Override - public synchronized Optional getCached(String image) { - return Optional.ofNullable(images.get(image)); - } - }; - } - public static synchronized void loadIfNecessary() { if (!loaded) { AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons"); diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java index a1dd3249..08c3e18b 100644 --- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java @@ -1,7 +1,9 @@ package io.xpipe.app.comp; +import io.xpipe.app.comp.base.BackgroundImageComp; import io.xpipe.app.comp.base.SideMenuBarComp; import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; @@ -10,17 +12,17 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import java.util.HashMap; import java.util.Map; -public class AppLayoutComp extends Comp> { +public class AppLayoutComp extends Comp> { private final AppLayoutModel model = AppLayoutModel.get(); - @Override - public CompStructure createBase() { + public CompStructure createBase() { var map = new HashMap(); getRegion(model.getEntries().get(0), map); getRegion(model.getEntries().get(1), map); @@ -39,7 +41,12 @@ public class AppLayoutComp extends Comp> { }); }); AppFont.normal(pane); - return new SimpleCompStructure<>(pane); + + var bg = new BackgroundImageComp(AppImages.image("bg.png")) + .styleClass("background") + .hide(AppPrefs.get().performanceMode()); + + return new SimpleCompStructure<>(new StackPane(bg.createRegion(), pane)); } private Region getRegion(AppLayoutModel.Entry entry, Map map) { diff --git a/app/src/main/java/io/xpipe/app/comp/DeveloperTabComp.java b/app/src/main/java/io/xpipe/app/comp/DeveloperTabComp.java index 0cd955ac..181ebcc2 100644 --- a/app/src/main/java/io/xpipe/app/comp/DeveloperTabComp.java +++ b/app/src/main/java/io/xpipe/app/comp/DeveloperTabComp.java @@ -33,6 +33,10 @@ public class DeveloperTabComp extends SimpleComp { System.exit(0); }); + var button6 = new ButtonComp(AppI18n.observable("Restart"), null, () -> { + OperationMode.restart(); + }); + var button4 = new ButtonComp(AppI18n.observable("Throw terminal exception"), null, () -> { try { throw new IllegalStateException(); @@ -48,6 +52,7 @@ public class DeveloperTabComp extends SimpleComp { button2.createRegion(), button3.createRegion(), button4.createRegion(), - button5.createRegion()); + button5.createRegion(), + button6.createRegion()); } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/CountComp.java b/app/src/main/java/io/xpipe/app/comp/base/CountComp.java index a420bd80..2861f367 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/CountComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/CountComp.java @@ -10,14 +10,22 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.OverrunStyle; +import java.util.function.Function; + public class CountComp extends Comp> { private final ObservableList sub; private final ObservableList all; + private final Function transformation; public CountComp(ObservableList sub, ObservableList all) { + this(sub, all, Function.identity()); + } + + public CountComp(ObservableList sub, ObservableList all, Function transformation) { this.sub = PlatformThread.sync(sub); this.all = PlatformThread.sync(all); + this.transformation = transformation; } @Override @@ -29,9 +37,9 @@ public class CountComp extends Comp> { .bind(Bindings.createStringBinding( () -> { if (sub.size() == all.size()) { - return all.size() + ""; + return transformation.apply(all.size() + ""); } else { - return sub.size() + "/" + all.size(); + return transformation.apply(sub.size() + "/" + all.size()); } }, sub, 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 new file mode 100644 index 00000000..5431a87e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java @@ -0,0 +1,68 @@ +package io.xpipe.app.comp.base; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.css.Size; +import javafx.css.SizeUnits; +import javafx.scene.Node; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class DropdownComp extends Comp >{ + + private final ObservableValue name; + private final ObjectProperty graphic; + private final List> items; + + public DropdownComp(ObservableValue name, List> items) { + this.name = name; + this.graphic = new SimpleObjectProperty<>(null); + this.items = items; + } + + public DropdownComp(ObservableValue name, Node graphic, List> items) { + this.name = name; + this.graphic = new SimpleObjectProperty<>(graphic); + this.items = items; + } + + public Node getGraphic() { + return graphic.get(); + } + + public ObjectProperty graphicProperty() { + return graphic; + } + + @Override + public CompStructure createBase() { + var button = new MenuButton(null); + if (name != null) { + button.textProperty().bind(name); + } + var graphic = getGraphic(); + if (graphic instanceof FontIcon f) { + SimpleChangeListener.apply(button.fontProperty(), c -> { + f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); + }); + } + + button.setGraphic(getGraphic()); + button.getStyleClass().add("dropdown-comp"); + + items.forEach(comp -> { + var i = new MenuItem(null,comp.createRegion()); + button.getItems().add(i); + }); + + return new SimpleCompStructure<>(button); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/base/FileDropOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/FileDropOverlayComp.java index 9bdac8cb..eb45a2c0 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/FileDropOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/FileDropOverlayComp.java @@ -3,6 +3,7 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.core.util.FailableConsumer; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.input.Dragboard; @@ -10,7 +11,6 @@ import javafx.scene.input.TransferMode; import javafx.scene.layout.StackPane; import lombok.Builder; import lombok.Value; -import org.apache.commons.lang3.function.FailableConsumer; import org.kordamp.ikonli.javafx.FontIcon; import java.io.File; diff --git a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java index e83cc895..55a97049 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java @@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; +import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.layout.StackPane; import lombok.Builder; @@ -93,7 +94,7 @@ public class LazyTextFieldComp extends Comp { @Builder public static class Structure implements CompStructure { StackPane pane; - JFXTextField textField; + TextField textField; @Override public StackPane get() { diff --git a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java index eed50fe2..90656ef9 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LoadingOverlayComp.java @@ -5,6 +5,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -29,6 +30,7 @@ public class LoadingOverlayComp extends Comp> { var loading = new RingProgressIndicator(0, false); loading.setProgress(-1); + loading.visibleProperty().bind(Bindings.not(AppPrefs.get().performanceMode())); var loadingOverlay = new StackPane(loading); loadingOverlay.getStyleClass().add("loading-comp"); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index 96c3aa9f..bf90e999 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -1,9 +1,5 @@ package io.xpipe.app.comp.base; -import com.vladsch.flexmark.html.HtmlRenderer; -import com.vladsch.flexmark.parser.Parser; -import com.vladsch.flexmark.util.ast.Document; -import com.vladsch.flexmark.util.data.MutableDataSet; import io.xpipe.app.core.AppResources; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; @@ -12,6 +8,7 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.MarkdownHelper; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -44,13 +41,7 @@ public class MarkdownComp extends Comp> { } private String getHtml() { - MutableDataSet options = new MutableDataSet(); - Parser parser = Parser.builder(options).build(); - HtmlRenderer renderer = HtmlRenderer.builder(options).build(); - Document document = parser.parse(markdown.getValue()); - var html = renderer.render(document); - var result = htmlTransformation.apply(html); - return "
" + result + "
"; + return MarkdownHelper.toHtml(markdown.getValue(), htmlTransformation); } @SneakyThrows diff --git a/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java b/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java index 3b4ed107..f7bba479 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/OsLogoComp.java @@ -3,7 +3,7 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.storage.store.StoreEntryWrapper; import io.xpipe.app.core.AppResources; import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.impl.FileNames; @@ -31,7 +31,7 @@ public class OsLogoComp extends SimpleComp { ? getImage(wrapper.getInformation().get()) : null; }, wrapper.getState(), wrapper.getInformation()); - return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), new PrettyImageComp(img, 24, 24))).createRegion(); + return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), PrettyImageHelper.ofSvg(img, 24, 24))).createRegion(); } private static final Map ICONS = new HashMap<>(); @@ -43,7 +43,7 @@ public class OsLogoComp extends SimpleComp { } if (ICONS.isEmpty()) { - AppResources.withResource(AppResources.XPIPE_MODULE, "img/os", ModuleLayer.boot(), file -> { + AppResources.with(AppResources.XPIPE_MODULE, "img/os", file -> { try (var list = Files.list(file)) { list.filter(path -> !path.toString().endsWith(LINUX_DEFAULT)).map(path -> FileNames.getFileName(path.toString())).forEach(path -> { var base = FileNames.getBaseName(path).replace("-dark", "") + ".svg"; diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java index 5dd58a44..bd13ccd6 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java @@ -2,14 +2,18 @@ package io.xpipe.app.comp.base; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.core.AppLogs; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.UserReportComp; import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.XPipeDistributionType; +import io.xpipe.app.util.Hyperlinks; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.css.PseudoClass; @@ -17,7 +21,6 @@ import javafx.scene.control.Button; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; -import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; @@ -52,7 +55,32 @@ public class SideMenuBarComp extends Comp> { }); { - var fi = new FontIcon("mdi2u-update"); + var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB)) + .apply(new FancyTooltipAugment<>("visitGithubRepository")); + b.apply(struc -> { + AppFont.setSize(struc.get(), 2); + }); + vbox.getChildren().add(b.createRegion()); + } + + { + var b = new IconButtonComp( + "mdal-bug_report", + () -> { + var event = ErrorEvent.fromMessage("User Report"); + if (AppLogs.get().isWriteToFile()) { + event.attachment(AppLogs.get().getSessionLogsDirectory()); + } + UserReportComp.show(event.build()); + }) + .apply(new FancyTooltipAugment<>("reportIssue")); + b.apply(struc -> { + AppFont.setSize(struc.get(), 2); + }); + vbox.getChildren().add(b.createRegion()); + } + + { var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) .apply(new FancyTooltipAugment<>("updateAvailableTooltip")); b.apply(struc -> { diff --git a/app/src/main/java/io/xpipe/app/comp/storage/DataStoreTypeComp.java b/app/src/main/java/io/xpipe/app/comp/storage/DataStoreTypeComp.java deleted file mode 100644 index 1eaf31b1..00000000 --- a/app/src/main/java/io/xpipe/app/comp/storage/DataStoreTypeComp.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.xpipe.app.comp.storage; - -import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.core.source.DataSource; -import javafx.beans.binding.Bindings; -import javafx.geometry.Pos; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.kordamp.ikonli.javafx.FontIcon; - -@Value -@EqualsAndHashCode(callSuper = true) -public class DataStoreTypeComp extends SimpleComp { - - DataSource source; - - @Override - protected Region createSimple() { - var icon = new FontIcon("mdoal-insert_drive_file"); - var sp = new StackPane(icon); - sp.setAlignment(Pos.CENTER); - icon.iconSizeProperty().bind(Bindings.divide(sp.heightProperty(), 1)); - sp.getStyleClass().add("data-store-type-comp"); - return sp; - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/StorageFilter.java b/app/src/main/java/io/xpipe/app/comp/storage/StorageFilter.java deleted file mode 100644 index 57817642..00000000 --- a/app/src/main/java/io/xpipe/app/comp/storage/StorageFilter.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.xpipe.app.comp.storage; - -import io.xpipe.app.fxcomps.util.SimpleChangeListener; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ObservableValue; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; - -import java.util.ArrayList; -import java.util.Comparator; - -public class StorageFilter { - - private final StringProperty filter = new SimpleStringProperty(""); - - public void createFilterBinding( - ObservableList all, ObservableList shown, ObservableValue> order) { - all.addListener((ListChangeListener) lc -> { - update(all, shown, order.getValue()); - }); - - SimpleChangeListener.apply(filter, n -> { - update(all, shown, order.getValue()); - }); - - order.addListener((observable, oldValue, newValue) -> { - update(all, shown, newValue); - }); - } - - private void update(ObservableList all, ObservableList shown, Comparator order) { - var updatedShown = new ArrayList<>(shown); - updatedShown.removeIf(e -> !all.contains(e) || !e.shouldShow(filter.get())); - for (var e : all) { - if (!updatedShown.contains(e) && e.shouldShow(filter.get())) { - updatedShown.add(e); - } - } - updatedShown.sort(order); - shown.setAll(updatedShown); - } - - public String getFilter() { - return filter.get(); - } - - public StringProperty filterProperty() { - return filter; - } - - public interface Filterable { - - boolean shouldShow(String filter); - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java index a2366d34..c84a5fff 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/DenseStoreEntryComp.java @@ -1,10 +1,14 @@ package io.xpipe.app.comp.storage.store; +import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.augment.GrowAugment; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Label; import javafx.scene.layout.*; public class DenseStoreEntryComp extends StoreEntryComp { @@ -16,6 +20,29 @@ public class DenseStoreEntryComp extends StoreEntryComp { this.showIcon = showIcon; } + private Label createInformation(GridPane grid) { + var information = new Label(); + information.setGraphicTextGap(7); + information.getStyleClass().add("information"); + AppFont.header(information); + + var state = wrapper.getEntry().getProvider() != null + ? wrapper.getEntry().getProvider().stateDisplay(wrapper) + : Comp.empty(); + information.setGraphic(state.createRegion()); + + SimpleChangeListener.apply(grid.hoverProperty(), val -> { + if (val && wrapper.getSummary().get() != null && wrapper.getEntry().getProvider().alwaysShowSummary()) { + information.textProperty().bind(PlatformThread.sync(wrapper.getSummary())); + } else { + information.textProperty().bind(PlatformThread.sync(wrapper.getInformation())); + + } + }); + + return information; + } + protected Region createContent() { var name = createName().createRegion(); @@ -24,7 +51,7 @@ public class DenseStoreEntryComp extends StoreEntryComp { if (showIcon) { var storeIcon = createIcon(30, 25); - grid.getColumnConstraints().add(new ColumnConstraints(32)); + grid.getColumnConstraints().add(new ColumnConstraints(46)); grid.add(storeIcon, 0, 0); GridPane.setHalignment(storeIcon, HPos.CENTER); } @@ -33,9 +60,9 @@ public class DenseStoreEntryComp extends StoreEntryComp { var custom = new ColumnConstraints(0, customSize, customSize); custom.setHalignment(HPos.RIGHT); - var info = new ColumnConstraints(); - info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH); - info.setHalignment(HPos.LEFT); + var infoCC = new ColumnConstraints(); + infoCC.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH); + infoCC.setHalignment(HPos.LEFT); var nameCC = new ColumnConstraints(); nameCC.setMinWidth(100); @@ -43,8 +70,9 @@ public class DenseStoreEntryComp extends StoreEntryComp { grid.getColumnConstraints().addAll(nameCC); grid.addRow(0, name); - grid.addRow(0, createInformation()); - grid.getColumnConstraints().addAll(info, custom); + var info = createInformation(grid); + grid.addRow(0, info); + grid.getColumnConstraints().addAll(infoCC, custom); var cr = content != null ? content.createRegion() : new Region(); var bb = createButtonBar().createRegion(); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java index e8909911..b6cf75ed 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StandardStoreEntryComp.java @@ -22,7 +22,7 @@ public class StandardStoreEntryComp extends StoreEntryComp { var storeIcon = createIcon(50, 39); grid.add(storeIcon, 0, 0, 1, 2); - grid.getColumnConstraints().add(new ColumnConstraints(50)); + grid.getColumnConstraints().add(new ColumnConstraints(66)); grid.add(name, 1, 0); grid.add(createSummary(), 1, 1); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java new file mode 100644 index 00000000..5ad1f063 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCategoryWrapper.java @@ -0,0 +1,145 @@ +package io.xpipe.app.comp.storage.store; + +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreCategory; +import io.xpipe.app.storage.DataStoreEntry; +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import lombok.Getter; + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; + +@Getter +public class StoreCategoryWrapper { + + private final int depth; + private final Property name; + private final DataStoreCategory category; + private final Property lastAccess; + private final Property sortMode; + private final Property share; + private final ObservableList children; + private final ObservableList containedEntries; + + public StoreCategoryWrapper(DataStoreCategory category) { + var d = 0; + DataStoreCategory p = category; + while ((p = DataStorage.get() + .getStoreCategoryIfPresent(p.getParentCategory()) + .orElse(null)) + != null) { + d++; + } + depth = d; + + this.category = category; + this.name = new SimpleStringProperty(); + this.lastAccess = new SimpleObjectProperty<>(); + this.sortMode = new SimpleObjectProperty<>(); + this.share = new SimpleObjectProperty<>(); + this.children = FXCollections.observableArrayList(); + this.containedEntries = FXCollections.observableArrayList(); + setupListeners(); + update(); + } + + public StoreCategoryWrapper getParent() { + return StoreViewState.get().getCategories().stream() + .filter(storeCategoryWrapper -> + storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory())) + .findAny().orElse(null); + } + + public boolean contains(DataStoreEntry entry) { + return entry.getCategoryUuid().equals(category.getUuid()) + || children.stream().anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)); + } + + public void select() { + Platform.runLater(() -> { + StoreViewState.get().getActiveCategory().setValue(this); + }); + } + + public void delete() { + DataStorage.get().deleteStoreCategory(category); + } + + private void setupListeners() { + name.addListener((c, o, n) -> { + category.setName(n); + }); + + category.addListener(() -> PlatformThread.runLaterIfNeeded(() -> { + update(); + })); + + sortMode.addListener((observable, oldValue, newValue) -> { + category.setSortMode(newValue); + }); + + share.addListener((observable, oldValue, newValue) -> { + category.setShare(newValue); + + DataStoreCategory p = category; + if (newValue) { + while ((p = DataStorage.get() + .getStoreCategoryIfPresent(p.getParentCategory()) + .orElse(null)) + != null) { + p.setShare(true); + } + } + }); + } + + public void update() { + // Avoid reupdating name when changed from the name property! + if (!category.getName().equals(name.getValue())) { + name.setValue(category.getName()); + } + + lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500))); + sortMode.setValue(category.getSortMode()); + share.setValue(category.isShare()); + + if (StoreViewState.get() != null) { + containedEntries.setAll(StoreViewState.get().getAllEntries().stream() + .filter(entry -> contains(entry.getEntry())) + .toList()); + children.setAll(StoreViewState.get().getCategories().stream() + .filter(storeCategoryWrapper -> getCategory() + .getUuid() + .equals(storeCategoryWrapper.getCategory().getParentCategory())) + .toList()); + + Optional.ofNullable(getParent()) + .ifPresent(storeCategoryWrapper -> { + storeCategoryWrapper.update(); + }); + } + } + + public String getName() { + return name.getValue(); + } + + public Property nameProperty() { + return name; + } + + public Instant getLastAccess() { + return lastAccess.getValue(); + } + + public Property lastAccessProperty() { + return lastAccess; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationBarComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationBarComp.java deleted file mode 100644 index c398f74a..00000000 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationBarComp.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.xpipe.app.comp.storage.store; - -import atlantafx.base.theme.Styles; -import io.xpipe.app.comp.base.ButtonComp; -import io.xpipe.app.comp.store.GuiDsStoreCreator; -import io.xpipe.app.core.AppFont; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.DataStoreProvider; -import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; -import io.xpipe.app.fxcomps.impl.VerticalComp; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; -import javafx.scene.layout.Region; -import org.kordamp.ikonli.javafx.FontIcon; - -import java.util.List; - -public class StoreCreationBarComp extends SimpleComp { - - @Override - protected Region createSimple() { - var newStreamStore = new ButtonComp( - AppI18n.observable("addCommand"), new FontIcon("mdi2c-code-greater-than"), () -> { - GuiDsStoreCreator.showCreation( - v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.COMMAND)); - }) - .styleClass(Styles.FLAT) - .shortcut(new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN)) - .apply(new FancyTooltipAugment<>("addCommand")); - - var newHostStore = new ButtonComp(AppI18n.observable("addHost"), new FontIcon("mdi2h-home-plus"), () -> { - GuiDsStoreCreator.showCreation( - v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.HOST)); - }) - .styleClass(Styles.FLAT) - .shortcut(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN)) - .apply(new FancyTooltipAugment<>("addHost")); - - var newShellStore = new ButtonComp( - AppI18n.observable("addShell"), new FontIcon("mdi2t-text-box-multiple"), () -> { - GuiDsStoreCreator.showCreation( - v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.SHELL)); - }) - .styleClass(Styles.FLAT) - .shortcut(new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN)) - .apply(new FancyTooltipAugment<>("addShell")); - - var newDbStore = new ButtonComp(AppI18n.observable("addDatabase"), new FontIcon("mdi2d-database-plus"), () -> { - GuiDsStoreCreator.showCreation( - v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.DATABASE)); - }) - .styleClass(Styles.FLAT) - .shortcut(new KeyCodeCombination(KeyCode.D, KeyCombination.SHORTCUT_DOWN)) - .apply(new FancyTooltipAugment<>("addDatabase")); - - var newTunnelStore = new ButtonComp(AppI18n.observable("addTunnel"), new FontIcon("mdi2v-vector-polyline-plus"), () -> { - GuiDsStoreCreator.showCreation( - v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.TUNNEL)); - }) - .styleClass(Styles.FLAT) - .shortcut(new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN)) - .apply(new FancyTooltipAugment<>("addTunnel")); - - var box = new VerticalComp(List.of(newHostStore, newShellStore, newStreamStore, newDbStore, newTunnelStore)) - .apply(struc -> struc.get().setFillWidth(true)); - box.apply(s -> AppFont.medium(s.get())); - var bar = box.createRegion(); - bar.getStyleClass().add("bar"); - bar.getStyleClass().add("store-creation-bar"); - return bar; - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationMenu.java new file mode 100644 index 00000000..ede1106b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreCreationMenu.java @@ -0,0 +1,85 @@ +package io.xpipe.app.comp.storage.store; + +import io.xpipe.app.comp.store.GuiDsStoreCreator; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.DataStoreProviders; +import io.xpipe.app.util.ScanAlert; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; +import org.kordamp.ikonli.javafx.FontIcon; + +public class StoreCreationMenu { + + public static void addButtons(MenuButton menu) { + { + var automatically = new MenuItem(); + automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline")); + automatically.textProperty().bind(AppI18n.observable("addAutomatically")); + automatically.setOnAction(event -> { + ScanAlert.showAsync(null); + event.consume(); + }); + menu.getItems().add(automatically); + menu.getItems().add(new SeparatorMenuItem()); + } + + { + var host = new MenuItem(); + host.setGraphic(new FontIcon("mdi2h-home-plus")); + host.textProperty().bind(AppI18n.observable("addHost")); + host.setOnAction(event -> { + GuiDsStoreCreator.showCreation(DataStoreProviders.byName("ssh").orElseThrow(), + v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.HOST)); + event.consume(); + }); + menu.getItems().add(host); + } + { + var shell = new MenuItem(); + shell.setGraphic(new FontIcon("mdi2t-text-box-multiple")); + shell.textProperty().bind(AppI18n.observable("addShell")); + shell.setOnAction(event -> { + GuiDsStoreCreator.showCreation(null, + v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.SHELL)); + event.consume(); + }); + menu.getItems().add(shell); + } + { + var cmd = new MenuItem(); + cmd.setGraphic(new FontIcon("mdi2c-code-greater-than")); + cmd.textProperty().bind(AppI18n.observable("addCommand")); + cmd.setOnAction(event -> { + GuiDsStoreCreator.showCreation(null, + v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.COMMAND)); + event.consume(); + }); + menu.getItems().add(cmd); + } + { + var db = new MenuItem(); + db.setGraphic(new FontIcon("mdi2d-database-plus")); + db.textProperty().bind(AppI18n.observable("addDatabase")); + db.setOnAction(event -> { + GuiDsStoreCreator.showCreation(null, + v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.DATABASE)); + event.consume(); + }); + menu.getItems().add(db); + } + { + var tunnel = new MenuItem(); + tunnel.setGraphic(new FontIcon("mdi2v-vector-polyline-plus")); + tunnel.textProperty().bind(AppI18n.observable("addTunnel")); + tunnel.setOnAction(event -> { + GuiDsStoreCreator.showCreation(null, + v -> v.getDisplayCategory().equals(DataStoreProvider.DisplayCategory.TUNNEL)); + event.consume(); + }); + menu.getItems().add(tunnel); + } + } + +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java index d18e5973..2b845e70 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java @@ -1,9 +1,9 @@ package io.xpipe.app.comp.storage.store; import atlantafx.base.theme.Styles; -import com.jfoenix.controls.JFXButton; import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.core.App; +import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ActionProvider; @@ -13,11 +13,14 @@ import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.*; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.DesktopHelper; +import io.xpipe.app.util.DesktopShortcuts; import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; @@ -26,20 +29,37 @@ import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; import javafx.scene.control.*; import javafx.scene.input.MouseButton; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import org.kordamp.ikonli.javafx.FontIcon; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; import java.util.ArrayList; public abstract class StoreEntryComp extends SimpleComp { - public static Comp customSection(StoreSection e) { + public static StoreEntryComp create( + StoreEntryWrapper entry, boolean showIcon, Comp content, boolean preferLarge) { + if (!preferLarge) { + return new DenseStoreEntryComp(entry, showIcon, content); + } else { + return new StandardStoreEntryComp(entry, content); + } + } + + public static Comp customSection(StoreSection e, boolean topLevel) { var prov = e.getWrapper().getEntry().getProvider(); if (prov != null) { - return prov.customDisplay(e); + return prov.customEntryComp(e, topLevel); } else { return new StandardStoreEntryComp(e.getWrapper(), null); } @@ -47,8 +67,10 @@ public abstract class StoreEntryComp extends SimpleComp { public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed"); public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete"); - public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = App.getApp().getStage().widthProperty().divide(2.2); - public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = App.getApp().getStage().widthProperty().divide(2.2).add(-300); + public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = + App.getApp().getStage().widthProperty().divide(2.2); + public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = + App.getApp().getStage().widthProperty().divide(2.2).add(-300); protected final StoreEntryWrapper wrapper; protected final Comp content; @@ -61,7 +83,7 @@ public abstract class StoreEntryComp extends SimpleComp { protected final Region createSimple() { var r = createContent(); - var button = new JFXButton(); + var button = new Button(); button.setGraphic(r); GrowAugment.create(true, false).augment(new SimpleCompStructure<>(r)); button.getStyleClass().add("store-entry-comp"); @@ -83,7 +105,10 @@ public abstract class StoreEntryComp extends SimpleComp { }); new ContextMenuAugment<>(() -> this.createContextMenu()).augment(new SimpleCompStructure<>(button)); - var loading = new LoadingOverlayComp(Comp.of(() -> button), wrapper.getValidating()); + var loading = new LoadingOverlayComp( + Comp.of(() -> button), + BindingsHelper.persist( + wrapper.getInRefresh().and(wrapper.getObserving().not()))); return loading.createRegion(); } @@ -163,16 +188,22 @@ public abstract class StoreEntryComp extends SimpleComp { : wrapper.getEntry() .getProvider() .getDisplayIconFileName(wrapper.getEntry().getStore()); - var imageComp = new PrettyImageComp(new SimpleStringProperty(img), w, h); + var imageComp = PrettyImageHelper.ofFixed(img, w, h); var storeIcon = imageComp.createRegion(); - storeIcon.getStyleClass().add("icon"); if (wrapper.getState().getValue().isUsable()) { new FancyTooltipAugment<>(new SimpleStringProperty( - wrapper.getEntry().getProvider().getDisplayName())) + wrapper.getEntry().getProvider().getDisplayName())) .augment(storeIcon); } - storeIcon.setPadding(new Insets(3, 0, 0, 0)); - return storeIcon; + + var stack = new StackPane(storeIcon); + stack.setMinHeight(w + 7); + stack.setMinWidth(w + 7); + stack.setMaxHeight(w + 7); + stack.setMaxWidth(w + 7); + stack.getStyleClass().add("icon"); + stack.setAlignment(Pos.CENTER); + return stack; } protected Comp createButtonBar() { @@ -227,9 +258,9 @@ public abstract class StoreEntryComp extends SimpleComp { } protected Comp createSettingsButton() { - var settingsButton = new IconButtonComp("mdomz-settings"); + var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", () -> {}); settingsButton.styleClass("settings"); - settingsButton.accessibleText("Settings"); + settingsButton.accessibleText("More"); settingsButton.apply(new ContextMenuAugment<>( event -> event.getButton() == MouseButton.PRIMARY, () -> StoreEntryComp.this.createContextMenu())); settingsButton.apply(new FancyTooltipAugment<>("more")); @@ -240,22 +271,38 @@ public abstract class StoreEntryComp extends SimpleComp { var contextMenu = new ContextMenu(); AppFont.normal(contextMenu.getStyleableNode()); + var hasSep = false; for (var p : wrapper.getActionProviders().entrySet()) { var actionProvider = p.getKey().getDataStoreCallSite(); if (actionProvider.isMajor(wrapper.getEntry().getStore().asNeeded())) { continue; } + if (actionProvider.isSystemAction() && !hasSep) { + if (contextMenu.getItems().size() > 0) { + contextMenu.getItems().add(new SeparatorMenuItem()); + } + hasSep = true; + } + var name = actionProvider.getName(wrapper.getEntry().getStore().asNeeded()); var icon = actionProvider.getIcon(wrapper.getEntry().getStore().asNeeded()); - var item = new MenuItem(null, new FontIcon(icon)); - item.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - var action = actionProvider.createAction( - wrapper.getEntry().getStore().asNeeded()); - action.execute(); + var item = actionProvider.canLinkTo() + ? new Menu(null, new FontIcon(icon)) + : new MenuItem(null, new FontIcon(icon)); + Menu menu = actionProvider.canLinkTo() ? (Menu) item : null; + item.setOnAction(event -> { + if (menu != null && !event.getTarget().equals(menu)) { + return; + } + + contextMenu.hide(); + ThreadHelper.runFailableAsync(() -> { + var action = actionProvider.createAction( + wrapper.getEntry().getStore().asNeeded()); + action.execute(); + }); }); - }); item.textProperty().bind(name); if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { item.visibleProperty().bind(p.getValue()); @@ -263,19 +310,57 @@ public abstract class StoreEntryComp extends SimpleComp { item.disableProperty().bind(Bindings.not(p.getValue())); } contextMenu.getItems().add(item); + + if (menu != null) { + var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); + var url = "xpipe://action/" + p.getKey().getId() + "/" + + wrapper.getEntry().getUuid(); + sc.textProperty().bind(AppI18n.observable("base.createShortcut")); + sc.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + DesktopShortcuts.create(url, + wrapper.getName() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().getStore().asNeeded()).getValue() + ")"); + }); + }); + menu.getItems().add(sc); + + if (XPipeDistributionType.get().isSupportsUrls()) { + var l = new MenuItem(null, new FontIcon("mdi2c-clipboard-list-outline")); + l.textProperty().bind(AppI18n.observable("base.copyShareLink")); + l.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + var selection = new StringSelection(url); + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + AppActionLinkDetector.setLastDetectedAction(url); + clipboard.setContents(selection, selection); + }); + }); + menu.getItems().add(l); + } + } } - if (wrapper.getActionProviders().size() > 0) { + if (contextMenu.getItems().size() > 0 && !hasSep) { contextMenu.getItems().add(new SeparatorMenuItem()); } if (AppPrefs.get().developerMode().getValue()) { - var browse = new MenuItem(AppI18n.get("browse"), new FontIcon("mdi2f-folder-open-outline")); + var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline")); browse.setOnAction( event -> DesktopHelper.browsePath(wrapper.getEntry().getDirectory())); contextMenu.getItems().add(browse); } + var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); + StoreViewState.get().getSortedCategories().forEach(storeCategoryWrapper -> { + MenuItem m = new MenuItem(storeCategoryWrapper.getName()); + m.setOnAction(event -> { + wrapper.moveTo(storeCategoryWrapper.getCategory()); + }); + move.getItems().add(m); + }); + contextMenu.getItems().add(move); + var refresh = new MenuItem(AppI18n.get("refresh"), new FontIcon("mdal-360")); refresh.setOnAction(event -> { DataStorage.get().refreshAsync(wrapper.getEntry(), true); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryFlatMiniSectionComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryFlatMiniSectionComp.java deleted file mode 100644 index dc58322f..00000000 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryFlatMiniSectionComp.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.xpipe.app.comp.storage.store; - -import atlantafx.base.controls.Spacer; -import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.fxcomps.impl.PrettyImageComp; -import io.xpipe.app.storage.DataStoreEntry; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.geometry.Orientation; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import lombok.EqualsAndHashCode; -import lombok.Value; - -@Value -@EqualsAndHashCode(callSuper = true) -public class StoreEntryFlatMiniSectionComp extends SimpleComp { - - public static final ObservableList ALL = FXCollections.observableArrayList(); - - static { - var topLevel = StoreSection.createTopLevel(); - - // Listen for any entry list change, not only top level changes - StoreViewState.get().getAllEntries().addListener((ListChangeListener) c -> { - ALL.clear(); - var depth = 0; - for (StoreSection v : topLevel.getChildren()) { - add(depth, v); - } - }); - - var depth = 0; - for (StoreSection v : topLevel.getChildren()) { - add(depth, v); - } - } - - private static void add(int depth, StoreSection section) { - if (!section.getWrapper().getState().getValue().isUsable()) { - return; - } - - if (!section.getWrapper().getEntry().getProvider().shouldShowInSelectionTree()) { - return; - } - - ALL.add(new StoreEntryFlatMiniSectionComp(depth, section.getWrapper().getEntry())); - for (StoreSection child : section.getChildren()) { - add(depth + 1, child); - } - } - - int depth; - DataStoreEntry entry; - - @Override - protected Region createSimple() { - var image = entry.getState() == DataStoreEntry.State.LOAD_FAILED - ? "disabled_icon.png" - : entry.getProvider().getDisplayIconFileName(entry.getStore()); - var label = - new Label(entry.getName(), new PrettyImageComp(new SimpleStringProperty(image), 20, 20).createRegion()); - var spacer = new Spacer(depth * 10, Orientation.HORIZONTAL); - return new HBox(spacer, label); - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java index a1319e3a..9a351a74 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java @@ -17,15 +17,10 @@ import java.util.List; public class StoreEntryListComp extends SimpleComp { private Comp createList() { - var topLevel = StoreSection.createTopLevel(); - var filtered = BindingsHelper.filteredContentBinding( - topLevel.getChildren(), - StoreViewState.get() - .getFilterString() - .map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s)))); - var content = new ListBoxViewComp<>(filtered, topLevel.getChildren(), (StoreSection e) -> { - var custom = StoreSection.customSection(e).hgrow(); - return new HorizontalComp(List.of(Comp.spacer(10), custom, Comp.spacer(10))).styleClass("top"); + var topLevel = StoreViewState.get().getTopLevelSection(); + var content = new ListBoxViewComp<>(topLevel.getShownChildren(), topLevel.getAllChildren(), (StoreSection e) -> { + var custom = StoreSection.customSection(e, true).hgrow(); + return new HorizontalComp(List.of(Comp.hspacer(10), custom, Comp.hspacer(10))).styleClass("top"); }).apply(struc -> ((Region) struc.get().getContent()).setPadding(new Insets(10, 0, 10, 0))); return content.styleClass("store-list-comp"); } @@ -42,14 +37,14 @@ public class StoreEntryListComp extends SimpleComp { map.put( createList(), BindingsHelper.persist( - Bindings.not(Bindings.isEmpty(StoreViewState.get().getShownEntries())))); + Bindings.not(Bindings.isEmpty(StoreViewState.get().getTopLevelSection().getShownChildren())))); map.put(new StoreIntroComp(), showIntro); map.put( new StoreNotFoundComp(), BindingsHelper.persist(Bindings.and( Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())), - Bindings.isEmpty(StoreViewState.get().getShownEntries())))); + Bindings.isEmpty(StoreViewState.get().getTopLevelSection().getShownChildren())))); return new MultiContentComp(map).createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListHeaderComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java similarity index 60% rename from app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListHeaderComp.java rename to app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java index d592b516..e4721bd2 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListHeaderComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListSideComp.java @@ -2,24 +2,36 @@ package io.xpipe.app.comp.storage.store; import io.xpipe.app.comp.base.CountComp; import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.FilterComp; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.util.ThreadHelper; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Pos; import javafx.scene.control.Label; +import javafx.scene.control.MenuButton; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.*; +import org.kordamp.ikonli.javafx.FontIcon; -public class StoreEntryListHeaderComp extends SimpleComp { +public class StoreEntryListSideComp extends SimpleComp { private Region createGroupListHeader() { var label = new Label("Connections"); label.getStyleClass().add("name"); - var count = new CountComp<>( - StoreViewState.get().getShownEntries(), StoreViewState.get().getAllEntries()); + + var shownList = BindingsHelper.filteredContentBinding( + StoreViewState.get().getAllEntries(), + storeEntryWrapper -> { + return storeEntryWrapper.shouldShow( + StoreViewState.get().getFilterString().getValue()); + }, + StoreViewState.get().getFilterString()); + var count = new CountComp<>(shownList, StoreViewState.get().getAllEntries()); var spacer = new Region(); @@ -35,10 +47,10 @@ public class StoreEntryListHeaderComp extends SimpleComp { var filterProperty = new SimpleStringProperty(); filterProperty.addListener((observable, oldValue, newValue) -> { ThreadHelper.runAsync(() -> { - StoreViewState.get().getFilter().filterProperty().setValue(newValue); + StoreViewState.get().getFilterString().setValue(newValue); }); }); - var filter = new FilterComp(StoreViewState.get().getFilter().filterProperty()); + var filter = new FilterComp(StoreViewState.get().getFilterString()); filter.shortcut(new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), s -> { s.getText().requestFocus(); }); @@ -49,9 +61,18 @@ public class StoreEntryListHeaderComp extends SimpleComp { return r; } + private Region createButtons() { + var menu = new MenuButton(AppI18n.get("addConnections"), new FontIcon("mdi2p-plus-box-outline")); + AppFont.medium(menu); + GrowAugment.create(true, false).augment(menu); + StoreCreationMenu.addButtons(menu); + return menu; + } + @Override public Region createSimple() { - var bar = new VBox(createGroupListHeader(), createGroupListFilter()); + var bar = new VBox(createGroupListHeader(), createGroupListFilter(), createButtons()); + bar.setFillWidth(true); bar.getStyleClass().add("bar"); bar.getStyleClass().add("store-header-bar"); return bar; diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java deleted file mode 100644 index 1673fafc..00000000 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryTree.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.xpipe.app.comp.storage.store; - -import javafx.collections.ListChangeListener; -import javafx.scene.control.TreeItem; - -public class StoreEntryTree { - - public static TreeItem createTree() { - var topLevel = StoreSection.createTopLevel(); - var root = new TreeItem(); - root.setExpanded(true); - - // Listen for any entry list change, not only top level changes - StoreViewState.get().getAllEntries().addListener((ListChangeListener) c -> { - root.getChildren().clear(); - for (StoreSection v : topLevel.getChildren()) { - add(root, v); - } - }); - - for (StoreSection v : topLevel.getChildren()) { - add(root, v); - } - - return root; - } - - private static void add(TreeItem parent, StoreSection section) { - if (!section.getWrapper().getEntry().getState().isUsable()) { - return; - } - - if (!section.getWrapper().getEntry().getProvider().shouldShowInSelectionTree()) { - return; - } - - var item = new TreeItem<>(section.getWrapper()); - item.setExpanded(section.getWrapper().getExpanded().getValue()); - parent.getChildren().add(item); - for (StoreSection child : section.getChildren()) { - add(item, child); - } - } -} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java index 450c653a..49f9c633 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryWrapper.java @@ -1,12 +1,12 @@ package io.xpipe.app.comp.storage.store; -import io.xpipe.app.comp.storage.StorageFilter; import io.xpipe.app.comp.store.GuiDsStoreCreator; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.DataStore; @@ -16,17 +16,19 @@ import lombok.Getter; import java.time.Duration; import java.time.Instant; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; @Getter -public class StoreEntryWrapper implements StorageFilter.Filterable { +public class StoreEntryWrapper { private final Property name; private final DataStoreEntry entry; private final Property lastAccess; private final BooleanProperty disabled = new SimpleBooleanProperty(); - private final BooleanProperty validating = new SimpleBooleanProperty(); + private final BooleanProperty inRefresh = new SimpleBooleanProperty(); + private final BooleanProperty observing = new SimpleBooleanProperty(); private final Property state = new SimpleObjectProperty<>(); private final StringProperty information = new SimpleStringProperty(); private final StringProperty summary = new SimpleStringProperty(); @@ -34,6 +36,9 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { private final Property> defaultActionProvider; private final BooleanProperty deletable = new SimpleBooleanProperty(); private final BooleanProperty expanded = new SimpleBooleanProperty(); + private final Property category = new SimpleObjectProperty<>(); + private final Property displayParent = new SimpleObjectProperty<>(); + private final IntegerProperty depth = new SimpleIntegerProperty(); public StoreEntryWrapper(DataStoreEntry entry) { this.entry = entry; @@ -49,6 +54,7 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { .getApplicableClass() .isAssignableFrom(entry.getStore().getClass()); }) + .sorted(Comparator.comparing(actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction())) .forEach(dataStoreActionProvider -> { actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true)); }); @@ -57,6 +63,24 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { update(); } + public void moveTo(DataStoreCategory category) { + ThreadHelper.runAsync(() -> { + DataStorage.get().updateCategory(entry, category); + }); + } + + private StoreEntryWrapper computeDisplayParent() { + if (StoreViewState.get() == null) { + return null; + } + + var p = DataStorage.get().getParent(entry, true).orElse(null); + return StoreViewState.get().getAllEntries().stream() + .filter(storeEntryWrapper -> storeEntryWrapper.getEntry().equals(p)) + .findFirst() + .orElse(null); + } + public boolean isInStorage() { return DataStorage.get().getStoreEntries().contains(entry); } @@ -66,8 +90,10 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { } public void delete() { - DataStorage.get().deleteChildren(this.entry, true); - DataStorage.get().deleteStoreEntry(this.entry); + ThreadHelper.runAsync(() -> { + DataStorage.get().deleteChildren(this.entry, true); + DataStorage.get().deleteStoreEntry(this.entry); + }); } private void setupListeners() { @@ -85,6 +111,12 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { } public void update() { + // var cat = StoreViewState.get().getCategories().stream() + // .filter(storeCategoryWrapper -> + // Objects.equals(storeCategoryWrapper.getCategory().getUuid(), entry.getCategoryUuid())) + // .findFirst(); + // category.setValue(cat.orElseThrow()); + // Avoid reupdating name when changed from the name property! if (!entry.getName().equals(name.getValue())) { name.setValue(entry.getName()); @@ -94,9 +126,11 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { disabled.setValue(entry.isDisabled()); state.setValue(entry.getState()); expanded.setValue(entry.isExpanded()); + observing.setValue(entry.isObserving()); information.setValue(entry.getInformation()); + displayParent.setValue(computeDisplayParent()); - validating.setValue(entry.isValidating()); + inRefresh.setValue(entry.isInRefresh()); if (entry.getState().isUsable()) { try { summary.setValue(entry.getProvider().toSummaryString(entry.getStore(), 50)); @@ -108,6 +142,13 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { deletable.setValue(entry.getConfiguration().isDeletable() || AppPrefs.get().developerDisableGuiRestrictions().getValue()); + var d = 0; + var c = this; + while ((c = c.getDisplayParent().getValue()) != null) { + d++; + } + depth.setValue(d); + actionProviders.keySet().forEach(dataStoreActionProvider -> { if (!isInStorage()) { actionProviders.get(dataStoreActionProvider).set(false); @@ -205,9 +246,9 @@ public class StoreEntryWrapper implements StorageFilter.Filterable { this.expanded.set(!expanded.getValue()); } - @Override public boolean shouldShow(String filter) { - return filter == null || getName().toLowerCase().contains(filter.toLowerCase()) + return filter == null + || getName().toLowerCase().contains(filter.toLowerCase()) || (summary.get() != null && summary.get().toLowerCase().contains(filter.toLowerCase())) || (information.get() != null && information.get().toLowerCase().contains(filter.toLowerCase())); } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreIntroComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreIntroComp.java index e5154e94..c30f0d90 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreIntroComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreIntroComp.java @@ -3,16 +3,19 @@ package io.xpipe.app.comp.storage.store; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.ScanAlert; import io.xpipe.core.impl.LocalStore; +import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.Separator; +import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -29,7 +32,7 @@ public class StoreIntroComp extends SimpleComp { var introDesc = new Label(AppI18n.get("storeIntroDescription")); var mfi = new FontIcon("mdi2p-playlist-plus"); - var machine = new Label(AppI18n.get("storeMachineDescription"), mfi); + var machine = new Label(AppI18n.get("storeMachineDescription")); machine.heightProperty().addListener((c, o, n) -> { mfi.iconSizeProperty().set(n.intValue()); }); @@ -51,8 +54,15 @@ public class StoreIntroComp extends SimpleComp { var docLinkPane = new StackPane(docLink); docLinkPane.setAlignment(Pos.CENTER); + var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 180).createRegion(); + var hbox = new HBox(img, new VBox( + title, introDesc, new Separator(Orientation.HORIZONTAL), machine + )); + hbox.setSpacing(35); + hbox.setAlignment(Pos.CENTER); + var v = new VBox( - title, introDesc, new Separator(Orientation.HORIZONTAL), machine, scanPane + hbox, scanPane // new Separator(Orientation.HORIZONTAL), // documentation, // docLinkPane diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java index 1208213d..0e012fa2 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSection.java @@ -1,6 +1,5 @@ package io.xpipe.app.comp.storage.store; -import io.xpipe.app.comp.storage.StorageFilter; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.storage.DataStorage; @@ -8,84 +7,164 @@ import io.xpipe.app.storage.DataStoreEntry; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableStringValue; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import lombok.Value; import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; @Value -public class StoreSection implements StorageFilter.Filterable { +public class StoreSection { - public static Comp customSection(StoreSection e) { + public static Comp customSection(StoreSection e, boolean topLevel) { var prov = e.getWrapper().getEntry().getProvider(); if (prov != null) { - return prov.customContainer(e); + return prov.customSectionComp(e, topLevel); } else { - return new StoreSectionComp(e); + return new StoreSectionComp(e, topLevel); } } StoreEntryWrapper wrapper; - ObservableList children; + ObservableList allChildren; + ObservableList shownChildren; + ObservableList shownEntries; int depth; ObservableBooleanValue showDetails; - - static ObservableValue> sortMode = Bindings.createObjectBinding(() -> { - return Comparator.comparingInt(value -> value.getWrapper().getEntry().getState().isUsable() ? 1 : -1) - .thenComparing(StoreViewState.get().getSortMode().getValue().comparator()); - }, StoreViewState.get().getSortMode()); - - public StoreSection(StoreEntryWrapper wrapper, ObservableList children, int depth) { + public StoreSection( + StoreEntryWrapper wrapper, + ObservableList allChildren, + ObservableList shownChildren, + int depth) { this.wrapper = wrapper; - this.children = children; + this.allChildren = allChildren; + this.shownChildren = shownChildren; this.depth = depth; if (wrapper != null) { this.showDetails = Bindings.createBooleanBinding( () -> { - return wrapper.getExpanded().get() || children.size() == 0; + return wrapper.getExpanded().get() || allChildren.size() == 0; }, wrapper.getExpanded(), - children); + allChildren); } else { this.showDetails = new SimpleBooleanProperty(true); } - } - public static StoreSection createTopLevel() { - var topLevel = BindingsHelper.cachedMappedContentBinding( - StoreViewState.get().getAllEntries(), storeEntryWrapper -> create(storeEntryWrapper, 1)); - var filtered = BindingsHelper.filteredContentBinding(topLevel, section -> { - return DataStorage.get() - .getParent(section.getWrapper().getEntry(), true) - .isEmpty(); + this.shownEntries = FXCollections.observableArrayList(); + this.shownChildren.addListener((ListChangeListener) c -> { + shownEntries.clear(); + addShown(shownEntries); }); - var ordered = BindingsHelper.orderedContentBinding(filtered, sortMode); - return new StoreSection(null, ordered, 0); } - private static StoreSection create(StoreEntryWrapper e, int depth) { + private void addShown(List list) { + getShownChildren().forEach(shown -> { + list.add(shown.getWrapper()); + shown.addShown(list); + }); + } + + private static ObservableList sorted( + ObservableList list, ObservableValue category) { + var c = Comparator.comparingInt( + value -> value.getWrapper().getEntry().getState().isUsable() ? 1 : -1); + category.getValue().getSortMode().addListener((observable, oldValue, newValue) -> { + int a = 0; + }); + var mapped = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper.getSortMode()); + mapped.addListener((observable, oldValue, newValue) -> { + int a = 0; + }); + return BindingsHelper.orderedContentBinding( + list, + (o1, o2) -> { + var current = category.getValue(); + if (current != null) { + return c.thenComparing(current.getSortMode().getValue().comparator()) + .compare(o1, o2); + } else { + return c.compare(o1, o2); + } + }, + category, + mapped); + } + + public static StoreSection createTopLevel( + ObservableList all, + Predicate entryFilter, + ObservableStringValue filterString, + ObservableValue category) { + var cached = BindingsHelper.cachedMappedContentBinding( + all, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); + var ordered = sorted(cached, category); + var topLevel = BindingsHelper.filteredContentBinding( + ordered, + section -> { + var noParent = DataStorage.get() + .getParent(section.getWrapper().getEntry(), true) + .isEmpty(); + var sameCategory = + category.getValue().contains(section.getWrapper().getEntry()); + var diffParentCategory = DataStorage.get() + .getParent(section.getWrapper().getEntry(), true) + .map(entry -> !category.getValue().contains(entry)) + .orElse(false); + var showFilter = section.shouldShow(filterString.get()); + var matchesSelector = section.anyMatches(entryFilter); + return (noParent || diffParentCategory) && showFilter && sameCategory && matchesSelector; + }, + category, + filterString); + return new StoreSection(null, cached, topLevel, 0); + } + + private static StoreSection create( + StoreEntryWrapper e, + int depth, + ObservableList all, + Predicate entryFilter, + ObservableStringValue filterString, + ObservableValue category) { if (e.getEntry().getState() == DataStoreEntry.State.LOAD_FAILED) { - return new StoreSection(e, FXCollections.observableArrayList(), depth); + return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth); } - var filtered = - BindingsHelper.filteredContentBinding(StoreViewState.get().getAllEntries(), other -> { - return DataStorage.get() - .getParent(other.getEntry(), true) - .map(found -> found.equals(e.getEntry())) - .orElse(false); - }); - var children = BindingsHelper.cachedMappedContentBinding(filtered, entry1 -> create(entry1, depth + 1)); - var ordered = BindingsHelper.orderedContentBinding(children, sortMode); - return new StoreSection(e, ordered, depth); + var allChildren = BindingsHelper.filteredContentBinding(all, other -> { + return DataStorage.get() + .getParent(other.getEntry(), true) + .map(found -> found.equals(e.getEntry())) + .orElse(false); + }); + var cached = BindingsHelper.cachedMappedContentBinding( + allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); + var ordered = sorted(cached, category); + var filtered = BindingsHelper.filteredContentBinding( + ordered, + section -> { + return category.getValue().contains(section.getWrapper().getEntry()) + && section.shouldShow(filterString.get()) + && section.anyMatches(entryFilter); + }, + category, + filterString); + return new StoreSection(e, cached, filtered, depth); } - @Override public boolean shouldShow(String filter) { - return wrapper.shouldShow(filter) - || children.stream().anyMatch(storeEntrySection -> storeEntrySection.shouldShow(filter)); + return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter)); + } + + public boolean anyMatches(Predicate c) { + return c == null + || c.test(wrapper) + || allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c)); } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionComp.java index 9c6ac803..cd83fbbd 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionComp.java @@ -24,21 +24,23 @@ public class StoreSectionComp extends Comp> { public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded"); private final StoreSection section; + private final boolean topLevel; - public StoreSectionComp(StoreSection section) { + public StoreSectionComp(StoreSection section, boolean topLevel) { this.section = section; + this.topLevel = topLevel; } @Override public CompStructure createBase() { - var root = StandardStoreEntryComp.customSection(section).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); + var root = StandardStoreEntryComp.customSection(section, topLevel).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); var button = new IconButtonComp( Bindings.createStringBinding( () -> section.getWrapper().getExpanded().get() - && section.getChildren().size() > 0 + && section.getShownChildren().size() > 0 ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", - section.getWrapper().getExpanded()), + section.getWrapper().getExpanded(), section.getShownChildren()), () -> { section.getWrapper().toggleExpanded(); }) @@ -47,24 +49,18 @@ public class StoreSectionComp extends Comp> { .focusTraversable() .accessibleText("Expand") .disable(BindingsHelper.persist( - Bindings.size(section.getChildren()).isEqualTo(0))) + Bindings.size(section.getShownChildren()).isEqualTo(0))) .grow(false, true) .styleClass("expand-button"); List> topEntryList = List.of(button, root); - var all = section.getChildren(); - var shown = BindingsHelper.filteredContentBinding( - all, - StoreViewState.get() - .getFilterString() - .map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s)))); - var content = new ListBoxViewComp<>(shown, all, (StoreSection e) -> { - return StoreSection.customSection(e).apply(GrowAugment.create(true, false)); + var content = new ListBoxViewComp<>(section.getShownChildren(), section.getAllChildren(), (StoreSection e) -> { + return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false)); }).hgrow(); var expanded = Bindings.createBooleanBinding(() -> { - return section.getWrapper().getExpanded().get() && section.getChildren().size() > 0; - }, section.getWrapper().getExpanded(), section.getChildren()); + return section.getWrapper().getExpanded().get() && section.getShownChildren().size() > 0; + }, section.getWrapper().getExpanded(), section.getShownChildren()); return new VerticalComp(List.of( new HorizontalComp(topEntryList) @@ -75,7 +71,7 @@ public class StoreSectionComp extends Comp> { .apply(struc -> struc.get().setFillHeight(true)) .hide(BindingsHelper.persist(Bindings.or( Bindings.not(section.getWrapper().getExpanded()), - Bindings.size(section.getChildren()).isEqualTo(0)))))) + Bindings.size(section.getAllChildren()).isEqualTo(0)))))) .styleClass("store-entry-section-comp") .apply(struc -> { struc.get().setFillWidth(true); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionMiniComp.java new file mode 100644 index 00000000..87c24eb4 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSectionMiniComp.java @@ -0,0 +1,109 @@ +package io.xpipe.app.comp.storage.store; + +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.comp.base.ListBoxViewComp; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.impl.HorizontalComp; +import io.xpipe.app.fxcomps.impl.IconButtonComp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; +import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.VBox; +import lombok.Builder; + +import java.util.List; +import java.util.function.BiConsumer; + +@Builder +public class StoreSectionMiniComp extends Comp> { + + public static Comp createList(StoreSection top, BiConsumer>> augment) { + var content = new ListBoxViewComp<>(top.getShownChildren(), top.getAllChildren(), (StoreSection e) -> { + var custom = StoreSectionMiniComp.builder().section(e).augment(augment).build().hgrow(); + return new HorizontalComp(List.of(custom)).styleClass("top"); + }); + return content.styleClass("store-mini-list-comp"); + } + + private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth"); + private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth"); + public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded"); + + private final StoreSection section; + + @Builder.Default + private final BiConsumer>> augment = (section1, buttonComp) -> {}; + + @Override + public CompStructure createBase() { + var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {}) + .apply(struc -> struc.get() + .setGraphic(PrettyImageHelper.ofFixedSmallSquare(section.getWrapper() + .getEntry() + .getProvider() + .getDisplayIconFileName(section.getWrapper() + .getEntry() + .getStore())) + .createRegion())) + .apply(struc -> { + struc.get().setAlignment(Pos.CENTER_LEFT); + }) + .grow(true, false) + .styleClass("item"); + augment.accept(section, root); + + var expanded = new SimpleBooleanProperty(section.getWrapper().getExpanded().get() + && section.getAllChildren().size() > 0); + var button = new IconButtonComp( + Bindings.createStringBinding( + () -> expanded.get() + ? "mdal-keyboard_arrow_down" + : "mdal-keyboard_arrow_right", + expanded), + () -> { + expanded.set(!expanded.get()); + }) + .apply(struc -> struc.get().setMinWidth(20)) + .apply(struc -> struc.get().setPrefWidth(20)) + .focusTraversable() + .accessibleText("Expand") + .disable(BindingsHelper.persist( + Bindings.size(section.getAllChildren()).isEqualTo(0))) + .grow(false, true) + .styleClass("expand-button"); + List> topEntryList = List.of(button, root); + + var content = new ListBoxViewComp<>(section.getShownChildren(), section.getAllChildren(), (StoreSection e) -> { + return StoreSectionMiniComp.builder().section(e).augment(this.augment).build(); + }) + .hgrow(); + + return new VerticalComp(List.of( + new HorizontalComp(topEntryList) + .apply(struc -> struc.get().setFillHeight(true)), + Comp.separator().visible(expanded), + new HorizontalComp(List.of(content)) + .styleClass("content") + .apply(struc -> struc.get().setFillHeight(true)) + .hide(BindingsHelper.persist(Bindings.or( + Bindings.not(expanded), + Bindings.size(section.getAllChildren()).isEqualTo(0)))))) + .styleClass("store-section-mini-comp") + .apply(struc -> { + struc.get().setFillWidth(true); + SimpleChangeListener.apply(expanded, val -> { + struc.get().pseudoClassStateChanged(EXPANDED, val); + }); + struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); + struc.get().pseudoClassStateChanged(ODD, section.getDepth() % 2 != 0); + }) + .createStructure(); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java index 47e35c39..b92dbc64 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSidebarComp.java @@ -3,6 +3,7 @@ package io.xpipe.app.comp.storage.store; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.util.FeatureProvider; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; @@ -14,13 +15,14 @@ public class StoreSidebarComp extends SimpleComp { @Override protected Region createSimple() { var sideBar = new VerticalComp(List.of( - new StoreEntryListHeaderComp(), - new StoreScanBarComp(), - new StoreCreationBarComp(), - new StoreOrganizationComp(), + new StoreEntryListSideComp(), + new StoreSortComp(), + FeatureProvider.get().organizationComp(), Comp.of(() -> new Region()).styleClass("bar").styleClass("filler-bar"))); - sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(4), Priority.ALWAYS)); + sideBar.apply(struc -> struc.get().setFillWidth(true)); + sideBar.apply(s -> VBox.setVgrow(s.get().getChildren().get(2), Priority.ALWAYS)); sideBar.styleClass("sidebar"); + sideBar.prefWidth(240); return sideBar.createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreOrganizationComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortComp.java similarity index 85% rename from app/src/main/java/io/xpipe/app/comp/storage/store/StoreOrganizationComp.java rename to app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortComp.java index cfe0533f..56a63ae5 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreOrganizationComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortComp.java @@ -5,8 +5,10 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; @@ -14,12 +16,16 @@ import javafx.scene.layout.Region; import java.util.List; -public class StoreOrganizationComp extends SimpleComp { +public class StoreSortComp extends SimpleComp { private final Property sortMode; - public StoreOrganizationComp() { - this.sortMode = StoreViewState.get().getSortMode(); + public StoreSortComp() { + this.sortMode = new SimpleObjectProperty<>(); + SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { + sortMode.unbind(); + sortMode.bindBidirectional(val.getSortMode()); + }); } private Comp createAlphabeticalSortButton() { @@ -102,11 +108,15 @@ public class StoreOrganizationComp extends SimpleComp { } private Comp createSortButtonBar() { - return new HorizontalComp(List.of(createDateSortButton(), createAlphabeticalSortButton())); + return new HorizontalComp(List.of(createDateSortButton(), createAlphabeticalSortButton())).apply(struc -> { + struc.get().setMinHeight(40); + struc.get().setPrefHeight(40); + struc.get().setMaxHeight(40); + }).styleClass("bar").styleClass("store-sort-bar"); } @Override protected Region createSimple() { - return createSortButtonBar().styleClass("bar").prefHeight(40).createRegion(); + return createSortButtonBar().createRegion(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortMode.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortMode.java index e600cfbd..0f3e6e62 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortMode.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreSortMode.java @@ -75,7 +75,7 @@ public interface StoreSortMode { static Stream flatten(StoreSection section) { return Stream.concat( Stream.of(section.getWrapper().getEntry()), - section.getChildren().stream().flatMap(section1 -> flatten(section1))); + section.getAllChildren().stream().flatMap(section1 -> flatten(section1))); } static List ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC); diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java index 441b1dda..cfb97200 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java @@ -1,22 +1,23 @@ package io.xpipe.app.comp.storage.store; -import io.xpipe.app.comp.storage.StorageFilter; import io.xpipe.app.core.AppCache; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.StorageListener; import javafx.application.Platform; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.Getter; -import java.time.Instant; import java.util.Arrays; import java.util.Comparator; +import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; @@ -24,38 +25,80 @@ public class StoreViewState { private static StoreViewState INSTANCE; - private final StorageFilter filter = new StorageFilter(); + private final StringProperty filter = new SimpleStringProperty(); + @Getter private final ObservableList allEntries = FXCollections.observableList(new CopyOnWriteArrayList<>()); - private final ObservableList shownEntries = - FXCollections.observableList(new CopyOnWriteArrayList<>()); @Getter - private final Property sortMode; + private final ObservableList categories = + FXCollections.observableList(new CopyOnWriteArrayList<>()); + + @Getter + private final StoreSection topLevelSection; + + @Getter + private final Property activeCategory = new SimpleObjectProperty<>(); private StoreViewState() { - var val = AppCache.getIfPresent("sortMode", String.class) - .flatMap(StoreSortMode::fromId) - .orElse(StoreSortMode.DATE_ASC); - this.sortMode = new SimpleObjectProperty<>(val); - this.sortMode.addListener((observable, oldValue, newValue) -> { - AppCache.update("sortMode", newValue.getId()); - }); - + StoreSection tl; try { - addStorageGroupListeners(); - addShownContentChangeListeners(); + initContent(); + addStorageListeners(); + tl = StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory); } catch (Exception exception) { + tl = new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0); + categories.setAll(new StoreCategoryWrapper(DataStorage.get().getAllCategory())); + activeCategory.setValue(getAllCategory()); ErrorEvent.fromThrowable(exception).handle(); } + topLevelSection = tl; + } + + public ObservableList getSortedCategories() { + Comparator comparator = new Comparator<>() { + @Override + public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { + if (o1.getParent() == null && o2.getParent() == null) { + return 0; + } + + if (o1.getParent() == null) { + return -1; + } + + if (o2.getParent() == null) { + return 1; + } + + var parent = compare(o1.getParent(), o2.getParent()); + if (parent != 0) { + return parent; + } + + return o1.getName().compareToIgnoreCase(o2.getName()); + } + }; + return categories.sorted(comparator); + } + + public StoreCategoryWrapper getAllCategory() { + return categories.stream() + .filter(storeCategoryWrapper -> + storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CATEGORY_UUID)) + .findFirst() + .orElseThrow(); } public static void init() { - INSTANCE = new StoreViewState(); + new StoreViewState(); } public static void reset() { + AppCache.update( + "selectedCategory", + INSTANCE.activeCategory.getValue().getCategory().getUuid()); INSTANCE = null; } @@ -63,17 +106,49 @@ public class StoreViewState { return INSTANCE; } - private void addStorageGroupListeners() { + private void initContent() { allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream() .map(StoreEntryWrapper::new) .toList())); + categories.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream() + .map(StoreCategoryWrapper::new) + .toList())); + activeCategory.addListener((observable, oldValue, newValue) -> { + DataStorage.get().setSelectedCategory(newValue.getCategory()); + }); + var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID); + activeCategory.setValue(categories.stream() + .filter(storeCategoryWrapper -> + storeCategoryWrapper.getCategory().getUuid().equals(selected)) + .findFirst() + .orElse(categories.stream() + .filter(storeCategoryWrapper -> + storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID)) + .findFirst() + .orElseThrow())); + + INSTANCE = this; + categories.forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); + allEntries.forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); + } + + private void addStorageListeners() { DataStorage.get().addListener(new StorageListener() { @Override public void onStoreAdd(DataStoreEntry... entry) { var l = Arrays.stream(entry).map(StoreEntryWrapper::new).toList(); Platform.runLater(() -> { allEntries.addAll(l); + categories.stream() + .filter(storeCategoryWrapper -> allEntries.stream() + .anyMatch(storeEntryWrapper -> storeEntryWrapper + .getEntry() + .getCategoryUuid() + .equals(storeCategoryWrapper + .getCategory() + .getUuid()))) + .forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); }); } @@ -83,35 +158,52 @@ public class StoreViewState { var l = StoreViewState.get().getAllEntries().stream() .filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry())) .toList(); + var cats = categories.stream() + .filter(storeCategoryWrapper -> allEntries.stream() + .anyMatch(storeEntryWrapper -> storeEntryWrapper + .getEntry() + .getCategoryUuid() + .equals(storeCategoryWrapper + .getCategory() + .getUuid()))).toList(); Platform.runLater(() -> { allEntries.removeAll(l); + cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); + }); + } + + @Override + public void onCategoryAdd(DataStoreCategory category) { + var l = new StoreCategoryWrapper(category); + Platform.runLater(() -> { + categories.add(l); + l.update(); + }); + } + + @Override + public void onCategoryRemove(DataStoreCategory category) { + var found = categories.stream() + .filter(storeCategoryWrapper -> + storeCategoryWrapper.getCategory().equals(category)) + .findFirst(); + if (found.isEmpty()) { + return; + } + + Platform.runLater(() -> { + categories.remove(found.get()); + var p = found.get().getParent(); + if (p != null) { + p.update(); + } }); } }); } - private void addShownContentChangeListeners() { - filter.createFilterBinding( - allEntries, - shownEntries, - new SimpleObjectProperty<>(Comparator.comparing( - storeEntryWrapper -> storeEntryWrapper.getLastAccess()) - .reversed())); - } - - public StorageFilter getFilter() { + public Property getFilterString() { return filter; } - public ObservableValue getFilterString() { - return filter.filterProperty(); - } - - public ObservableList getAllEntries() { - return allEntries; - } - - public ObservableList getShownEntries() { - return shownEntries; - } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/DataStoreSelectorComp.java b/app/src/main/java/io/xpipe/app/comp/store/DataStoreSelectorComp.java deleted file mode 100644 index 65569317..00000000 --- a/app/src/main/java/io/xpipe/app/comp/store/DataStoreSelectorComp.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.xpipe.app.comp.store; - -import com.jfoenix.controls.JFXButton; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.ext.DataStoreProvider; -import io.xpipe.app.ext.DataStoreProviders; -import io.xpipe.app.fxcomps.Comp; -import io.xpipe.app.fxcomps.CompStructure; -import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.util.JfxHelper; -import io.xpipe.core.impl.FileStore; -import io.xpipe.core.store.DataStore; -import javafx.beans.property.Property; -import javafx.scene.control.Button; -import javafx.scene.layout.Region; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.experimental.FieldDefaults; - -@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) -@AllArgsConstructor -public class DataStoreSelectorComp extends Comp> { - - DataStoreProvider.DisplayCategory category; - Property chosenStore; - - @Override - public CompStructure