diff --git a/doc/CONFIGURATION.json b/doc/CONFIGURATION.json index c5c8f4bbd..4ff8ada0f 100644 --- a/doc/CONFIGURATION.json +++ b/doc/CONFIGURATION.json @@ -41,6 +41,10 @@ "description" : "Delete", "value" : "com.owncloud.action.delete" }, + { + "description" : "Close Window", + "value" : "com.owncloud.action.discardscene" + }, { "description" : "Duplicate", "value" : "com.owncloud.action.duplicate" @@ -65,6 +69,10 @@ "description" : "Open in", "value" : "com.owncloud.action.openin" }, + { + "description" : "Open in a new Window", + "value" : "com.owncloud.action.openscene" + }, { "description" : "Go to page", "value" : "com.owncloud.action.pdfpage" @@ -175,6 +183,10 @@ "description" : "Delete", "value" : "com.owncloud.action.delete" }, + { + "description" : "Close Window", + "value" : "com.owncloud.action.discardscene" + }, { "description" : "Duplicate", "value" : "com.owncloud.action.duplicate" @@ -199,6 +211,10 @@ "description" : "Open in", "value" : "com.owncloud.action.openin" }, + { + "description" : "Open in a new Window", + "value" : "com.owncloud.action.openscene" + }, { "description" : "Go to page", "value" : "com.owncloud.action.pdfpage" @@ -298,6 +314,38 @@ "status" : "advanced", "type" : "stringArray" }, + { + "autoExpansion" : "none", + "category" : "Actions", + "categoryTag" : "actions", + "classIdentifier" : "action", + "className" : "ownCloudAppShared.Action", + "defaultValue" : "auto", + "description" : "Determines how to open a document in a web app.", + "flatIdentifier" : "action.open-in-web-app-mode", + "key" : "open-in-web-app-mode", + "label" : "Open In WebApp mode", + "possibleValues" : [ + { + "description" : "Open using `in-app-with-default-browser-option`, unless the respective endpoint is not available - in which case `default-browser` is used instead. If no endpoint to open the document is available, an error message is shown.", + "value" : "auto" + }, + { + "description" : "Open in default browser app. May require user to sign in.", + "value" : "default-browser" + }, + { + "description" : "Open inline in an in-app browser.", + "value" : "in-app" + }, + { + "description" : "Open inline in an in-app browser, but provide a button to open the document in the default browser (may require the user to sign in).", + "value" : "in-app-with-default-browser-option" + } + ], + "status" : "advanced", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "App", @@ -1701,6 +1749,10 @@ "description" : "Extension with the identifier com.owncloud.action.delete.", "value" : "com.owncloud.action.delete" }, + { + "description" : "Extension with the identifier com.owncloud.action.discardscene.", + "value" : "com.owncloud.action.discardscene" + }, { "description" : "Extension with the identifier com.owncloud.action.duplicate.", "value" : "com.owncloud.action.duplicate" @@ -1729,6 +1781,10 @@ "description" : "Extension with the identifier com.owncloud.action.openin.", "value" : "com.owncloud.action.openin" }, + { + "description" : "Extension with the identifier com.owncloud.action.openscene.", + "value" : "com.owncloud.action.openscene" + }, { "description" : "Extension with the identifier com.owncloud.action.pdfpage.", "value" : "com.owncloud.action.pdfpage" @@ -1929,6 +1985,30 @@ "status" : "debugOnly", "type" : "stringArray" }, + { + "autoExpansion" : "none", + "category" : "Connection", + "categoryTag" : "connection", + "classIdentifier" : "http", + "className" : "OCHTTPPipeline", + "defaultValue" : "json", + "description" : "If request and response logging is enabled, the format to use.", + "flatIdentifier" : "http.traffic-log-format", + "key" : "traffic-log-format", + "label" : "http.traffic-log-format", + "possibleValues" : [ + { + "description" : "JSON", + "value" : "json" + }, + { + "description" : "Plain text", + "value" : "plain" + } + ], + "status" : "supported", + "type" : "string" + }, { "autoExpansion" : "none", "category" : "Connection", diff --git a/doc/configuration.adoc b/doc/configuration.adoc index bb8f82d08..78d8940c1 100644 --- a/doc/configuration.adoc +++ b/doc/configuration.adoc @@ -38,6 +38,9 @@ tag::actions[] ! `com.owncloud.action.delete` ! Delete +! `com.owncloud.action.discardscene` +! Close Window + ! `com.owncloud.action.duplicate` ! Duplicate @@ -56,6 +59,9 @@ tag::actions[] ! `com.owncloud.action.openin` ! Open in +! `com.owncloud.action.openscene` +! Open in a new Window + ! `com.owncloud.action.pdfpage` ! Go to page @@ -139,6 +145,9 @@ action.create-document-mode ! `com.owncloud.action.delete` ! Delete +! `com.owncloud.action.discardscene` +! Close Window + ! `com.owncloud.action.duplicate` ! Duplicate @@ -157,6 +166,9 @@ action.create-document-mode ! `com.owncloud.action.openin` ! Open in +! `com.owncloud.action.openscene` +! Open in a new Window + ! `com.owncloud.action.pdfpage` ! Go to page @@ -233,6 +245,32 @@ action.create-document-mode |advanced `candidate` +|**Open In WebApp mode** + + + +action.open-in-web-app-mode +|string +|`auto` +|Determines how to open a document in a web app. +[cols="1,1"] +!=== +! Value +! Description +! `auto` +! Open using `in-app-with-default-browser-option`, unless the respective endpoint is not available - in which case `default-browser` is used instead. If no endpoint to open the document is available, an error message is shown. + +! `default-browser` +! Open in default browser app. May require user to sign in. + +! `in-app` +! Open inline in an in-app browser. + +! `in-app-with-default-browser-option` +! Open inline in an in-app browser, but provide a button to open the document in the default browser (may require the user to sign in). + +!=== + +|advanced `candidate` + |=== end::actions[] @@ -713,6 +751,24 @@ tag::connection[] |Enable or disable per-process, in-memory cookie storage. |supported `candidate` +|http.traffic-log-format +|string +|`json` +|If request and response logging is enabled, the format to use. +[cols="1,1"] +!=== +! Value +! Description +! `json` +! JSON + +! `plain` +! Plain text + +!=== + +|supported `candidate` + |http.user-agent |string |`ownCloudApp/{{app.version}} ({{app.part}}/{{app.build}}; {{os.name}}/{{os.version}}; {{device.model}})` @@ -1045,6 +1101,9 @@ tag::extensions[] ! `com.owncloud.action.delete` ! Extension with the identifier com.owncloud.action.delete. +! `com.owncloud.action.discardscene` +! Extension with the identifier com.owncloud.action.discardscene. + ! `com.owncloud.action.duplicate` ! Extension with the identifier com.owncloud.action.duplicate. @@ -1066,6 +1125,9 @@ tag::extensions[] ! `com.owncloud.action.openin` ! Extension with the identifier com.owncloud.action.openin. +! `com.owncloud.action.openscene` +! Extension with the identifier com.owncloud.action.openscene. + ! `com.owncloud.action.pdfpage` ! Extension with the identifier com.owncloud.action.pdfpage. diff --git a/ios-sdk b/ios-sdk index 9774d53f9..4dbc4ad41 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 9774d53f97aadbecada73d713795265fdbdf97f8 +Subproject commit 4dbc4ad4157eab1f9514aa8fc48d0148d228aeb4 diff --git a/ownCloud Share Extension/ShareExtensionViewController.swift b/ownCloud Share Extension/ShareExtensionViewController.swift index 1a5cd664e..9d640ea65 100644 --- a/ownCloud Share Extension/ShareExtensionViewController.swift +++ b/ownCloud Share Extension/ShareExtensionViewController.swift @@ -344,6 +344,13 @@ class ShareExtensionViewController: EmbeddingViewController, Themeable { // Show location picker showLocationPicker() } + + // Log in to first account if there's only one + let bookmarks = OCBookmarkManager.shared.bookmarks + + if bookmarks.count == 1, let onlyBookmark = bookmarks.first { + AccountConnectionPool.shared.connection(for: onlyBookmark)?.connect() + } } } diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 8bacbe3de..2bcf4fc09 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -217,6 +217,11 @@ DC0A35A124C1091400FB58FC /* UserInterfaceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0A35A024C1091400FB58FC /* UserInterfaceContext.swift */; }; DC0A5C432550C70800E6674B /* class-settings-sdk in Resources */ = {isa = PBXBuildFile; fileRef = DC0A5C422550C70800E6674B /* class-settings-sdk */; }; DC0CE19D28C89CD9009ABDFB /* CreateDocumentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */; }; + DC1621352B8FE26200EB17F8 /* OCVault+SidebarItems.h in Headers */ = {isa = PBXBuildFile; fileRef = DC1621332B8FE26200EB17F8 /* OCVault+SidebarItems.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC1621362B8FE26200EB17F8 /* OCVault+SidebarItems.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1621342B8FE26200EB17F8 /* OCVault+SidebarItems.m */; }; + DC1621382B8FE9BF00EB17F8 /* AddToSidebarAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1621372B8FE9BF00EB17F8 /* AddToSidebarAction.swift */; }; + DC16213A2B8FEEE500EB17F8 /* OCSidebarItem+Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1621392B8FEEE500EB17F8 /* OCSidebarItem+Cell.swift */; }; + DC16213E2B8FF06800EB17F8 /* OCSidebarItem+Interactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC16213D2B8FF06800EB17F8 /* OCSidebarItem+Interactions.swift */; }; DC18898E218A773700CFB3F9 /* ownCloudMocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; DC1B270C209CF34B004715E1 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */; }; DC20DE6A21C01B210096000B /* ownCloudSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 239369782076110900BCE21A /* ownCloudSDK.framework */; }; @@ -272,6 +277,7 @@ DC2A128728D0725D0088A2B7 /* OCSavedSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2A127D28D06F060088A2B7 /* OCSavedSearch.m */; }; DC2A68D529D492B300BFF393 /* space.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DC2A68D429D492B200BFF393 /* space.tvg */; }; DC2A68D729D4E93300BFF393 /* SharedKeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */; }; + DC2A8E6A2B57EA8F001F0522 /* AccountControllerSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2A8E692B57EA8F001F0522 /* AccountControllerSearchViewController.swift */; }; DC2FE2DA24C30586002AFDB3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 593A821320C7D4C5000E2A90 /* Localizable.strings */; }; DC36885824DC98BF00333600 /* OCFileProviderServiceSession.h in Headers */ = {isa = PBXBuildFile; fileRef = DC36885624DC98BF00333600 /* OCFileProviderServiceSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC36885924DC98BF00333600 /* OCFileProviderServiceSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DC36885724DC98BF00333600 /* OCFileProviderServiceSession.m */; }; @@ -368,6 +374,8 @@ DC7C101124B5FA7700227085 /* OCBookmark+AppExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = DC7C100E24B5F81E00227085 /* OCBookmark+AppExtensions.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC7C101224B5FD6500227085 /* OCBookmark+AppExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = DC7C100F24B5F81E00227085 /* OCBookmark+AppExtensions.m */; }; DC7DBA37207F84BF00E7337D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7DBA36207F84BF00E7337D /* main.swift */; }; + DC8087992B8FDFD900AB1C45 /* OCSidebarItem.h in Headers */ = {isa = PBXBuildFile; fileRef = DC8087972B8FDFD900AB1C45 /* OCSidebarItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DC80879A2B8FDFD900AB1C45 /* OCSidebarItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DC8087982B8FDFD900AB1C45 /* OCSidebarItem.m */; }; DC815C3F2A65D9CB00BFF393 /* AvailableOfflineAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC815C3E2A65D9CB00BFF393 /* AvailableOfflineAction.swift */; }; DC825E352A05083C00BFF393 /* GitInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC825E342A05083C00BFF393 /* GitInfo.swift */; }; DC82663C28168D2800F91F7D /* ClientContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC82663B28168D2800F91F7D /* ClientContext.swift */; }; @@ -465,6 +473,7 @@ DCB6B20A292E296800D27573 /* CollectionSidebarAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B209292E296800D27573 /* CollectionSidebarAction.swift */; }; DCB6B20C292E428000D27573 /* AccountController+ExtraItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B20B292E428000D27573 /* AccountController+ExtraItems.swift */; }; DCB6B20F292F843800D27573 /* CollectionViewAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB6B20E292F843800D27573 /* CollectionViewAction.swift */; }; + DCB796582BC535AD00D6D759 /* RemoveFromSidebarAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCB796572BC535AD00D6D759 /* RemoveFromSidebarAction.swift */; }; DCBAEADB29A3674700BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEAD429A361C100BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift */; }; DCBAEADE29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEADD29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift */; }; DCBAEAE029A554CC00BFF393 /* CollectionViewSupplementaryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCBAEADF29A554CC00BFF393 /* CollectionViewSupplementaryItem.swift */; }; @@ -1259,6 +1268,11 @@ DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInWebAppAction.swift; sourceTree = ""; }; DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDocumentAction.swift; sourceTree = ""; }; DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmark+Extension.swift"; sourceTree = ""; }; + DC1621332B8FE26200EB17F8 /* OCVault+SidebarItems.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCVault+SidebarItems.h"; sourceTree = ""; }; + DC1621342B8FE26200EB17F8 /* OCVault+SidebarItems.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCVault+SidebarItems.m"; sourceTree = ""; }; + DC1621372B8FE9BF00EB17F8 /* AddToSidebarAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToSidebarAction.swift; sourceTree = ""; }; + DC1621392B8FEEE500EB17F8 /* OCSidebarItem+Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCSidebarItem+Cell.swift"; sourceTree = ""; }; + DC16213D2B8FF06800EB17F8 /* OCSidebarItem+Interactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCSidebarItem+Interactions.swift"; sourceTree = ""; }; DC1AC7CF2319ADAE002B7892 /* ScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewController.swift; sourceTree = ""; }; DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewController.swift; sourceTree = ""; }; DC2218C42822C5B900808BCE /* OCVFSNode+FileProviderItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCVFSNode+FileProviderItem.h"; sourceTree = ""; }; @@ -1314,6 +1328,7 @@ DC2A128128D0718B0088A2B7 /* OCVault+SavedSearches.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCVault+SavedSearches.m"; sourceTree = ""; }; DC2A68D429D492B200BFF393 /* space.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = space.tvg; path = "img/filetypes-tvg/space.tvg"; sourceTree = SOURCE_ROOT; }; DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedKeyCommands.swift; sourceTree = ""; }; + DC2A8E692B57EA8F001F0522 /* AccountControllerSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountControllerSearchViewController.swift; sourceTree = ""; }; DC321260207EB01B00DB171D /* ThemeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeImage.swift; sourceTree = ""; }; DC3393A722E0C4ED00DD3DA4 /* icon-available-offline.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "icon-available-offline.tvg"; path = "../img/filetypes-tvg/icon-available-offline.tvg"; sourceTree = ""; }; DC36885624DC98BF00333600 /* OCFileProviderServiceSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCFileProviderServiceSession.h; sourceTree = ""; }; @@ -1426,6 +1441,8 @@ DC7DBA36207F84BF00E7337D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; DC7DBA52207F8BD600E7337D /* img */ = {isa = PBXFileReference; lastKnownFileType = folder; path = img; sourceTree = SOURCE_ROOT; }; DC7DBA53207FA80C00E7337D /* TVGImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVGImage.swift; sourceTree = ""; }; + DC8087972B8FDFD900AB1C45 /* OCSidebarItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCSidebarItem.h; sourceTree = ""; }; + DC8087982B8FDFD900AB1C45 /* OCSidebarItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCSidebarItem.m; sourceTree = ""; }; DC815C3E2A65D9CB00BFF393 /* AvailableOfflineAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailableOfflineAction.swift; sourceTree = ""; }; DC825E342A05083C00BFF393 /* GitInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitInfo.swift; sourceTree = ""; }; DC82663B28168D2800F91F7D /* ClientContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientContext.swift; sourceTree = ""; }; @@ -1532,6 +1549,7 @@ DCB6B20B292E428000D27573 /* AccountController+ExtraItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountController+ExtraItems.swift"; sourceTree = ""; }; DCB6B20E292F843800D27573 /* CollectionViewAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewAction.swift; sourceTree = ""; }; DCB6C4DD24559B1600C1EAE1 /* AccountAuthenticationUpdaterPasswordPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAuthenticationUpdaterPasswordPromptViewController.swift; sourceTree = ""; }; + DCB796572BC535AD00D6D759 /* RemoveFromSidebarAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveFromSidebarAction.swift; sourceTree = ""; }; DCBAEAD429A361C100BFF393 /* OCItemPolicy+UniversalItemListCellContentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCItemPolicy+UniversalItemListCellContentProvider.swift"; sourceTree = ""; }; DCBAEADD29A5536F00BFF393 /* CollectionViewSupplementaryCellProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSupplementaryCellProvider.swift; sourceTree = ""; }; DCBAEADF29A554CC00BFF393 /* CollectionViewSupplementaryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewSupplementaryItem.swift; sourceTree = ""; }; @@ -1982,6 +2000,7 @@ DCB5D5692861BE9A004AF425 /* Search */, DCA2EDDB279B0E5D001F04E6 /* Resource Sources */, DCE4E43424C199860051722F /* Actions */, + DC16213C2B8FF02500EB17F8 /* Sidebar Items */, 399EA6ED25E6544000B6FF11 /* Sharing */, DCE4E42F24C1963F0051722F /* User Interface */, ); @@ -2247,6 +2266,8 @@ 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */, 39EF06AF25D6C3FC001E1E19 /* PresentationModeAction.swift */, DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */, + DC1621372B8FE9BF00EB17F8 /* AddToSidebarAction.swift */, + DCB796572BC535AD00D6D759 /* RemoveFromSidebarAction.swift */, ); path = "Actions+Extensions"; sourceTree = ""; @@ -2372,6 +2393,15 @@ path = "Cursor Support"; sourceTree = ""; }; + DC16213C2B8FF02500EB17F8 /* Sidebar Items */ = { + isa = PBXGroup; + children = ( + DC1621392B8FEEE500EB17F8 /* OCSidebarItem+Cell.swift */, + DC16213D2B8FF06800EB17F8 /* OCSidebarItem+Interactions.swift */, + ); + path = "Sidebar Items"; + sourceTree = ""; + }; DC2323DA2AA7B5A100BFF393 /* Composer */ = { isa = PBXGroup; children = ( @@ -2652,6 +2682,7 @@ DC6C0A532923FFF30045FF2A /* AccountControllerSection.swift */, DC298C8C2934B3E7009FA87F /* AccountConnectionErrorHandler.swift */, DC298CAF29366B96009FA87F /* AccountControllerSpacesGridViewController.swift */, + DC2A8E692B57EA8F001F0522 /* AccountControllerSearchViewController.swift */, DC89EA662995456A00BFF393 /* BrowserNavigationBookmark+AccountController.swift */, ); path = Controller; @@ -2807,6 +2838,17 @@ path = tools; sourceTree = ""; }; + DC8087962B8FDFB600AB1C45 /* Sidebar Items */ = { + isa = PBXGroup; + children = ( + DC8087982B8FDFD900AB1C45 /* OCSidebarItem.m */, + DC8087972B8FDFD900AB1C45 /* OCSidebarItem.h */, + DC1621342B8FE26200EB17F8 /* OCVault+SidebarItems.m */, + DC1621332B8FE26200EB17F8 /* OCVault+SidebarItems.h */, + ); + path = "Sidebar Items"; + sourceTree = ""; + }; DC82664028168DAA00F91F7D /* Context */ = { isa = PBXGroup; children = ( @@ -3145,6 +3187,7 @@ DCF575E52796CBB3003BEBBA /* View Providers */, DC774E5422F44DF6000B11A1 /* SDK Extensions */, DC2A128828D076750088A2B7 /* Search */, + DC8087962B8FDFB600AB1C45 /* Sidebar Items */, DC0030BE2350B1CE00BB8570 /* Tools */, DCEA7F38282D3ACA0050A3C0 /* VFS */, DC3DDEFE287E1AA500E5586D /* UIKit Extensions */, @@ -3774,6 +3817,8 @@ DCC0856E2293F1FD008CC05C /* ownCloudApp.h in Headers */, DCF2DA8124C836240026D790 /* OCBookmark+FPServices.h in Headers */, DC66F3A523965A1400CF4812 /* NSDate+RFC3339.h in Headers */, + DC1621352B8FE26200EB17F8 /* OCVault+SidebarItems.h in Headers */, + DC8087992B8FDFD900AB1C45 /* OCSidebarItem.h in Headers */, DC0030C22350B1CE00BB8570 /* NSData+Encoding.h in Headers */, DCEA7F41282D3B110050A3C0 /* VFSManager.h in Headers */, DCCD77792604C91600098573 /* NSDate+ComputedTimes.h in Headers */, @@ -4528,6 +4573,7 @@ 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, 399698ED260A3CEE00E5AEBA /* ImportPasteboardAction.swift in Sources */, DC59087F2AA8D25400BFF393 /* CertificateSummaryView.swift in Sources */, + DCB796582BC535AD00D6D759 /* RemoveFromSidebarAction.swift in Sources */, 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */, DCC832DE242C0C3700153F8C /* DisplaySleepPreventer.swift in Sources */, 6E586CFC2199A72600F680C4 /* OpenInAction.swift in Sources */, @@ -4580,6 +4626,7 @@ DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, DCC8535823CE1236007BA3EB /* LicenseInAppProductListViewController.swift in Sources */, + DC1621382B8FE9BF00EB17F8 /* AddToSidebarAction.swift in Sources */, 23EC775B2137F3DD0032D4E6 /* OCExtensionType+Extension.swift in Sources */, DC3F4522271A23A000ED2383 /* AcknowledgementsTableViewController.swift in Sources */, 4C88041822E78D790016CBA9 /* MediaFilesSettings.swift in Sources */, @@ -4665,6 +4712,7 @@ DCFC9ECC28002303005D9144 /* CollectionViewSection.swift in Sources */, DC89EA602993AC4F00BFF393 /* NSUserActivity+SaveRestore.swift in Sources */, DC62F567292504060095BB5D /* AccountConnection.swift in Sources */, + DC16213A2B8FEEE500EB17F8 /* OCSidebarItem+Cell.swift in Sources */, DC9219FD2966229100F538EE /* UniversalItemListCell.swift in Sources */, DC28F826294B733700AC4013 /* OCItemPolicy+Interactions.swift in Sources */, DC89EA5C2993A0E000BFF393 /* AppStateActionConnect.swift in Sources */, @@ -4701,6 +4749,7 @@ DCB1B99929D187B400BFF393 /* ThemeCSSButton.swift in Sources */, 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */, DC298C9E2934D6D9009FA87F /* AccountAuthenticationUpdater.swift in Sources */, + DC2A8E6A2B57EA8F001F0522 /* AccountControllerSearchViewController.swift in Sources */, DC0A356D24C0E42200FB58FC /* PasscodeViewController.swift in Sources */, DC24B2AB25BA316D005783E2 /* Branding+App.swift in Sources */, 0234EF0E2515138B00AE921A /* PasscodeSetupCoordinator.swift in Sources */, @@ -4728,6 +4777,7 @@ DCDE444D2A36F56000BFF393 /* AppStateActionOpenItem.swift in Sources */, DCEA89822AD84D6000BFF393 /* BrandView.swift in Sources */, DC60F2AA29802D5800905EC8 /* NavigationContentItem.swift in Sources */, + DC16213E2B8FF06800EB17F8 /* OCSidebarItem+Interactions.swift in Sources */, DC0A356F24C0E42700FB58FC /* StaticTableViewController.swift in Sources */, DC62F569292504510095BB5D /* AccountConnectionPool.swift in Sources */, DC0A358F24C0E46000FB58FC /* PointerEffect.swift in Sources */, @@ -4932,9 +4982,11 @@ DCF2DA7E24C835BF0026D790 /* OCVault+FPServices.m in Sources */, DC2A128728D0725D0088A2B7 /* OCSavedSearch.m in Sources */, DCDC208D239912DC003CFF5B /* OCLicenseTransaction.m in Sources */, + DC1621362B8FE26200EB17F8 /* OCVault+SidebarItems.m in Sources */, DC23D1D9238F390A00423F62 /* OCLicenseAppStoreReceipt.m in Sources */, DCC832F2242CC28400153F8C /* NotificationMessagePresenter.m in Sources */, DCF2DA8224C836240026D790 /* OCBookmark+FPServices.m in Sources */, + DC80879A2B8FDFD900AB1C45 /* OCSidebarItem.m in Sources */, DCFEFE2B236876BD009A142F /* OCLicenseManager.m in Sources */, DCDC20A22399A715003CFF5B /* OCCore+LicenseEnvironment.m in Sources */, DCEA7F42282D3B110050A3C0 /* VFSManager.m in Sources */, diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index 819e89639..db2e75706 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -482,6 +482,11 @@ value = "false" isEnabled = "NO"> + + . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp +import ownCloudAppShared + +class AddToSidebarAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.addToSidebar") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Add to sidebar".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.contextMenuItem, .moreItem] } + + // MARK: - Extension matching + override class func applicablePosition(forContext context: ActionContext) -> ActionPosition { + guard context.items.count > 0 else { + return .none + } + + var sidebarItemLocationStrings: [String]? + + for item in context.items { + if item.type != .collection { + return .none + } + + if sidebarItemLocationStrings == nil { + sidebarItemLocationStrings = context.core?.vault.sidebarItems?.compactMap({ item in + return item.location?.string + }) ?? [] + } + + if let sidebarItemLocationStrings, sidebarItemLocationStrings.count > 0, let itemLocationString = item.location?.string { + if sidebarItemLocationStrings.contains(itemLocationString) { + return .none + } + } + } + + return .middle + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let core = core else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + + for item in context.items { + if let location = item.location { + location.bookmarkUUID = context.core?.bookmark.uuid + core.vault.add(OCSidebarItem(location: location)) + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(named: "sidebar.leading.badge.plus")?.withRenderingMode(.alwaysTemplate) + } + +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift index 2814c1cf4..55b649b90 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift @@ -30,35 +30,28 @@ public enum CreateDocumentActionMode: String { } class CreateDocumentAction: Action { - private static var _classSettingsRegistered: Bool = false - override public class var actionExtension: ActionExtension { - if !_classSettingsRegistered { - _classSettingsRegistered = true - - self.registerOCClassSettingsDefaults([ - .createDocumentMode : CreateDocumentActionMode.createAndOpen.rawValue - ], metadata: [ - .createDocumentMode : [ - .type : OCClassSettingsMetadataType.string, - .label : "Create Document Mode", - .description : "Determines behaviour when creating a document.", - .status : OCClassSettingsKeyStatus.advanced, - .category : "Actions", - .possibleValues : [ - [ - OCClassSettingsMetadataKey.value : CreateDocumentActionMode.create.rawValue, - OCClassSettingsMetadataKey.description : "Creates the document." - ], - [ - OCClassSettingsMetadataKey.value : CreateDocumentActionMode.createAndOpen.rawValue, - OCClassSettingsMetadataKey.description : "Creates the document and opens it in a web app for the document format." - ] + public static func registerSettings() { + self.registerOCClassSettingsDefaults([ + .createDocumentMode : CreateDocumentActionMode.createAndOpen.rawValue + ], metadata: [ + .createDocumentMode : [ + .type : OCClassSettingsMetadataType.string, + .label : "Create Document Mode", + .description : "Determines behaviour when creating a document.", + .status : OCClassSettingsKeyStatus.advanced, + .category : "Actions", + .possibleValues : [ + [ + OCClassSettingsMetadataKey.value : CreateDocumentActionMode.create.rawValue, + OCClassSettingsMetadataKey.description : "Creates the document." + ], + [ + OCClassSettingsMetadataKey.value : CreateDocumentActionMode.createAndOpen.rawValue, + OCClassSettingsMetadataKey.description : "Creates the document and opens it in a web app for the document format." ] ] - ]) - } - - return super.actionExtension + ] + ]) } override open class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.createDocument") } diff --git a/ownCloud/Client/Actions/Actions+Extensions/RemoveFromSidebarAction.swift b/ownCloud/Client/Actions/Actions+Extensions/RemoveFromSidebarAction.swift new file mode 100644 index 000000000..78070ea26 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/RemoveFromSidebarAction.swift @@ -0,0 +1,87 @@ +// +// RemoveFromSidebarAction.swift +// ownCloud +// +// Created by Felix Schwarz on 09.04.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp +import ownCloudAppShared + +class RemoveFromSidebarAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.removeFromSidebar") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Remove from sidebar".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.contextMenuItem, .moreItem] } + + // MARK: - Extension matching + override class func applicablePosition(forContext context: ActionContext) -> ActionPosition { + guard context.items.count > 0 else { + return .none + } + + var sidebarItemLocationStrings: [String]? + + for item in context.items { + if item.type != .collection { + return .none + } + + if sidebarItemLocationStrings == nil { + sidebarItemLocationStrings = context.core?.vault.sidebarItems?.compactMap({ item in + return item.location?.string + }) + + if sidebarItemLocationStrings == nil { + return .none + } + } + + if let sidebarItemLocationStrings, let itemLocationString = item.location?.string { + if sidebarItemLocationStrings.contains(itemLocationString) { + return .middle + } + } + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let core = core, let sidebarItems = context.core?.vault.sidebarItems else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + + for item in context.items { + if let location = item.location { + location.bookmarkUUID = context.core?.bookmark.uuid + for sidebarItem in sidebarItems { + if sidebarItem.location == location { + core.vault.delete(sidebarItem) + break + } + } + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(named: "sidebar.leading.badge.minus")?.withRenderingMode(.alwaysTemplate) + } + +} diff --git a/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.minus.symbolset/Contents.json b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.minus.symbolset/Contents.json new file mode 100644 index 000000000..6276f5fd2 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.minus.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "custom.sidebar.leading.badge.minus.svg", + "idiom" : "universal" + } + ] +} diff --git a/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.minus.symbolset/custom.sidebar.leading.badge.minus.svg b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.minus.symbolset/custom.sidebar.leading.badge.minus.svg new file mode 100644 index 000000000..5399e1cd3 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.minus.symbolset/custom.sidebar.leading.badge.minus.svg @@ -0,0 +1,119 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.5.0 + Requires Xcode 15 or greater + Generated from + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.plus.symbolset/Contents.json b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.plus.symbolset/Contents.json new file mode 100644 index 000000000..dd360656a --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.plus.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "custom.sidebar.leading.badge.plus.svg", + "idiom" : "universal" + } + ] +} diff --git a/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.plus.symbolset/custom.sidebar.leading.badge.plus.svg b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.plus.symbolset/custom.sidebar.leading.badge.plus.svg new file mode 100644 index 000000000..9f30d4c09 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/sidebar.leading.badge.plus.symbolset/custom.sidebar.leading.badge.plus.svg @@ -0,0 +1,119 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.5.0 + Requires Xcode 15 or greater + Generated from + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index 5048aa3f6..f4761f3d8 100644 --- a/ownCloud/Resources/de.lproj/Localizable.strings +++ b/ownCloud/Resources/de.lproj/Localizable.strings @@ -142,17 +142,15 @@ "Image grid" = "Bild-Kacheln"; /* Saved searches */ -"Saved searches" = "Gespeicherte Suchanfragen"; -"Saved search templates" = "Vorlagen für gespeicherte Suchanfragen"; -"Save search" = "Suchanfrage speichern"; -"Save as search template" = "Suchanfrage als Vorlagen speichern"; - -"Name of saved search" = "Name der gespeicherten Suchanfrage"; -"Saved search" = "Gespeicherte Suchanfrage"; -"Saved searches" = "Gespeicherte Suchanfragen"; +"Save search" = "Suche speichern"; +"Save as search template" = "Suche als Vorlage speichern"; + +"Name of saved search" = "Name der gespeicherten Suche"; +"Saved search" = "Gespeicherte Suche"; +"Saved searches" = "Gespeicherte Suchen"; "Name of template" = "Name der Vorlage"; -"Search template" = "Vorlage für Suchanfragen"; -"Search templates" = "Vorlagen für Suchanfragen"; +"Search template" = "Suchvorlage"; +"Search templates" = "Suchvorlagen"; /* Search scope */ "Search scope" = "Suchbereich"; diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index a8e5cbba8..30637d3ba 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -142,8 +142,6 @@ "Image grid" = "Image grid"; /* Saved searches */ -"Saved searches" = "Saved searches"; -"Saved search templates" = "Saved search templates"; "Save search" = "Save search"; "Save as search template" = "Save as search template"; @@ -633,9 +631,9 @@ "Enter password" = "Enter password"; -/* Quick Access view */ +/* Quick Access search suggestions */ "Quick Access" = "Quick Access"; -"Collection" = "Collection"; +"Add to sidebar" = "Add to sidebar"; "Recents" = "Recents"; "Favorites"= "Favorites"; "Images" = "Images"; diff --git a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h index 91aeb1e75..85b9192b7 100644 --- a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h +++ b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.h @@ -25,7 +25,7 @@ typedef NSString* OCSavedSearchUserInfoKey NS_TYPED_ENUM; NS_ASSUME_NONNULL_BEGIN -@interface OCSavedSearch : NSObject +@interface OCSavedSearch : NSObject @property(strong) OCSavedSearchUUID uuid; //!< Unique ID of the saved search diff --git a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m index d4cfa8f51..7efc6b996 100644 --- a/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m +++ b/ownCloudAppFramework/Search/Saved Searches/OCSavedSearch.m @@ -59,7 +59,7 @@ - (OCDataItemReference)dataItemReference - (OCDataItemVersion)dataItemVersion { - return (@(self.hash)); + return ([NSString stringWithFormat:@"%lu%@%@", self.hash, self.name, self.uuid]); } #pragma mark - Comparison @@ -87,6 +87,13 @@ - (BOOL)isEqual:(id)object return (NO); } +#pragma mark - Copying +- (id)copyWithZone:(NSZone *)zone +{ + // Issues a new UUID - this is desired + return ([[self.class alloc] initWithScope:_scope location:_location name:[_name copy] isTemplate:_isTemplate searchTerm:[_searchTerm copy] userInfo:[_userInfo copy]]); +} + #pragma mark - Secure coding + (BOOL)supportsSecureCoding { diff --git a/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.h b/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.h index 466e118c7..fcc08bbce 100644 --- a/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.h +++ b/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property(readonly,strong,nullable) NSArray *savedSearches; - (void)addSavedSearch:(OCSavedSearch *)savedSearch; +- (void)updateSavedSearch:(OCSavedSearch *)savedSearch; - (void)deleteSavedSearch:(OCSavedSearch *)savedSearch; - (void)addSavedSearchesObserver:(id)owner withInitial:(BOOL)initial updateHandler:(void(^)(id owner, NSArray * _Nullable savedSearches, BOOL initial))updateHandler; diff --git a/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.m b/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.m index 1defd71c9..b4c253c57 100644 --- a/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.m +++ b/ownCloudAppFramework/Search/Saved Searches/OCVault+SavedSearches.m @@ -52,6 +52,33 @@ - (void)addSavedSearch:(OCSavedSearch *)savedSearch [self didChangeValueForKey:@"savedSearches"]; } +- (void)updateSavedSearch:(OCSavedSearch *)savedSearch +{ + [self willChangeValueForKey:@"savedSearches"]; + + [self.keyValueStore updateObjectForKey:OCKeyValueStoreKeySavedSearches usingModifier:^id _Nullable(id _Nullable existingObject, BOOL * _Nonnull outDidModify) { + NSMutableArray *savedSearches = OCTypedCast(existingObject, NSMutableArray); + NSUInteger countBefore; + + if (savedSearches != nil) + { + NSUInteger existingOffset = [savedSearches indexOfObjectPassingTest:^BOOL(OCSavedSearch * _Nonnull existingSearch, NSUInteger idx, BOOL * _Nonnull stop) { + return ([existingSearch.uuid isEqual:savedSearch.uuid]); + }]; + + if (existingOffset != NSNotFound) + { + [savedSearches replaceObjectAtIndex:existingOffset withObject:savedSearch]; + *outDidModify = YES; + } + } + + return (savedSearches); + }]; + + [self didChangeValueForKey:@"savedSearches"]; +} + - (void)deleteSavedSearch:(OCSavedSearch *)savedSearch { [self willChangeValueForKey:@"savedSearches"]; diff --git a/ownCloudAppFramework/Sidebar Items/OCSidebarItem.h b/ownCloudAppFramework/Sidebar Items/OCSidebarItem.h new file mode 100644 index 000000000..79f1fb683 --- /dev/null +++ b/ownCloudAppFramework/Sidebar Items/OCSidebarItem.h @@ -0,0 +1,37 @@ +// +// OCSidebarItem.h +// ownCloudApp +// +// Created by Felix Schwarz on 28.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import + +typedef NSString* OCSidebarItemUUID; + +NS_ASSUME_NONNULL_BEGIN + +@interface OCSidebarItem : NSObject + +@property(strong,readonly) OCSidebarItemUUID uuid; +@property(strong,nullable) OCLocation *location; + +- (instancetype)initWithLocation:(OCLocation *)location; + +@end + +extern OCDataItemType OCDataItemTypeSidebarItem; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Sidebar Items/OCSidebarItem.m b/ownCloudAppFramework/Sidebar Items/OCSidebarItem.m new file mode 100644 index 000000000..5e36d118f --- /dev/null +++ b/ownCloudAppFramework/Sidebar Items/OCSidebarItem.m @@ -0,0 +1,104 @@ +// +// OCSidebarItem.m +// ownCloudApp +// +// Created by Felix Schwarz on 28.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCSidebarItem.h" + +@implementation OCSidebarItem + +- (instancetype)init +{ + if ((self = [super init]) != nil) + { + _uuid = NSUUID.UUID.UUIDString; + } + + return (self); +} + +- (instancetype)initWithLocation:(OCLocation *)location +{ + if ((self = [self init]) != nil) + { + self.location = location; + } + + return (self); +} + +//! MARK: - Data item +- (OCDataItemType)dataItemType +{ + return (OCDataItemTypeSidebarItem); +} + +- (OCDataItemReference)dataItemReference +{ + return (_uuid); +} + +- (OCDataItemVersion)dataItemVersion +{ + return ([NSString stringWithFormat:@"%@%@", self.uuid, self.location.lastPathComponent]); +} + +//! MARK: - Secure coding ++ (BOOL)supportsSecureCoding +{ + return (YES); +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder +{ + [coder encodeObject:_uuid forKey:@"uuid"]; + [coder encodeObject:_location forKey:@"location"]; +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder +{ + if ((self = [self init]) != nil) + { + _uuid = [coder decodeObjectOfClass:NSString.class forKey:@"uuid"]; + _location = [coder decodeObjectOfClass:OCLocation.class forKey:@"location"]; + } + + return (self); +} + +//! MARK: - Comparison +- (NSUInteger)hash +{ + return (_uuid.hash ^ _location.hash); +} + +- (BOOL)isEqual:(id)object +{ + OCSidebarItem *otherSidebarItem; + + if ((otherSidebarItem = OCTypedCast(object, OCSidebarItem)) != nil) + { + return (OCNAIsEqual(otherSidebarItem.uuid, _uuid) && + OCNAIsEqual(otherSidebarItem.location, _location) + ); + } + + return (NO); +} + +@end + +OCDataItemType OCDataItemTypeSidebarItem = @"sidebarItem"; diff --git a/ownCloudAppFramework/Sidebar Items/OCVault+SidebarItems.h b/ownCloudAppFramework/Sidebar Items/OCVault+SidebarItems.h new file mode 100644 index 000000000..7c9cebe03 --- /dev/null +++ b/ownCloudAppFramework/Sidebar Items/OCVault+SidebarItems.h @@ -0,0 +1,38 @@ +// +// OCVault+SidebarItems.h +// ownCloudApp +// +// Created by Felix Schwarz on 28.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import "OCSidebarItem.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface OCVault (SidebarItems) + +@property(readonly,strong,nullable) NSArray *sidebarItems; + +- (void)addSidebarItem:(OCSidebarItem *)sidebarItem; +- (void)updateSidebarItem:(OCSidebarItem *)sidebarItem; +- (void)deleteSidebarItem:(OCSidebarItem *)sidebarItem; + +- (void)addSidebarItemObserver:(id)owner withInitial:(BOOL)initial updateHandler:(void(^)(id owner, NSArray * _Nullable sidebarItems, BOOL initial))updateHandler; + +@end + +extern OCKeyValueStoreKey OCKeyValueStoreKeySidebarItems; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Sidebar Items/OCVault+SidebarItems.m b/ownCloudAppFramework/Sidebar Items/OCVault+SidebarItems.m new file mode 100644 index 000000000..1612172c7 --- /dev/null +++ b/ownCloudAppFramework/Sidebar Items/OCVault+SidebarItems.m @@ -0,0 +1,123 @@ +// +// OCVault+SidebarItems.m +// ownCloudApp +// +// Created by Felix Schwarz on 28.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "OCVault+SidebarItems.h" + +@implementation OCVault (SidebarItems) + ++ (void)load +{ + [OCKeyValueStore registerClasses:[NSSet setWithObjects:NSArray.class, OCSidebarItem.class, nil] forKey:OCKeyValueStoreKeySidebarItems]; +} + +- (NSArray *)sidebarItems +{ + NSMutableArray *sidebarItems = [self.keyValueStore readObjectForKey:OCKeyValueStoreKeySidebarItems]; + + return (sidebarItems); +} + +- (void)addSidebarItem:(OCSidebarItem *)sidebarItem +{ + [self willChangeValueForKey:@"sidebarItems"]; + + [self.keyValueStore updateObjectForKey:OCKeyValueStoreKeySidebarItems usingModifier:^id _Nullable(id _Nullable existingObject, BOOL * _Nonnull outDidModify) { + NSMutableArray *sidebarItems = OCTypedCast(existingObject, NSMutableArray); + if (sidebarItems == nil) + { + sidebarItems = [NSMutableArray new]; + } + + [sidebarItems addObject:sidebarItem]; + + *outDidModify = YES; + return (sidebarItems); + }]; + + [self didChangeValueForKey:@"sidebarItems"]; +} + +- (void)updateSidebarItem:(OCSidebarItem *)sidebarItem +{ + [self willChangeValueForKey:@"sidebarItems"]; + + [self.keyValueStore updateObjectForKey:OCKeyValueStoreKeySidebarItems usingModifier:^id _Nullable(id _Nullable existingObject, BOOL * _Nonnull outDidModify) { + NSMutableArray *sidebarItems = OCTypedCast(existingObject, NSMutableArray); + NSUInteger countBefore; + + if (sidebarItems != nil) + { + NSUInteger existingOffset = [sidebarItems indexOfObjectPassingTest:^BOOL(OCSidebarItem * _Nonnull existingSidebarItem, NSUInteger idx, BOOL * _Nonnull stop) { + return ([existingSidebarItem.uuid isEqual:sidebarItem.uuid]); + }]; + + if (existingOffset != NSNotFound) + { + [sidebarItems replaceObjectAtIndex:existingOffset withObject:sidebarItem]; + *outDidModify = YES; + } + } + + return (sidebarItems); + }]; + + [self didChangeValueForKey:@"sidebarItems"]; +} + +- (void)deleteSidebarItem:(OCSidebarItem *)sidebarItem +{ + [self willChangeValueForKey:@"sidebarItems"]; + + [self.keyValueStore updateObjectForKey:OCKeyValueStoreKeySidebarItems usingModifier:^id _Nullable(id _Nullable existingObject, BOOL * _Nonnull outDidModify) { + NSMutableArray *sidebarItems = OCTypedCast(existingObject, NSMutableArray); + NSUInteger countBefore; + + if ((sidebarItems != nil) && ((countBefore = sidebarItems.count) > 0)) + { + [sidebarItems removeObject:sidebarItem]; + + if (countBefore != sidebarItems.count) + { + *outDidModify = YES; + } + } + + return (sidebarItems); + }]; + + [self didChangeValueForKey:@"sidebarItems"]; +} + +- (void)addSidebarItemObserver:(id)owner withInitial:(BOOL)initial updateHandler:(void (^)(id _Nonnull, NSArray * _Nullable, BOOL))updateHandler +{ + __block BOOL isInitial = initial; + + [self.keyValueStore addObserver:^(OCKeyValueStore * _Nonnull store, id _Nullable owner, OCKeyValueStoreKey _Nonnull key, id _Nullable newValue) { + NSMutableArray *sidebarItems = OCTypedCast(newValue, NSMutableArray); + BOOL isInitialCall = isInitial; + isInitial = NO; + + dispatch_async(dispatch_get_main_queue(), ^{ + updateHandler(owner, sidebarItems, isInitialCall); + }); + } forKey:OCKeyValueStoreKeySidebarItems withOwner:owner initial:initial]; +} + +@end + +OCKeyValueStoreKey OCKeyValueStoreKeySidebarItems = @"sidebarItems"; diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 0a40dcd33..1241683b9 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -93,3 +93,6 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import #import + +#import +#import diff --git a/ownCloudAppShared/Client/Account/Controller/AccountController.swift b/ownCloudAppShared/Client/Account/Controller/AccountController.swift index 02e3c040d..668afda5d 100644 --- a/ownCloudAppShared/Client/Account/Controller/AccountController.swift +++ b/ownCloudAppShared/Client/Account/Controller/AccountController.swift @@ -33,8 +33,12 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco public struct Configuration { public var showAccountPill: Bool public var showShared: Bool + public var showSearch: Bool public var showSavedSearches: Bool - public var showQuickAccess: Bool + public var showUserSidebarItems: Bool + public var showRecents: Bool + public var showFavorites: Bool + public var showAvailableOffline: Bool public var showActivity: Bool public var autoSelectPersonalFolder: Bool @@ -47,9 +51,9 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco public static var pickerConfiguration: Configuration { var config = Configuration() - config.showSavedSearches = false - config.showQuickAccess = false + config.showSearch = false config.showActivity = false + config.showAvailableOffline = false config.sectionAppearance = .insetGrouped @@ -61,8 +65,12 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco public init() { showAccountPill = true showShared = true + showSearch = true showSavedSearches = true - showQuickAccess = true + showUserSidebarItems = true + showFavorites = true + showRecents = true + showAvailableOffline = true showActivity = true autoSelectPersonalFolder = true @@ -77,19 +85,11 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco case spacesFolder - case savedSearchesFolder + case globalSearch - case quickAccessFolder - case favoriteItems - case availableOfflineItems - - case searchPDFDocuments - case searchDocuments - // case searchText - case searchImages - case searchVideos - case searchAudios - case recents + case recents + case favoriteItems + case availableOfflineItems case activity } @@ -194,18 +194,22 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco if let vault = connection.core?.vault { // Create savedSearchesDataSource if wanted if configuration.showSavedSearches, savedSearchesDataSource == nil { - savedSearchesDataSource = OCDataSourceKVO(object: vault, keyPath: "savedSearches", versionedItemUpdateHandler: { [weak self] obj, keypath, newValue in + savedSearchesDataSource = OCDataSourceKVO(object: vault, keyPath: "savedSearches", versionedItemUpdateHandler: { obj, keypath, newValue in if let savedSearches = newValue as? [OCSavedSearch] { - let searches = savedSearches.filter { savedSearch in return !savedSearch.isTemplate } - self?.savedSearchesVisible = searches.count > 0 - return searches + return savedSearches.filter { savedSearch in return !savedSearch.isTemplate } } return nil }) } + + // Create + if configuration.showUserSidebarItems, userSidebarItemsDataSource == nil { + userSidebarItemsDataSource = OCDataSourceKVO(object: vault, keyPath: "sidebarItems") + } } else { savedSearchesDataSource = nil + userSidebarItemsDataSource = nil } switch status { @@ -283,15 +287,10 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco @objc dynamic var showDisconnectButton: Bool = false var savedSearchesDataSource: OCDataSourceKVO? - var savedSearchesVisible: Bool = true { - didSet { - if oldValue != savedSearchesVisible, let savedSearchesFolderDatasource = specialItemsDataSources[.savedSearchesFolder] { - itemsDataSource.setInclude(savedSearchesVisible, for: savedSearchesFolderDatasource) - } - } - } var savedSearchesCondition: DataSourceCondition? + var userSidebarItemsDataSource: OCDataSourceKVO? + open var specialItems: [SpecialItem : OCDataItem & OCDataItemVersioning] = [:] open var specialItemsDataReferences: [SpecialItem : OCDataItemReference] = [:] open var specialItemsDataSources: [SpecialItem : OCDataSource] = [:] @@ -324,6 +323,8 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco private var legacyAccountRootLocation: OCLocation + private let useFolderForSearches: Bool = false + func composeItemsDataSource() { if let core = connection?.core { var sources : [OCDataSource] = [] @@ -388,90 +389,88 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco } // Saved searches - if configuration.showSavedSearches, let savedSearchesDataSource = savedSearchesDataSource { - savedSearchesCondition = DataSourceCondition(.empty, with: savedSearchesDataSource, initial: true, action: { [weak self] condition in - self?.savedSearchesVisible = condition.fulfilled == false - }) + var savedSearchesSidebarDataSource: OCDataSource? + var userSidebarItemsSidebarDataSource: OCDataSource? + + if configuration.showSearch, let savedSearchesDataSource { + if useFolderForSearches { + // Use "Search" item in sidebar, showing saved searches when unfolded + let savedSearchesFolderDataSource = self.buildTopFolder(with: savedSearchesDataSource, title: "Search".localized, icon: OCSymbol.icon(forSymbolName: "magnifyingglass"), topItem: .globalSearch) { [weak self] context, action in + return self?.provideViewController(for: .globalSearch, in: context) + } - let (savedSearchesFolderDataSource, savedSearchesFolderItem) = self.buildFolder(with: savedSearchesDataSource, title: "Saved searches".localized, icon: OCSymbol.icon(forSymbolName: "magnifyingglass"), folderItemRef:specialItemsDataReferences[.savedSearchesFolder]!) + sources.append(savedSearchesFolderDataSource) + } else { + // Add "Search" item to sidebar, making saved searches standalone items + let globalSearchItem = CollectionSidebarAction(with: "Search".localized, icon: OCSymbol.icon(forSymbolName: "magnifyingglass"), identifier: specialItemsDataReferences[.globalSearch], viewControllerProvider: { [weak self] context, action in + return self?.provideViewController(for: .globalSearch, in: context) + }, cacheViewControllers: false) - specialItems[.savedSearchesFolder] = savedSearchesFolderItem - specialItemsDataSources[.savedSearchesFolder] = savedSearchesFolderDataSource + specialItems[.globalSearch] = globalSearchItem - sources.append(savedSearchesFolderDataSource) + sources.append(OCDataSourceArray(items: [ globalSearchItem ])) + savedSearchesSidebarDataSource = savedSearchesDataSource // Add saved searches only after Available Offline + } } - // Quick access - if configuration.showQuickAccess { - var quickAccessItems: [OCDataItem & OCDataItemVersioning] = [] + // User sidebar items + if configuration.showUserSidebarItems, let userSidebarItemsDataSource { + // Add user sidebar items + userSidebarItemsSidebarDataSource = userSidebarItemsDataSource + } - // Recents - if specialItems[.recents] == nil { - specialItems[.recents] = OCSavedSearch(scope: .account, location: nil, name: "Recents".localized, isTemplate: false, searchTerm: ":recent :file").withCustomIcon(name: "clock.arrow.circlepath").useNameAsTitle(true).useSortDescriptor(SortDescriptor(method: .lastUsed, direction: .ascendant)) - } - if let sideBarItem = specialItems[.recents] { - quickAccessItems.append(sideBarItem) - } + // Other sidebar items + var otherItems: [OCDataItem & OCDataItemVersioning] = [] - // Favorites - if bookmark?.hasCapability(.favorites) == true { - if specialItems[.favoriteItems] == nil { - specialItems[.favoriteItems] = buildSidebarSpecialItem(with: "Favorites".localized, icon: OCSymbol.icon(forSymbolName: "star"), for: .favoriteItems) - } - if let sideBarItem = specialItems[.favoriteItems] { - quickAccessItems.append(sideBarItem) - } - } + func addSidebarItem(_ itemID: SpecialItem, _ generate: ()->OCDataItem&OCDataItemVersioning) { + var item = specialItems[itemID] - // Available offline - if specialItems[.availableOfflineItems] == nil { - specialItems[.availableOfflineItems] = buildSidebarSpecialItem(with: "Available Offline".localized, icon: OCItem.cloudAvailableOfflineStatusIcon, for: .availableOfflineItems) - } - if let sideBarItem = specialItems[.availableOfflineItems] { - quickAccessItems.append(sideBarItem) + if item == nil { + item = generate() + specialItems[itemID] = item } - // Convenience searches - if specialItems[.searchPDFDocuments] == nil { - specialItems[.searchPDFDocuments] = OCSavedSearch(scope: .account, location: nil, name: "PDF Documents".localized, isTemplate: false, searchTerm: ":pdf").withCustomIcon(name: "doc.richtext").useNameAsTitle(true) - } - if specialItems[.searchDocuments] == nil { - specialItems[.searchDocuments] = OCSavedSearch(scope: .account, location: nil, name: "Documents".localized, isTemplate: false, searchTerm: ":document").withCustomIcon(name: "doc").useNameAsTitle(true) - } - if specialItems[.searchImages] == nil { - specialItems[.searchImages] = OCSavedSearch(scope: .account, location: nil, name: "Images".localized, isTemplate: false, searchTerm: ":image").withCustomIcon(name: "photo").useNameAsTitle(true) - } - if specialItems[.searchVideos] == nil { - specialItems[.searchVideos] = OCSavedSearch(scope: .account, location: nil, name: "Videos".localized, isTemplate: false, searchTerm: ":video").withCustomIcon(name: "film").useNameAsTitle(true) - } - if specialItems[.searchAudios] == nil { - specialItems[.searchAudios] = OCSavedSearch(scope: .account, location: nil, name: "Audios".localized, isTemplate: false, searchTerm: ":audio").withCustomIcon(name: "waveform").useNameAsTitle(true) + if let item { + otherItems.append(item) } + } - let addSpecialItemsTypes: [SpecialItem] = [ .searchPDFDocuments, .searchDocuments, .searchImages, .searchVideos, .searchAudios ] + // Recents + if configuration.showRecents { + // Recents + addSidebarItem(.recents) { + return OCSavedSearch(scope: .account, location: nil, name: "Recents".localized, isTemplate: false, searchTerm: ":recent :file").withCustomIcon(name: "clock.arrow.circlepath").useNameAsTitle(true).useSortDescriptor(SortDescriptor(method: .lastUsed, direction: .ascendant)) + } + } - for specialItemType in addSpecialItemsTypes { - if let item = specialItems[specialItemType] as? OCSavedSearch { - if let representationUUID = specialItemsDataReferences[specialItemType] as? String { - item.uuid = representationUUID - } - quickAccessItems.append(item) - } + // Favorites + if configuration.showFavorites, bookmark?.hasCapability(.favorites) == true { + addSidebarItem(.favoriteItems) { + return buildSidebarSpecialItem(with: "Favorites".localized, icon: OCSymbol.icon(forSymbolName: "star"), for: .favoriteItems) } + } - quickAccessItemsDataSource.setVersionedItems(quickAccessItems) + // Available offline + if configuration.showAvailableOffline { + addSidebarItem(.availableOfflineItems) { + return buildSidebarSpecialItem(with: "Available Offline".localized, icon: OCItem.cloudAvailableOfflineStatusIcon, for: .availableOfflineItems) + } + } - // Quick access folder - if specialItems[.quickAccessFolder] == nil { - let (quickAccessFolderDataSource, quickAccessFolderItem) = self.buildFolder(with: quickAccessItemsDataSource, title: "Quick Access".localized, icon: OCSymbol.icon(forSymbolName: "speedometer"), folderItemRef:specialItemsDataReferences[.quickAccessFolder]!) + if otherItems.count > 0 { + let otherItemsDataSource = OCDataSourceArray() + otherItemsDataSource.setVersionedItems(otherItems) + sources.append(otherItemsDataSource) + } - specialItems[.quickAccessFolder] = quickAccessFolderItem - specialItemsDataSources[.quickAccessFolder] = quickAccessFolderDataSource - } + // Saved searches (if not in folder) + if let savedSearchesSidebarDataSource { + sources.append(savedSearchesSidebarDataSource) + } - if let quickAccessFolderDataSource = specialItemsDataSources[.quickAccessFolder] { - sources.append(quickAccessFolderDataSource) - } + // User sidebar items + if let userSidebarItemsSidebarDataSource { + sources.append(userSidebarItemsSidebarDataSource) } // Extra items (Activity & Co via class extension in the app) @@ -482,10 +481,6 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco } itemsDataSource.sources = sources - - if let savedSearchesFolderDataSource = specialItemsDataSources[.savedSearchesFolder], !savedSearchesVisible { - itemsDataSource.setInclude(savedSearchesVisible, for: savedSearchesFolderDataSource) - } } } @@ -547,6 +542,9 @@ public class AccountController: NSObject, OCDataItem, OCDataItemVersioning, Acco case .spacesFolder: viewController = AccountControllerSpacesGridViewController(with: context) + case .globalSearch: + viewController = AccountControllerSearchViewController(context: context) + case .availableOfflineItems: if let core = context.core { let availableOfflineFilesDataSource = core.availableOfflineFilesDataSource @@ -654,7 +652,6 @@ extension AccountController: DataItemSelectionInteraction { let /* spacesFolderItemRef */ _ = section?.collectionViewController?.wrap(references: [specialItemsDataReferences[.spacesFolder]!], forSection: sectionID).first { section?.collectionViewController?.addActions([ CollectionViewAction(kind: .select(animated: false, scrollPosition: .centeredVertically), itemReference: personalFolderItemRef) - // CollectionViewAction(kind: .expand(animated: true), itemReference: spacesFolderItemRef) ]) } } diff --git a/ownCloudAppShared/Client/Account/Controller/AccountControllerSearchViewController.swift b/ownCloudAppShared/Client/Account/Controller/AccountControllerSearchViewController.swift new file mode 100644 index 000000000..95fe9c15a --- /dev/null +++ b/ownCloudAppShared/Client/Account/Controller/AccountControllerSearchViewController.swift @@ -0,0 +1,84 @@ +// +// AccountControllerSearchViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 17.01.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp + +class AccountControllerSearchViewController: ClientItemViewController { + convenience init(context inContext: ClientContext) { + self.init(context: inContext, query: nil, itemsDatasource: OCDataSourceArray()) + revoke(in: inContext, when: [ .connectionClosed ]) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Bring up search + startSearch() + } + + override var searchViewController: SearchViewController? { + didSet { + // Modify newly created SearchViewController before it is used + searchViewController?.showCancelButton = false + searchViewController?.hideNavigationButtons = false + } + } + + let quickAccessSuggestions: [OCSavedSearch] = [ + OCSavedSearch(scope: .account, location: nil, name: "PDF Documents".localized, isTemplate: true, searchTerm: ":pdf").withCustomIcon(name: "doc.richtext").useNameAsTitle(true).isQuickAccess(true), + OCSavedSearch(scope: .account, location: nil, name: "Documents".localized, isTemplate: true, searchTerm: ":document").withCustomIcon(name: "doc").useNameAsTitle(true).isQuickAccess(true), + OCSavedSearch(scope: .account, location: nil, name: "Images".localized, isTemplate: true, searchTerm: ":image").withCustomIcon(name: "photo").useNameAsTitle(true).isQuickAccess(true), + OCSavedSearch(scope: .account, location: nil, name: "Videos".localized, isTemplate: true, searchTerm: ":video").withCustomIcon(name: "film").useNameAsTitle(true).isQuickAccess(true), + OCSavedSearch(scope: .account, location: nil, name: "Audios".localized, isTemplate: true, searchTerm: ":audio").withCustomIcon(name: "waveform").useNameAsTitle(true).isQuickAccess(true) + ] + + override func composeSuggestionContents(from savedSearches: [OCSavedSearch]?, clientContext: ClientContext, includingFallbacks: Bool) -> [OCDataItem & OCDataItemVersioning] { + var suggestions = super.composeSuggestionContents(from: savedSearches, clientContext: clientContext, includingFallbacks: false) + + let savedSearches = clientContext.core?.vault.savedSearches ?? [] + var thinnedQuickAccessSuggestions : [OCSavedSearch] = [] + + for quickAccessSuggestion in quickAccessSuggestions { + let storedSearch = savedSearches.first(where: { storedSavedSearch in + return storedSavedSearch.searchTerm == quickAccessSuggestion.searchTerm && + storedSavedSearch.customIconName == quickAccessSuggestion.customIconName && + storedSavedSearch.name == quickAccessSuggestion.name && + storedSavedSearch.isTemplate != quickAccessSuggestion.isTemplate && + storedSavedSearch.isQuickAccess == quickAccessSuggestion.isQuickAccess && + storedSavedSearch.useNameAsTitle == quickAccessSuggestion.useNameAsTitle && + storedSavedSearch.scope == quickAccessSuggestion.scope + }) + + if storedSearch == nil { + thinnedQuickAccessSuggestions.append(quickAccessSuggestion) + } + } + + if thinnedQuickAccessSuggestions.count > 0 { + let headerView = ComposedMessageView.sectionHeader(titled: "Quick Access".localized) + headerView.elementInsets = .zero + suggestions.insert(headerView, at: 0) + + suggestions.insert(contentsOf: thinnedQuickAccessSuggestions, at: 1) + } + + return suggestions + } +} diff --git a/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift b/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift index 4a7621390..e28a9cfd8 100644 --- a/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift +++ b/ownCloudAppShared/Client/Account/Controller/BrowserNavigationBookmark+AccountController.swift @@ -34,14 +34,24 @@ public extension BrowserNavigationBookmark { switch type { case .dataItem: + if let savedSearchUUID = savedSearch?.uuid { + // OCSavedSearch.uuid + itemRefs.append(savedSearchUUID as NSString) + } + + if let sidebarItemUUID = sidebarItem?.uuid { + // OCSidebarItem.uuid + itemRefs.append(sidebarItemUUID as NSString) + } + if let driveID = location?.driveID as? NSString { // Respective driveID (OCDrive.dataItemReference) if let bookmarkUUID, let spacesFolderID = BrowserNavigationBookmark(type: .specialItem, bookmarkUUID: bookmarkUUID, specialItem: .spacesFolder).representationSideBarItemRef { // Provide spaces folder as fallback - return [driveID, spacesFolderID] + itemRefs.append(contentsOf: [driveID, spacesFolderID]) } else { - return [driveID] + itemRefs.append(contentsOf: [driveID]) } } else if let locationItemRef = location?.dataItemReference { // OCLocation.dataItemReference @@ -49,18 +59,7 @@ public extension BrowserNavigationBookmark { let rootLocation = OCLocation.legacyRoot rootLocation.bookmarkUUID = bookmarkUUID - return [locationItemRef, rootLocation.dataItemReference] - } else if let savedSearchUUID = savedSearch?.uuid { - // OCSavedSearch.uuid - itemRefs.append(savedSearchUUID as NSString) - - switch specialItem { - case .searchPDFDocuments, .searchDocuments, .searchImages, .searchVideos, .searchAudios: - itemRefs.append(composedItemRef(for: .quickAccessFolder)) - - default: break - - } + itemRefs.append(contentsOf: [locationItemRef, rootLocation.dataItemReference]) } case .specialItem: diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift index aae7ff73e..132e5cf67 100644 --- a/ownCloudAppShared/Client/Actions/Action.swift +++ b/ownCloudAppShared/Client/Actions/Action.swift @@ -57,8 +57,8 @@ public extension OCExtensionType { public extension OCExtensionLocationIdentifier { static let tableRow: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("tableRow") //!< Present as table row action - static let moreItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreDetailItem") //!< Present in "more" card view for a single item in detail view - static let moreDetailItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreItem") //!< Present in "more" card view for a single item + static let moreItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreItem") //!< Present in "more" card view for a single item + static let moreDetailItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreDetailItem") //!< Present in "more" card view for a single item in detail view static let moreFolder: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreFolder") //!< Present in "more" options for a whole folder static let emptyFolder: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("emptyFolder") //!< Present in "more" options for a whole folder static let multiSelection: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("multiSelection") //!< Present as action when selecting multiple items diff --git a/ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift b/ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift index 570c5944b..92c986761 100644 --- a/ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift +++ b/ownCloudAppShared/Client/Actions/Implementations/OpenInWebAppAction.swift @@ -31,43 +31,36 @@ public enum OpenInWebAppActionMode: String { } public class OpenInWebAppAction: Action { - private static var _classSettingsRegistered: Bool = false - override public class var actionExtension: ActionExtension { - if !_classSettingsRegistered { - _classSettingsRegistered = true - - self.registerOCClassSettingsDefaults([ - .openInWebAppMode : OpenInWebAppActionMode.auto.rawValue - ], metadata: [ - .openInWebAppMode : [ - .type : OCClassSettingsMetadataType.string, - .label : "Open In WebApp mode", - .description : "Determines how to open a document in a web app.", - .status : OCClassSettingsKeyStatus.advanced, - .category : "Actions", - .possibleValues : [ - [ - OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.auto.rawValue, - OCClassSettingsMetadataKey.description : "Open using `\(OpenInWebAppActionMode.inAppWithDefaultBrowserOption.rawValue)`, unless the respective endpoint is not available - in which case `\(OpenInWebAppActionMode.defaultBrowser.rawValue)` is used instead. If no endpoint to open the document is available, an error message is shown." - ], - [ - OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.defaultBrowser.rawValue, - OCClassSettingsMetadataKey.description : "Open in default browser app. May require user to sign in." - ], - [ - OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.inApp.rawValue, - OCClassSettingsMetadataKey.description : "Open inline in an in-app browser." - ], - [ - OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.inAppWithDefaultBrowserOption.rawValue, - OCClassSettingsMetadataKey.description : "Open inline in an in-app browser, but provide a button to open the document in the default browser (may require the user to sign in)." - ] + public static func registerSettings() { + self.registerOCClassSettingsDefaults([ + .openInWebAppMode : OpenInWebAppActionMode.auto.rawValue + ], metadata: [ + .openInWebAppMode : [ + .type : OCClassSettingsMetadataType.string, + .label : "Open In WebApp mode", + .description : "Determines how to open a document in a web app.", + .status : OCClassSettingsKeyStatus.advanced, + .category : "Actions", + .possibleValues : [ + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.auto.rawValue, + OCClassSettingsMetadataKey.description : "Open using `\(OpenInWebAppActionMode.inAppWithDefaultBrowserOption.rawValue)`, unless the respective endpoint is not available - in which case `\(OpenInWebAppActionMode.defaultBrowser.rawValue)` is used instead. If no endpoint to open the document is available, an error message is shown." + ], + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.defaultBrowser.rawValue, + OCClassSettingsMetadataKey.description : "Open in default browser app. May require user to sign in." + ], + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.inApp.rawValue, + OCClassSettingsMetadataKey.description : "Open inline in an in-app browser." + ], + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.inAppWithDefaultBrowserOption.rawValue, + OCClassSettingsMetadataKey.description : "Open inline in an in-app browser, but provide a button to open the document in the default browser (may require the user to sign in)." ] ] - ]) - } - - return super.actionExtension + ] + ]) } override public class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openinwebapp") } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift index 82c576c0d..8476322ef 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/SavedSearchCell.swift @@ -34,6 +34,7 @@ class SavedSearchCell: ThemeableCollectionViewCell { let iconView = UIImageView() let titleLabel = ThemeCSSLabel(withSelectors: [.title]) let segmentView = SegmentView(with: [], truncationMode: .truncateTail) + let sideButton = UIButton() var iconInsets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 0, right: 5) var titleInsets: UIEdgeInsets = UIEdgeInsets(top: 5, left: 3, bottom: 5, right: 3) @@ -52,19 +53,53 @@ class SavedSearchCell: ThemeableCollectionViewCell { var items: [SegmentViewItem]? { didSet { segmentView.items = items ?? [] + hasItemsConstraint?.isActive = segmentView.items.count > 0 } } + private var sideButtonUIAction: UIAction? + var sideButtonAction: OCAction? { + didSet { + if let sideButtonUIAction { + sideButton.removeAction(sideButtonUIAction, for: .primaryActionTriggered) + self.sideButtonUIAction = nil + } + + if let sideButtonAction { + sideButton.configuration?.image = sideButtonAction.icon + sideButton.configuration?.imagePadding = (sideButtonAction.icon != nil) ? 5 : 0 + sideButton.configuration?.title = sideButtonAction.title + sideButton.configuration?.buttonSize = .small + sideButton.isEnabled = true + sideButton.isHidden = false + + sideButtonUIAction = sideButtonAction.uiAction() + if let sideButtonUIAction { + sideButton.addAction(sideButtonUIAction, for: .primaryActionTriggered) + } + } else { + sideButton.isHidden = true + sideButton.isEnabled = false + sideButton.configuration?.image = nil + sideButton.configuration?.title = "" + } + } + } + + private var hasItemsConstraint: NSLayoutConstraint? + func configure() { cssSelector = .savedSearch iconView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false segmentView.translatesAutoresizingMaskIntoConstraints = false + sideButton.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(titleLabel) contentView.addSubview(iconView) contentView.addSubview(segmentView) + contentView.addSubview(sideButton) iconView.image = icon iconView.contentMode = .scaleAspectFit @@ -78,7 +113,11 @@ class SavedSearchCell: ThemeableCollectionViewCell { iconView.setContentHuggingPriority(.required, for: .horizontal) - let backgroundConfig = UIBackgroundConfiguration.clear() + sideButton.configuration = .tinted() + sideButton.configuration?.title = "Add to sidebar".localized + + var backgroundConfig = UIBackgroundConfiguration.clear() + backgroundConfig.cornerRadius = 10 backgroundConfiguration = backgroundConfig } @@ -91,6 +130,8 @@ class SavedSearchCell: ThemeableCollectionViewCell { titleLabel.font = UIFont.preferredFont(forTextStyle: .body) titleLabel.adjustsFontForContentSizeCategory = true + hasItemsConstraint = titleLabel.bottomAnchor.constraint(equalTo: segmentView.topAnchor, constant: -titleSegmentSpacing) + self.configuredConstraints = [ iconView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: iconInsets.left), iconView.trailingAnchor.constraint(equalTo: titleLabel.leadingAnchor, constant: -(iconInsets.right + titleInsets.left)), @@ -103,11 +144,15 @@ class SavedSearchCell: ThemeableCollectionViewCell { titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -titleInsets.right), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: titleInsets.top), - titleLabel.bottomAnchor.constraint(equalTo: segmentView.topAnchor, constant: -titleSegmentSpacing), + titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -titleInsets.bottom).with(priority: .defaultHigh), // Constraint effective if the cell has no items, overridden by hasItemsConstraint if active + hasItemsConstraint!, // Constraint effective if the cell has items segmentView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), segmentView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), - segmentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -titleInsets.bottom) + segmentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -titleInsets.bottom), + + sideButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -titleInsets.right), + sideButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ] } } @@ -162,15 +207,37 @@ extension OCSavedSearch { extension SavedSearchCell { static let savedTemplateIcon = OCSymbol.icon(forSymbolName: "square.dashed.inset.filled") - static let savedSearchIcon = OCSymbol.icon(forSymbolName: "gearshape.fill") + static let savedSearchIcon = OCSymbol.icon(forSymbolName: "folder.badge.gearshape") static func registerCellProvider() { let savedSearchCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in if let savedSearch = OCDataRenderer.default.renderItem(item, asType: .savedSearch, error: nil, withOptions: nil) as? OCSavedSearch { + var icon: UIImage? + + if let customIconName = savedSearch.customIconName { + icon = OCSymbol.icon(forSymbolName: customIconName) + } + + if icon == nil { + icon = savedSearch.isTemplate ? savedTemplateIcon : savedSearchIcon + } + cell.title = savedSearch.displayName - cell.icon = savedSearch.isTemplate ? savedTemplateIcon : savedSearchIcon - cell.items = savedSearch.segmentViewItemsForDisplay + cell.icon = icon + cell.items = (savedSearch.isQuickAccess == true) ? nil : savedSearch.segmentViewItemsForDisplay + + let clientContext = cellConfiguration.clientContext + + cell.sideButtonAction = savedSearch.isQuickAccess == true && savedSearch.isTemplate ? OCAction(title: "Add to sidebar".localized, icon: OCSymbol.icon(forSymbolName: "plus.circle.fill"), action: { [weak clientContext] _, _, completed in + // Make a copy of the saved search object, so it has a different UUID (avoiding ID clashes in collection views) and can be modified + if let saveSearch = savedSearch.copy() as? OCSavedSearch { + saveSearch.isQuickAccess = true + saveSearch.isTemplate = false + clientContext?.core?.vault.add(saveSearch) + } + completed(nil) + }) : nil } }) } diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift index e28348d83..fb8689955 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewCellProvider+StandardImplementations.swift @@ -18,6 +18,7 @@ import UIKit import ownCloudSDK +import ownCloudApp public extension CollectionViewCellProvider { static func registerStandardImplementations() { @@ -37,6 +38,9 @@ public extension CollectionViewCellProvider { OCIdentity.registerUniversalCellProvider() // Cell providers for .identity OptionItem.registerUniversalCellProvider() // Cell providers for .optionItem + // Other cell providers + OCSidebarItem.registerCellProvider() // Cell provider for .sidebarItem + // Register cell providers for .presentable registerPresentableCellProvider() } diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift index b9d428724..965dc8311 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift @@ -527,6 +527,17 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, var snapshot = NSDiffableDataSourceSnapshot() var snapshotsBySection = [CollectionViewSection.SectionIdentifier : NSDiffableDataSourceSectionSnapshot]() var updatedItems : [CollectionViewController.ItemRef] = [] + var selectedItemRefs: [ItemRef]? + + let updateWithAnimation = animatingDifferences && !useWrappedIdentifiers + + if !updateWithAnimation { + // Selection is lost when updating without animation (via https://forums.developer.apple.com/forums/thread/656529?answerId=627227022#627227022) + let selectedIndexPaths = collectionView.indexPathsForSelectedItems + selectedItemRefs = selectedIndexPaths?.compactMap({ indexPath in + return collectionViewDataSource.itemIdentifier(for: indexPath) + }) + } // Log.debug("<=========================>") @@ -541,7 +552,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } } - collectionViewDataSource.apply(snapshot, animatingDifferences: animatingDifferences && !useWrappedIdentifiers) + collectionViewDataSource.apply(snapshot, animatingDifferences: updateWithAnimation) if useWrappedIdentifiers { for section in sections { @@ -563,6 +574,23 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } } + // Restore selected item refs + if let selectedItemRefs { + var selectIndexPaths: [IndexPath]? + + selectIndexPaths = selectedItemRefs.compactMap({ itemRef in + collectionViewDataSource.indexPath(for: itemRef) + }) + + if let selectIndexPaths, selectIndexPaths.count > 0 { + OnMainThread { + for selectedIndexPath in selectIndexPaths { + self.collectionView.selectItem(at: selectedIndexPath, animated: false, scrollPosition: .centeredVertically) + } + } + } + } + // Notify view controller of content updates setContentDidUpdate() } @@ -635,6 +663,17 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } } + // MARK: - Client Contexts + open func clientContext(for indexPath: IndexPath?) -> ClientContext? { + var sectionClientContext: ClientContext? + + if let indexPath, let section = section(at: indexPath.section) { + sectionClientContext = section.clientContext + } + + return sectionClientContext ?? clientContext + } + // MARK: - Item references public typealias ItemRef = NSObject public class WrappedItem : NSObject { @@ -998,7 +1037,7 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, return nil } - if let menuItems = contextMenuInteraction.composeContextMenuItems(in: self, location: .contextMenuItem, with: self.clientContext) { + if let menuItems = contextMenuInteraction.composeContextMenuItems(in: self, location: .contextMenuItem, with: clientContext) { return UIMenu(title: "", children: menuItems) } @@ -1141,21 +1180,36 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, var lastDropProposalDestinationIndexPath : IndexPath? var lastDropProposalDestinationIndexPathValid : Bool = false + func dropInteraction(for destinationIndexPath: IndexPath?) -> DataItemDropInteraction? { + var dropInteraction: DataItemDropInteraction? + + if let item = targetedDataItem(for: destinationIndexPath, interaction: .acceptDrop), + let itemDropInteraction = item as? DataItemDropInteraction { + dropInteraction = itemDropInteraction + } + + if let sectionIndex = destinationIndexPath?.section, + let section = section(at: sectionIndex), + let sectionDropInteraction = section.sectionDropInteraction { + dropInteraction = sectionDropInteraction + } + + return dropInteraction + } + public func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { updateDropTargetsFor(collectionView, dropSession: session) Log.debug("Destination index path: \(String(describing: destinationIndexPath))") - if let item = targetedDataItem(for: destinationIndexPath, interaction: .acceptDrop), - let dropInteraction = item as? DataItemDropInteraction { - if let dropProposal = dropInteraction.allowDropOperation?(for: session, with: clientContext) { - // Save last requested indexPath because UICollectionViewDropCoordinator.destinationIndexPath will only return the last hit-tested one, - // so that dropping into a cell-less region of the collection view will have UICollectionViewDropCoordinator.destinationIndexPath return - // the last hit-tested cell's indexPath - rather than (the accurate) nil - lastDropProposalDestinationIndexPath = destinationIndexPath - lastDropProposalDestinationIndexPathValid = true - return dropProposal - } + if let dropInteraction = dropInteraction(for: destinationIndexPath), + let dropProposal = dropInteraction.allowDropOperation?(for: session, with: clientContext(for: destinationIndexPath)) { + // Save last requested indexPath because UICollectionViewDropCoordinator.destinationIndexPath will only return the last hit-tested one, + // so that dropping into a cell-less region of the collection view will have UICollectionViewDropCoordinator.destinationIndexPath return + // the last hit-tested cell's indexPath - rather than (the accurate) nil + lastDropProposalDestinationIndexPath = destinationIndexPath + lastDropProposalDestinationIndexPathValid = true + return dropProposal } lastDropProposalDestinationIndexPathValid = false @@ -1164,11 +1218,10 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, } public func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { - if let item = targetedDataItem(for: (lastDropProposalDestinationIndexPathValid ? lastDropProposalDestinationIndexPath : coordinator.destinationIndexPath), interaction: .acceptDrop), - let dropInteraction = item as? DataItemDropInteraction { + if let dropInteraction = dropInteraction(for: (lastDropProposalDestinationIndexPathValid ? lastDropProposalDestinationIndexPath : coordinator.destinationIndexPath)) { let dragItems = coordinator.items.compactMap { collectionViewDropItem in collectionViewDropItem.dragItem } - dropInteraction.performDropOperation(of: dragItems, with: clientContext, handlingCompletion: { didSucceed in + dropInteraction.performDropOperation(of: dragItems, with: clientContext(for: coordinator.destinationIndexPath), handlingCompletion: { didSucceed in }) } } diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift index 8edc2a1f2..0bff16002 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewSection.swift @@ -691,6 +691,9 @@ public class CollectionViewSection: NSObject, OCDataItem, OCDataItemVersioning { return layoutSection } + // MARK: - Drag and drop additions + var sectionDropInteraction: DataItemDropInteraction? + // MARK: - Data Item & Versioning conformance public let dataItemType: OCDataItemType = .collectionViewSection diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift index 409f231b6..a5e8b7733 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCLocation+Interactions.swift @@ -22,7 +22,7 @@ import ownCloudApp // MARK: - Selection > Open extension OCLocation : DataItemSelectionInteraction { - public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + public func customizedOpenItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, customizeViewController: ((ClientItemViewController) -> Void)?, completion: ((Bool) -> Void)?) -> UIViewController? { let driveContext = ClientContext(with: context, modifier: { context in if let driveID = self.driveID, let core = context.core { context.drive = core.drive(withIdentifier: driveID) @@ -42,6 +42,8 @@ extension OCLocation : DataItemSelectionInteraction { viewController.navigationBookmark = BrowserNavigationBookmark.from(dataItem: location, clientContext: context, restoreAction: .open) viewController.revoke(in: context, when: [ .connectionClosed, .driveRemoved ]) + customizeViewController?(viewController) + return viewController }, push: pushViewController, animated: animated) @@ -50,6 +52,10 @@ extension OCLocation : DataItemSelectionInteraction { return locationViewController } + public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + return customizedOpenItem(from: viewController, with: context, animated: animated, pushViewController: pushViewController, customizeViewController: nil, completion: completion) + } + public func revealItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { if let core = context?.core { if let item = try? core.cachedItem(at: self) { diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift index 6e9c233bc..a9349cfb7 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCSavedSearch+Interactions.swift @@ -24,6 +24,7 @@ extension OCSavedSearchUserInfoKey { static let customIconName = OCSavedSearchUserInfoKey(rawValue: "customIconName") static let useNameAsTitle = OCSavedSearchUserInfoKey(rawValue: "useNameAsTitle") static let useSortDescriptor = OCSavedSearchUserInfoKey(rawValue: "useSortDescriptor") + static let isQuickAccess = OCSavedSearchUserInfoKey(rawValue: "isQuickAccess") } extension OCSavedSearch { @@ -37,6 +38,10 @@ extension OCSavedSearch { }) } + func canRename(in context: ClientContext?) -> Bool { + return canDelete(in: context) && (isQuickAccess != true) + } + func delete(in context: ClientContext?) { guard let context = context, let core = context.core else { return @@ -45,6 +50,31 @@ extension OCSavedSearch { core.vault.delete(self) } + func rename(in context: ClientContext?) { + guard let context = context, context.core != nil else { + return + } + + let namePrompt = UIAlertController(title: "Name of saved search".localized, message: nil, preferredStyle: .alert) + + namePrompt.addTextField(configurationHandler: { textField in + textField.placeholder = "Saved search".localized + textField.text = self.isNameUserDefined ? self.name : "" + }) + + namePrompt.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel)) + namePrompt.addAction(UIAlertAction(title: "Save".localized, style: .default, handler: { [weak self, weak namePrompt] action in + guard let self else { + return + } + self.name = namePrompt?.textFields?.first?.text ?? "" + + context.core?.vault.update(self) + })) + + context.present(namePrompt, animated: true) + } + func condition() -> OCQueryCondition? { let searchTermCondition = OCQueryCondition.fromSearchTerm(searchTerm) var composedCondition = searchTermCondition @@ -123,6 +153,20 @@ extension OCSavedSearch { } } + var isQuickAccess: Bool? { + set { + if userInfo == nil, let newValue { + userInfo = [.isQuickAccess : newValue] + } else { + userInfo?[.isQuickAccess] = newValue + } + } + + get { + return userInfo?[.isQuickAccess] as? Bool + } + } + func withCustomIcon(name: String) -> OCSavedSearch { customIconName = name return self @@ -137,6 +181,11 @@ extension OCSavedSearch { useSortDescriptor = sortDescriptor return self } + + func isQuickAccess(_ isQuickAccess: Bool) -> OCSavedSearch { + self.isQuickAccess = isQuickAccess + return self + } } extension OCSavedSearch: DataItemSelectionInteraction { @@ -210,18 +259,35 @@ extension OCSavedSearch: DataItemSwipeInteraction { extension OCSavedSearch: DataItemContextMenuInteraction { public func composeContextMenuItems(in viewController: UIViewController?, location: OCExtensionLocationIdentifier, with context: ClientContext?) -> [UIMenuElement]? { - guard canDelete(in: context) else { + let canDelete = canDelete(in: context), canRename = canRename(in: context) + var actions: [UIMenuElement] = [] + + guard canDelete || canRename else { return nil } - let deleteAction = UIAction(handler: { [weak self] action in - self?.delete(in: context) - }) - deleteAction.title = "Delete".localized - deleteAction.image = OCSymbol.icon(forSymbolName: "trash") - deleteAction.attributes = .destructive + if canRename { + let renameAction = UIAction(handler: { [weak self] action in + self?.rename(in: context) + }) + renameAction.title = "Rename".localized + renameAction.image = OCSymbol.icon(forSymbolName: "pencil") + + actions.append(renameAction) + } + + if canDelete { + let deleteAction = UIAction(handler: { [weak self] action in + self?.delete(in: context) + }) + deleteAction.title = "Delete".localized + deleteAction.image = OCSymbol.icon(forSymbolName: "trash") + deleteAction.attributes = .destructive + + actions.append(deleteAction) + } - return [ deleteAction ] + return actions } } diff --git a/ownCloudAppShared/Client/Search/SearchViewController.swift b/ownCloudAppShared/Client/Search/SearchViewController.swift index f40aa699f..d5c24e90e 100644 --- a/ownCloudAppShared/Client/Search/SearchViewController.swift +++ b/ownCloudAppShared/Client/Search/SearchViewController.swift @@ -225,6 +225,9 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl } // MARK: - Navigation item modification + open var showCancelButton: Bool = true + open var hideNavigationButtons: Bool = true + var targetNavigationItem: UINavigationItem? var niInjected: Bool = false @@ -232,18 +235,29 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl func injectIntoNavigationItem() { if !niInjected, let targetNavigationItem { + var navigationContentItems: [NavigationContentItem] = [] + // Store content niHidesBackButton = targetNavigationItem.hidesBackButton - // Alternative implementation as a standard "Cancel" button, more convention compliant, but needs more space: let cancelToolbarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(endSearch)) - let cancelToolbarButton = UIBarButtonItem(image: OCSymbol.icon(forSymbolName: "xmark"), style: .done, target: self, action: #selector(endSearch)) + // Compose navigation views + // - "Overwrite" <> navigation buttons with no content + if hideNavigationButtons { + navigationContentItems.append(NavigationContentItem(identifier: "search-left", area: .left, priority: .highest, position: .trailing, items: [ ])) + } + + // - add search field + navigationContentItems.append(NavigationContentItem(identifier: "search-field", area: .title, priority: .highest, position: .leading, titleView: searchField)) + + // - add X (cancel) button + if showCancelButton { + // Alternative implementation as a standard "Cancel" button, more convention compliant, but needs more space: let cancelToolbarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(endSearch)) + let cancelToolbarButton = UIBarButtonItem(image: OCSymbol.icon(forSymbolName: "xmark"), style: .done, target: self, action: #selector(endSearch)) + navigationContentItems.append(NavigationContentItem(identifier: "search-right", area: .right, priority: .highest, position: .trailing, items: [ cancelToolbarButton ])) + } // Overwrite content - targetNavigationItem.navigationContent.add(items: [ - NavigationContentItem(identifier: "search-left", area: .left, priority: .highest, position: .trailing, items: [ ]), - NavigationContentItem(identifier: "search-field", area: .title, priority: .highest, position: .leading, titleView: searchField), - NavigationContentItem(identifier: "search-right", area: .right, priority: .highest, position: .trailing, items: [ cancelToolbarButton ]) - ]) + targetNavigationItem.navigationContent.add(items: navigationContentItems) targetNavigationItem.hidesBackButton = true niInjected = true @@ -251,7 +265,7 @@ open class SearchViewController: UIViewController, UITextFieldDelegate, Themeabl } func restoreNavigationItem() { - if niInjected, let targetNavigationItem = targetNavigationItem { + if niInjected, let targetNavigationItem { // Restore content targetNavigationItem.navigationContent.remove(itemsWithIdentifiers: [ "search-left", diff --git a/ownCloudAppShared/Client/Sidebar Items/OCSidebarItem+Cell.swift b/ownCloudAppShared/Client/Sidebar Items/OCSidebarItem+Cell.swift new file mode 100644 index 000000000..ab35ac137 --- /dev/null +++ b/ownCloudAppShared/Client/Sidebar Items/OCSidebarItem+Cell.swift @@ -0,0 +1,43 @@ +// +// OCSidebarItem+Cell.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 28.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +extension OCSidebarItem { + static func registerCellProvider() { + let sidebarItemSidebarCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, collectionItemRef) in + var content = cell.defaultContentConfiguration() + + collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in + if let savedSearch = OCDataRenderer.default.renderItem(item, asType: .sidebarItem, error: nil, withOptions: nil) as? OCSidebarItem { + content.text = savedSearch.location?.displayName(in: cellConfiguration.clientContext) + content.image = OCSymbol.icon(forSymbolName: "folder") + } + }) + + cell.backgroundConfiguration = .listSidebarCell() + cell.contentConfiguration = content + cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) + } + + CollectionViewCellProvider.register(CollectionViewCellProvider(for: .sidebarItem, with: { collectionView, cellConfiguration, itemRecord, itemRef, indexPath in + return collectionView.dequeueConfiguredReusableCell(using: sidebarItemSidebarCellRegistration, for: indexPath, item: itemRef) + })) + } +} diff --git a/ownCloudAppShared/Client/Sidebar Items/OCSidebarItem+Interactions.swift b/ownCloudAppShared/Client/Sidebar Items/OCSidebarItem+Interactions.swift new file mode 100644 index 000000000..26c4fe312 --- /dev/null +++ b/ownCloudAppShared/Client/Sidebar Items/OCSidebarItem+Interactions.swift @@ -0,0 +1,142 @@ +// +// OCSidebarItem+Interactions.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 28.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudApp + +extension OCSidebarItem { + func delete(in context: ClientContext?) { + guard let context = context, let core = context.core else { + return + } + + core.vault.delete(self) + } +} + +// MARK: - Selection > Open +extension OCSidebarItem : DataItemSelectionInteraction { + public func openItem(from viewController: UIViewController?, with context: ClientContext?, animated: Bool, pushViewController: Bool, completion: ((Bool) -> Void)?) -> UIViewController? { + var viewController: UIViewController? + + if let location { + viewController = location.customizedOpenItem(from: viewController, with: context, animated: animated, pushViewController: pushViewController, customizeViewController: { itemViewController in + itemViewController.navigationBookmark = BrowserNavigationBookmark.from(dataItem: self, clientContext: context, restoreAction: .open) + }, completion: completion) + } + + return viewController + } +} + +// MARK: - Swipe Interaction +extension OCSidebarItem: DataItemSwipeInteraction { + public func provideTrailingSwipeActions(with context: ClientContext?) -> UISwipeActionsConfiguration? { + let deleteAction = UIContextualAction(style: .destructive, title: "Remove".localized, handler: { [weak self] (_ action, _ view, _ uiCompletionHandler) in + uiCompletionHandler(false) + self?.delete(in: context) + }) + deleteAction.image = OCSymbol.icon(forSymbolName: "trash") + + return UISwipeActionsConfiguration(actions: [ deleteAction ]) + } +} + +// MARK: - Context Menu Interaction +extension OCSidebarItem: DataItemContextMenuInteraction { + public func composeContextMenuItems(in viewController: UIViewController?, location: OCExtensionLocationIdentifier, with context: ClientContext?) -> [UIMenuElement]? { + let deleteAction = UIAction(handler: { [weak self] action in + self?.delete(in: context) + }) + deleteAction.title = "Remove".localized + deleteAction.image = OCSymbol.icon(forSymbolName: "trash") + deleteAction.attributes = .destructive + + return [ deleteAction ] + } +} + +// MARK: - Drop +extension OCSidebarItem : DataItemDropInteraction { + public func allowDropOperation(for session: UIDropSession, with context: ClientContext?) -> UICollectionViewDropProposal? { + if session.localDragSession != nil { + // Prevent drop of items onto themselves - or in their existing location + if let dragItems = session.localDragSession?.items, let bookmarkUUID = context?.core?.bookmark.uuid { + for dragItem in dragItems { + if let localDataItem = dragItem.localObject as? LocalDataItem { + if let item = localDataItem.dataItem as? OCItem, localDataItem.bookmarkUUID == bookmarkUUID, item.driveID == location?.driveID, let itemLocation = item.location { + if (item.path == location?.path) || (itemLocation.parent?.path == location?.path) { + return UICollectionViewDropProposal(operation: .cancel, intent: .unspecified) + } + } + } + } + } + + // Return drop proposal based on item type + switch location?.type { + case .folder?, .drive?, .account?: + return UICollectionViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) + + default: + return UICollectionViewDropProposal(operation: .move) + } + } else { + // External items from other apps can only be copied into the app + return UICollectionViewDropProposal(operation: .copy) + } + } + + public func performDropOperation(of droppedItems: [UIDragItem], with context: ClientContext?, handlingCompletion: @escaping (_ didSucceed: Bool) -> Void) { + if let location { + context?.core?.cachedItem(at: location, resultHandler: { error, item in + OnMainThread { + if let item { + item.performDropOperation(of: droppedItems, with: context, handlingCompletion: handlingCompletion) + handlingCompletion(true) + } else { + handlingCompletion(false) + } + } + }) + } else { + handlingCompletion(false) + } + } +} + +// MARK: - BrowserNavigationBookmark (re)store +extension OCSidebarItem: DataItemBrowserNavigationBookmarkReStore { + public func store(in bookmarkUUID: UUID?, context: ClientContext?, restoreAction: BrowserNavigationBookmark.BookmarkRestoreAction) -> BrowserNavigationBookmark? { + let navigationBookmark = BrowserNavigationBookmark(for: self, in: bookmarkUUID, restoreAction: restoreAction) + + navigationBookmark?.sidebarItem = self + navigationBookmark?.location = self.location + + return navigationBookmark + } + + public static func restore(navigationBookmark: BrowserNavigationBookmark, in viewController: UIViewController?, with context: ClientContext?, completion: ((Error?, UIViewController?) -> Void)) { + if let sidebarItem = navigationBookmark.sidebarItem { + let viewController = sidebarItem.openItem(from: viewController, with: context, animated: false, pushViewController: false, completion: nil) + completion(nil, viewController) + } else { + completion(NSError(ocError: .insufficientParameters), nil) + } + } +} diff --git a/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift index 990cd564b..1e5c59717 100644 --- a/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift +++ b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift @@ -673,10 +673,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } set { - if navigationItem.titleView is ClientLocationPopupButton { - navigationItem.titleView = nil - } - + navigationItem.navigationContent.remove(itemsWithIdentifier: "navigation-location") navigationItem.titleLabelText = newValue navigationItem.title = newValue } @@ -689,11 +686,11 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, var navigationLocation: OCLocation? { didSet { if let clientContext, let navigationLocation, !navigationLocation.isRoot { - navigationItem.titleView = ClientLocationPopupButton(clientContext: clientContext, location: navigationLocation) + navigationItem.navigationContent.add(items: [NavigationContentItem(identifier: "navigation-location", area: .title, priority: .standard, position: .leading, titleView: + ClientLocationPopupButton(clientContext: clientContext, location: navigationLocation) + )]) } else { - if navigationItem.titleView is ClientLocationPopupButton { - navigationItem.titleView = nil - } + navigationItem.navigationContent.remove(itemsWithIdentifier: "navigation-location") } } } @@ -725,7 +722,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, self.navigationLocation = navigationLocation } - if navigationLocation == nil || !useNavigationLocationBreadcrumbDropdown { + if navigationLocation == nil || !useNavigationLocationBreadcrumbDropdown || navigationLocation?.isRoot == true { if let navigationTitle { self.navigationTitle = navigationTitle } else { @@ -1019,25 +1016,38 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // MARK: - Search open var searchViewController: SearchViewController? + open func searchScopes(for clientContext: ClientContext, cellStyle: CollectionViewCellStyle) -> [SearchScope] { + var scopes : [SearchScope] = [] + + if clientContext.query?.queryLocation != nil { + // - Folder + // - Tree (folder + subfolders) + scopes.append(contentsOf: [ + // - In this folder + .modifyingQuery(with: clientContext, localizedName: "Folder".localized), + + // - Folder and subfolders (tree / container) + .containerSearch(with: clientContext, cellStyle: cellStyle, localizedName: "Tree".localized) + ]) + + // - Drive + if clientContext.core?.useDrives == true { + let driveName = "Space".localized + scopes.append(.driveSearch(with: clientContext, cellStyle: cellStyle, localizedName: driveName)) + } + } + + // - Account + scopes.append(.accountSearch(with: clientContext, cellStyle: cellStyle, localizedName: "Account".localized)) + + return scopes + } + @objc open func startSearch() { if searchViewController == nil { if let clientContext = clientContext, let cellStyle = itemSection?.cellStyle { - var scopes : [SearchScope] = [ - // - In this folder - .modifyingQuery(with: clientContext, localizedName: "Folder".localized), - - // - Folder and subfolders (tree / container) - .containerSearch(with: clientContext, cellStyle: cellStyle, localizedName: "Tree".localized) - ] - - // - Drive - if clientContext.core?.useDrives == true { - let driveName = "Space".localized - scopes.append(.driveSearch(with: clientContext, cellStyle: cellStyle, localizedName: driveName)) - } - - // - Account - scopes.append(.accountSearch(with: clientContext, cellStyle: cellStyle, localizedName: "Account".localized)) + // Scopes + let scopes = searchScopes(for: clientContext, cellStyle: cellStyle) // No results let noResultContent = SearchViewController.Content(type: .noResults, source: OCDataSourceArray(), style: emptySection!.cellStyle) @@ -1049,45 +1059,12 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // Suggestion view let suggestionsSource = OCDataSourceArray() - let suggestionsContent = SearchViewController.Content(type: .suggestion, source: suggestionsSource, style: emptySection!.cellStyle) - - if let vault = clientContext.core?.vault { - vault.addSavedSearchesObserver(suggestionsSource, withInitial: true) { suggestionsSource, savedSearches, isInitial in - guard let suggestionsSource = suggestionsSource as? OCDataSourceArray else { - return - } - - var suggestionItems : [OCDataItem & OCDataItemVersioning] = [] - - // Offer saved search templates - if let savedTemplates = vault.savedSearches?.filter({ savedSearch in - return savedSearch.isTemplate - }), savedTemplates.count > 0 { - let savedSearchTemplatesHeaderView = ComposedMessageView.sectionHeader(titled: "Saved search templates".localized) - savedSearchTemplatesHeaderView.elementInsets = .zero - - suggestionItems.append(savedSearchTemplatesHeaderView) - suggestionItems.append(contentsOf: savedTemplates) - } - - // Offer saved searches - if let savedSearches = vault.savedSearches?.filter({ savedSearch in - return !savedSearch.isTemplate - }), savedSearches.count > 0 { - let savedSearchTemplatesHeaderView = ComposedMessageView.sectionHeader(titled: "Saved searches".localized) - savedSearchTemplatesHeaderView.elementInsets = .zero - - suggestionItems.append(savedSearchTemplatesHeaderView) - suggestionItems.append(contentsOf: savedSearches) - } + suggestionsSource.trackItemVersions = true - // Provide "Enter a search term" placeholder if there is no other content available - if suggestionItems.count == 0 { - suggestionItems.append( ComposedMessageView.infoBox(image: nil, subtitle: "Enter a search term".localized) ) - } + let suggestionsContent = SearchViewController.Content(type: .suggestion, source: suggestionsSource, style: emptySection!.cellStyle) - suggestionsSource.setVersionedItems(suggestionItems) - } + if clientContext.core?.vault != nil { + startProvidingSearchSuggestions(to: suggestionsSource, in: clientContext) } // Create and install SearchViewController @@ -1100,6 +1077,62 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, } } + func startProvidingSearchSuggestions(to suggestionsSource: OCDataSourceArray, in clientContext: ClientContext) { + if let vault = clientContext.core?.vault { + // Observe saved searches for changes and trigger updates accordingly + // This observer will automatically be removed once suggestionsSource is deallocated + vault.addSavedSearchesObserver(suggestionsSource, withInitial: true) { [weak clientContext, weak self] suggestionsSource, savedSearches, isInitial in + guard let suggestionsSource = suggestionsSource as? OCDataSourceArray, let self, let clientContext else { + return + } + + var suggestionItems : [OCDataItem & OCDataItemVersioning] = [] + + suggestionItems = self.composeSuggestionContents(from: vault.savedSearches, clientContext: clientContext, includingFallbacks: true) + + // Provide "Enter a search term" placeholder if there is no other content available + if suggestionItems.count == 0 { + suggestionItems.append( ComposedMessageView.infoBox(image: nil, subtitle: "Enter a search term".localized) ) + } + + suggestionsSource.setVersionedItems(suggestionItems) + } + } + } + + open func composeSuggestionContents(from savedSearches: [OCSavedSearch]?, clientContext: ClientContext, includingFallbacks: Bool) -> [OCDataItem & OCDataItemVersioning] { + var suggestionItems : [OCDataItem & OCDataItemVersioning] = [] + + // Offer saved search templates + if let savedTemplates = savedSearches?.filter({ savedSearch in + return savedSearch.isTemplate + }), savedTemplates.count > 0 { + let savedSearchTemplatesHeaderView = ComposedMessageView.sectionHeader(titled: "Search templates".localized) + savedSearchTemplatesHeaderView.elementInsets = .zero + + suggestionItems.append(savedSearchTemplatesHeaderView) + suggestionItems.append(contentsOf: savedTemplates) + } + + // Offer saved searches + if let savedSearches = savedSearches?.filter({ savedSearch in + return !savedSearch.isTemplate + }), savedSearches.count > 0 { + let savedSearchTemplatesHeaderView = ComposedMessageView.sectionHeader(titled: "Saved searches".localized) + savedSearchTemplatesHeaderView.elementInsets = .zero + + suggestionItems.append(savedSearchTemplatesHeaderView) + suggestionItems.append(contentsOf: savedSearches) + } + + // Provide "Enter a search term" placeholder if there is no other content available + if suggestionItems.count == 0, includingFallbacks { + suggestionItems.append( ComposedMessageView.infoBox(image: nil, subtitle: "Enter a search term".localized) ) + } + + return suggestionItems + } + func endSearch() { if let searchViewController = searchViewController { self.removeStacked(child: searchViewController) diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift index a530e8add..d87d6eabe 100644 --- a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationBookmark.swift @@ -33,11 +33,12 @@ open class BrowserNavigationBookmark: NSObject, NSSecureCoding { open var specialItem: AccountController.SpecialItem? open var savedSearch: OCSavedSearch? + open var sidebarItem: OCSidebarItem? open var restoreFromClass: String? open var restoreAction: BookmarkRestoreAction? - public init(type: BookmarkType, bookmarkUUID: UUID? = nil, location: OCLocation? = nil, itemLocalID: String? = nil, specialItem: AccountController.SpecialItem? = nil, savedSearchUUID: String? = nil, savedSearch: OCSavedSearch? = nil, restoreFromClass: String? = nil, action: BookmarkRestoreAction? = nil) { + public init(type: BookmarkType, bookmarkUUID: UUID? = nil, location: OCLocation? = nil, itemLocalID: String? = nil, specialItem: AccountController.SpecialItem? = nil, savedSearchUUID: String? = nil, savedSearch: OCSavedSearch? = nil, sidebarItem: OCSidebarItem? = nil, restoreFromClass: String? = nil, action: BookmarkRestoreAction? = nil) { self.type = type self.bookmarkUUID = bookmarkUUID @@ -52,6 +53,7 @@ open class BrowserNavigationBookmark: NSObject, NSSecureCoding { self.specialItem = specialItem self.savedSearch = savedSearch + self.sidebarItem = sidebarItem self.restoreFromClass = restoreFromClass self.restoreAction = action @@ -115,6 +117,7 @@ open class BrowserNavigationBookmark: NSObject, NSSecureCoding { coder.encode(specialItem?.rawValue, forKey: "specialItem") coder.encode(savedSearch, forKey: "savedSearch") + coder.encode(sidebarItem, forKey: "sidebarItem") coder.encode(restoreFromClass, forKey: "restoreFromClass") coder.encode(restoreAction, forKey: "restoreAction") @@ -132,6 +135,7 @@ open class BrowserNavigationBookmark: NSObject, NSSecureCoding { specialItem = AccountController.SpecialItem(rawValue: specialItemString) } savedSearch = coder.decodeObject(of: OCSavedSearch.self, forKey: "savedSearch") + sidebarItem = coder.decodeObject(of: OCSidebarItem.self, forKey: "sidebarItem") restoreFromClass = coder.decodeObject(of: NSString.self, forKey: "restoreFromClass") as? String restoreAction = coder.decodeObject(of: NSString.self, forKey: "restoreAction") as? String