diff --git a/ios-sdk b/ios-sdk index 809ccf849..19696380d 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 809ccf849501d67579392de33a5f0ddf60767a45 +Subproject commit 19696380ddbd9447887cec1b3c041e3cdc2ccf6d diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 9a01aa399..b111d583c 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -34,19 +34,35 @@ 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */; }; 23F6238120B587EF004FDE8B /* SortMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F6238020B587EF004FDE8B /* SortMethod.swift */; }; 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FA23E520BFD3D8009A6D73 /* SortBar.swift */; }; + 390B51E02292DBB100935E24 /* SharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */; }; 39104E10223991C8002FC02F /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39104E0A223991C8002FC02F /* UIButton+Extension.swift */; }; + 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */; }; + 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */; }; + 392557FE2278703300E83F60 /* UISearchBar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */; }; 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */; }; 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; + 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */; }; + 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C92289500E00B254A9 /* RoundedLabel.swift */; }; 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */; }; 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D422411EDF00B66713 /* BorderedLabel.swift */; }; 3998F5D72241486F00B66713 /* OCCertificate+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */; }; + 399DD7C722A691BC00B45EB2 /* UnshareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */; }; + 39A5135322608836002CF1AA /* OCShare+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A5135222608836002CF1AA /* OCShare+Extension.swift */; }; + 39A513AC22674E56002CF1AA /* OCCore+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */; }; + 39AFC3D1225E72FB00A6D3AE /* GroupSharingTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */; }; + 39AFC3D8225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */; }; + 39B289A8226F1EE000BE0E11 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B289A7226F1EE000BE0E11 /* MessageView.swift */; }; + 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8AE5228C12100020253B /* Array+Extension.swift */; }; 39CC8B01228C8A950020253B /* MediaUploadSettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */; }; + 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */; }; 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */; }; 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */; }; 39E2FE0021FF814A00F0117F /* ThemeRoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */; }; + 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */; }; + 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */; }; 46B9D336BF7FE50321823888 /* Pods_ownCloudScreenshotsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54199937F74A129BC74DEB0A /* Pods_ownCloudScreenshotsTests.framework */; }; 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */; }; 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1561EE22232357009C4EF3 /* PhotoSelectionViewCell.swift */; }; @@ -66,7 +82,6 @@ 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAF783B2282FD40000C85CF /* FileManager+Extension.swift */; }; 4CC46D212284C677009E938F /* BookmarkInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */; }; 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */; }; - 57E3880E6B8E521D27539509 /* Pods_ownCloudTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56EA84D8AD331FFA604138B /* Pods_ownCloudTests.framework */; }; 59056CAD22414F3C00A18A22 /* ownCloudScreenshotsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59056CAC22414F3C00A18A22 /* ownCloudScreenshotsTests.swift */; }; 59056CB422414F8000A18A22 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59371D7B224103D300C6BC5B /* SnapshotHelper.swift */; }; 5917244E20D3DC2100809B38 /* BiometricalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5917244D20D3DC2100809B38 /* BiometricalTests.swift */; }; @@ -114,7 +129,6 @@ 6E91F37E21ECA6FD009436D2 /* CopyAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */; }; 6E938522220322F30049D676 /* SplashImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 6E93851C220322F30049D676 /* SplashImage.png */; }; 6EA78B8F2179B55400A5216A /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA78B8E2179B55400A5216A /* ImageScrollView.swift */; }; - 6EADE9372192E235006821B3 /* UIImagePickerController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EADE9362192E235006821B3 /* UIImagePickerController+Extension.swift */; }; 6EB8EDC52114358400C2BF44 /* folder-create.tvg in Resources */ = {isa = PBXBuildFile; fileRef = 6EB8EDBE2114358300C2BF44 /* folder-create.tvg */; }; 6ED1B80B21A4004900E16C95 /* CreateFolderAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */; }; 6EE97DCA2204703C0062CCBC /* BackgroundImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 6EE97DC42204703C0062CCBC /* BackgroundImage.png */; }; @@ -148,6 +162,9 @@ DC297965226E4D1100E01BC7 /* PushTransitionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC297964226E4D1100E01BC7 /* PushTransitionDelegate.swift */; }; DC297967226E4D3100E01BC7 /* PushPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC297966226E4D3100E01BC7 /* PushPresentationController.swift */; }; DC297969226E52E600E01BC7 /* PushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC297968226E52E600E01BC7 /* PushTransition.swift */; }; + DC29F09022974AEA00F77349 /* QueryFileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC29F08F22974AEA00F77349 /* QueryFileListTableViewController.swift */; }; + DC29F09422976B9300F77349 /* LibraryFilesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC29F09322976B9200F77349 /* LibraryFilesTableViewController.swift */; }; + DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC29F09222976B9200F77349 /* LibrarySharesTableViewController.swift */; }; DC321261207EB01B00DB171D /* ThemeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC321260207EB01B00DB171D /* ThemeImage.swift */; }; DC3317CE2084966700E36C8F /* ThemeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3317CD2084966700E36C8F /* ThemeTableViewCell.swift */; }; DC3BE0D72077BC5D002A0AC0 /* openssl.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2347445E2076138000859C93 /* openssl.framework */; }; @@ -565,19 +582,35 @@ 23EC775C2137FB6B0032D4E6 /* WebViewDisplayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewDisplayViewController.swift; sourceTree = ""; }; 23F6238020B587EF004FDE8B /* SortMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; 23FA23E520BFD3D8009A6D73 /* SortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBar.swift; sourceTree = ""; }; + 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingTableViewController.swift; sourceTree = ""; }; 39104E0A223991C8002FC02F /* UIButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; + 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListTableViewController.swift; sourceTree = ""; }; + 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryTableViewController.swift; sourceTree = ""; }; + 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchBar+Extension.swift"; sourceTree = ""; }; 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadCrumbTableViewController.swift; sourceTree = ""; }; 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; + 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingSharesTableViewController.swift; sourceTree = ""; }; + 396BE4C92289500E00B254A9 /* RoundedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLabel.swift; sourceTree = ""; }; 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedInfoView.swift; sourceTree = ""; }; 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 3998F5D422411EDF00B66713 /* BorderedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedLabel.swift; sourceTree = ""; }; 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCertificate+Extension.swift"; sourceTree = ""; }; + 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnshareAction.swift; sourceTree = ""; }; + 39A5135222608836002CF1AA /* OCShare+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCShare+Extension.swift"; sourceTree = ""; }; + 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCCore+Extension.swift"; sourceTree = ""; }; + 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingTableViewController.swift; sourceTree = ""; }; + 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSharingEditTableViewController.swift; sourceTree = ""; }; + 39B289A7226F1EE000BE0E11 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 39CC8AE5228C12100020253B /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 39CC8B00228C8A950020253B /* MediaUploadSettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUploadSettingsSection.swift; sourceTree = ""; }; + 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientItemCell.swift; sourceTree = ""; }; 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; 39E2FDEC21FDEC7500F0117F /* ServerListTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListTableHeaderView.swift; sourceTree = ""; }; 39E2FDFF21FF814A00F0117F /* ThemeRoundedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeRoundedButton.swift; sourceTree = ""; }; + 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkTableViewController.swift; sourceTree = ""; }; + 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicLinkEditTableViewController.swift; sourceTree = ""; }; 3D753147564B1E4F47826109 /* Pods-ownCloud Screenshots Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ownCloud Screenshots Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ownCloud Screenshots Tests/Pods-ownCloud Screenshots Tests.debug.xcconfig"; sourceTree = ""; }; 42866B2892DC9EDC65D844E7 /* Pods_ownCloud_Screenshots_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ownCloud_Screenshots_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4C1561E7222321E0009C4EF3 /* PhotoSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoSelectionViewController.swift; sourceTree = ""; }; @@ -662,12 +695,10 @@ 6E586CFD2199A75900F680C4 /* MoveAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveAction.swift; sourceTree = ""; }; 6E586CFF2199A78E00F680C4 /* DeleteAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAction.swift; sourceTree = ""; }; 6E5FC171221590B000F60846 /* DisplayHostViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayHostViewController.swift; sourceTree = ""; }; - 6E83C77D20A32C1B0066EC23 /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; 6E83C78320A33C180066EC23 /* LAContext+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LAContext+Extension.swift"; sourceTree = ""; }; 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyAction.swift; sourceTree = ""; }; 6E93851C220322F30049D676 /* SplashImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SplashImage.png; sourceTree = ""; }; 6EA78B8E2179B55400A5216A /* ImageScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScrollView.swift; sourceTree = ""; }; - 6EADE9362192E235006821B3 /* UIImagePickerController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImagePickerController+Extension.swift"; sourceTree = ""; }; 6EB8EDBE2114358300C2BF44 /* folder-create.tvg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "folder-create.tvg"; path = "img/filetypes-tvg/folder-create.tvg"; sourceTree = SOURCE_ROOT; }; 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFolderAction.swift; sourceTree = ""; }; 6EE97DC42204703C0062CCBC /* BackgroundImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = BackgroundImage.png; sourceTree = ""; }; @@ -703,6 +734,9 @@ DC297964226E4D1100E01BC7 /* PushTransitionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTransitionDelegate.swift; sourceTree = ""; }; DC297966226E4D3100E01BC7 /* PushPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushPresentationController.swift; sourceTree = ""; }; DC297968226E52E600E01BC7 /* PushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushTransition.swift; sourceTree = ""; }; + DC29F08F22974AEA00F77349 /* QueryFileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryFileListTableViewController.swift; sourceTree = ""; }; + DC29F09222976B9200F77349 /* LibrarySharesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LibrarySharesTableViewController.swift; path = ownCloud/Client/Library/LibrarySharesTableViewController.swift; sourceTree = SOURCE_ROOT; }; + DC29F09322976B9200F77349 /* LibraryFilesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LibraryFilesTableViewController.swift; path = ownCloud/Client/Library/LibraryFilesTableViewController.swift; sourceTree = SOURCE_ROOT; }; DC321260207EB01B00DB171D /* ThemeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeImage.swift; sourceTree = ""; }; DC3317CD2084966700E36C8F /* ThemeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTableViewCell.swift; sourceTree = ""; }; DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientQueryViewController.swift; sourceTree = ""; }; @@ -837,7 +871,6 @@ DC18898E218A773700CFB3F9 /* ownCloudMocking.framework in Frameworks */, EA1D571C6B1E95925C459228 /* EarlGrey.framework in Frameworks */, 75AC0B4AD332C8CC785FE349 /* Pods_ownCloudTests.framework in Frameworks */, - 57E3880E6B8E521D27539509 /* Pods_ownCloudTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -926,7 +959,6 @@ 233BDE9D204FEFE500C06732 /* Products */, DC85573220513CC700189B9A /* Frameworks */, 5EE06126BB49344475598790 /* Pods */, - 39D06BE5229BE782000D7FC9 /* Recovered References */, ); sourceTree = ""; }; @@ -950,6 +982,7 @@ children = ( 233BDE9F204FEFE500C06732 /* AppDelegate.swift */, DCF4F1612051925A00189B9A /* Bookmarks */, + DC29F09122974F8000F77349 /* FileLists */, DC3BE0DB2077CC13002A0AC0 /* Client */, DC7DF17C205140F400189B9A /* Server List */, DCF4F1802051A91500189B9A /* Settings */, @@ -1035,12 +1068,13 @@ 6E83C78320A33C180066EC23 /* LAContext+Extension.swift */, DC434D1220D7A8F100740056 /* UIAlertController+OCIssue.swift */, DC248C66213E7DB00067FE94 /* NSLayoutConstraint+Extension.swift */, - 6EADE9362192E235006821B3 /* UIImagePickerController+Extension.swift */, 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */, 4C235CED21F88C0300A989A8 /* UIViewController+Extension.swift */, 4CF8CAB021F9B70500B8CA67 /* UIBarButtonItem+Extension.swift */, 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */, 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */, + 392557FD2278703300E83F60 /* UISearchBar+Extension.swift */, + 39CC8AE5228C12100020253B /* Array+Extension.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -1058,12 +1092,28 @@ path = Viewer; sourceTree = ""; }; - 39D06BE5229BE782000D7FC9 /* Recovered References */ = { + 3907CA6F225F5355001CFBD4 /* Sharing */ = { isa = PBXGroup; children = ( - 6E83C77D20A32C1B0066EC23 /* SettingsSection.swift */, + 39AFC3D0225E72FB00A6D3AE /* GroupSharingTableViewController.swift */, + 39AFC3D7225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift */, + 39E98B3D22797D1B009911F1 /* PublicLinkTableViewController.swift */, + 39E98B442279ACF5009911F1 /* PublicLinkEditTableViewController.swift */, + 390B51DF2292DBB100935E24 /* SharingTableViewController.swift */, + 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */, + 39CC8B36228D5B890020253B /* ShareClientItemCell.swift */, + ); + path = Sharing; + sourceTree = ""; + }; + 3918FE742287DB9B00BACE03 /* Library */ = { + isa = PBXGroup; + children = ( + 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */, + DC29F09222976B9200F77349 /* LibrarySharesTableViewController.swift */, + DC29F09322976B9200F77349 /* LibraryFilesTableViewController.swift */, ); - name = "Recovered References"; + path = Library; sourceTree = ""; }; 59056CAB22414F3C00A18A22 /* ownCloudScreenshotsTests */ = { @@ -1153,6 +1203,7 @@ 6E586CF52199A70100F680C4 /* Actions+Extensions */ = { isa = PBXGroup; children = ( + 399DD7C122A691BC00B45EB2 /* UnshareAction.swift */, 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */, 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */, 6E586CFF2199A78E00F680C4 /* DeleteAction.swift */, @@ -1203,16 +1254,27 @@ path = "Push Presentation Controller"; sourceTree = ""; }; + DC29F09122974F8000F77349 /* FileLists */ = { + isa = PBXGroup; + children = ( + 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */, + DC29F08F22974AEA00F77349 /* QueryFileListTableViewController.swift */, + ); + path = FileLists; + sourceTree = ""; + }; DC3BE0DB2077CC13002A0AC0 /* Client */ = { isa = PBXGroup; children = ( + 3918FE742287DB9B00BACE03 /* Library */, + 3907CA6F225F5355001CFBD4 /* Sharing */, 23EC774D2137F3CD0032D4E6 /* Viewer */, 236735A421217C2300E5834A /* Actions */, DCFED971208095E200A2D984 /* ClientItemCell.swift */, - DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */, DC63208421FCEBE9007EC0A8 /* ClientActivityCell.swift */, DC63208221FCAC1E007EC0A8 /* ClientActivityViewController.swift */, DC3BE0DD2077CC13002A0AC0 /* ClientRootViewController.swift */, + DC3BE0DC2077CC13002A0AC0 /* ClientQueryViewController.swift */, 23F6238020B587EF004FDE8B /* SortMethod.swift */, 23FA23E520BFD3D8009A6D73 /* SortBar.swift */, 4C6B780F2226B83300C5F3DB /* PhotoAlbumTableViewController.swift */, @@ -1511,6 +1573,8 @@ DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */, DC4FEAE6209E3A7700D4476B /* OCIssue+Extension.swift */, 3998F5D62241486F00B66713 /* OCCertificate+Extension.swift */, + 39A5135222608836002CF1AA /* OCShare+Extension.swift */, + 39A513AB22674E56002CF1AA /* OCCore+Extension.swift */, ); path = "SDK Extensions"; sourceTree = ""; @@ -1550,6 +1614,8 @@ 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */, 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */, 3998F5D422411EDF00B66713 /* BorderedLabel.swift */, + 39B289A7226F1EE000BE0E11 /* MessageView.swift */, + 396BE4C92289500E00B254A9 /* RoundedLabel.swift */, ); path = "UI Elements"; sourceTree = ""; @@ -1785,7 +1851,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1010; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = "ownCloud GmbH"; TargetAttributes = { 233BDE9B204FEFE500C06732 = { @@ -2221,6 +2287,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */, 2347446A20761BB700859C93 /* String+Extension.swift in Sources */, DCF4F17920519F8C00189B9A /* StaticTableViewController.swift in Sources */, DC680576212DF548006C3B1F /* CertificateManagementViewController.swift in Sources */, @@ -2241,6 +2308,7 @@ DC63208321FCAC1E007EC0A8 /* ClientActivityViewController.swift in Sources */, 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */, 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */, + 39A513AC22674E56002CF1AA /* OCCore+Extension.swift in Sources */, DC018F8320A0F56300135198 /* UIView+Animation.swift in Sources */, 4CF8CAB121F9B70600B8CA67 /* UIBarButtonItem+Extension.swift in Sources */, DC42244A207CAFAA0006A2A6 /* Theme.swift in Sources */, @@ -2251,6 +2319,7 @@ DC1B2708209CF0D3004715E1 /* IssuesPresentationAnimator.swift in Sources */, DC63208721FCEE5D007EC0A8 /* ProgressView.swift in Sources */, DC3BE0DF2077CC14002A0AC0 /* ClientRootViewController.swift in Sources */, + 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */, DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */, 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */, DC1B2709209CF0D3004715E1 /* CertificateViewController.swift in Sources */, @@ -2260,18 +2329,24 @@ 23D77FCD212BFBD100DE76F1 /* NamingViewController.swift in Sources */, 23FA23E620BFD3D8009A6D73 /* SortBar.swift in Sources */, 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */, + 390B51E02292DBB100935E24 /* SharingTableViewController.swift in Sources */, 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */, 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */, + 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */, 6E83C78420A33C180066EC23 /* LAContext+Extension.swift in Sources */, 593BAB46209AE1BC00023634 /* PasscodeViewController.swift in Sources */, DC6428D02081406800493A01 /* CollapsibleProgressBar.swift in Sources */, DCB44D87218718BA00DAA4CC /* VendorServices.swift in Sources */, + 39E98B3E22797D1B009911F1 /* PublicLinkTableViewController.swift in Sources */, DC63208521FCEBE9007EC0A8 /* ClientActivityCell.swift in Sources */, DC62514A225CEB4300736874 /* UploadMediaAction.swift in Sources */, + 39AFC3D1225E72FB00A6D3AE /* GroupSharingTableViewController.swift in Sources */, DC9BFBBD20A1C37B007064B5 /* PasswordManagerAccess.swift in Sources */, + 39A5135322608836002CF1AA /* OCShare+Extension.swift in Sources */, 23D5241521491C670002C566 /* DisplayViewController.swift in Sources */, DC0B379420514E4700189B9A /* ServerListBookmarkCell.swift in Sources */, DC297965226E4D1100E01BC7 /* PushTransitionDelegate.swift in Sources */, + 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */, 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */, DCF4F18B2052BA4C00189B9A /* Log.swift in Sources */, DC8549382183B4CD00782BA8 /* ThemeStyle+Extensions.swift in Sources */, @@ -2280,7 +2355,6 @@ DC89C45D20860B5D0044BCAE /* ProgressSummarizer.swift in Sources */, 232F7CAF2097260400EE22E4 /* SettingsViewController.swift in Sources */, 4C464BF32187AF1500D30602 /* PDFOutlineViewController.swift in Sources */, - 6EADE9372192E235006821B3 /* UIImagePickerController+Extension.swift in Sources */, DC85572C20513B8C00189B9A /* ServerListTableViewController.swift in Sources */, 6E0A569E218702400056B7B4 /* DownloadFileProgressHUDViewController.swift in Sources */, 233BDEA0204FEFE500C06732 /* AppDelegate.swift in Sources */, @@ -2295,7 +2369,9 @@ 6E5FC172221590B000F60846 /* DisplayHostViewController.swift in Sources */, DC85493421831B0B00782BA8 /* Tools.swift in Sources */, DCFED972208095E200A2D984 /* ClientItemCell.swift in Sources */, + 39E98B452279ACF5009911F1 /* PublicLinkEditTableViewController.swift in Sources */, 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */, + 39CC8B37228D5B890020253B /* ShareClientItemCell.swift in Sources */, 23E22BB720C6A5C40024D11E /* UIDevice+UIUserInterfaceIdiom.swift in Sources */, 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */, 4C235CEE21F88C0300A989A8 /* UIViewController+Extension.swift in Sources */, @@ -2303,6 +2379,7 @@ DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, 23EC77582137F3DD0032D4E6 /* PDFViewerViewController.swift in Sources */, + DC29F09422976B9300F77349 /* LibraryFilesTableViewController.swift in Sources */, 239F1319205A693A0029F186 /* UIColor+Extension.swift in Sources */, 23EC775B2137F3DD0032D4E6 /* OCExtensionType+Extension.swift in Sources */, DC3BE0E12077CD4B002A0AC0 /* Synchronized.swift in Sources */, @@ -2314,6 +2391,7 @@ DC42244C207CAFBB0006A2A6 /* ThemeCollection.swift in Sources */, DC68057A212EAB5E006C3B1F /* ThemeCertificateViewController.swift in Sources */, DCFED9BA20809B8900A2D984 /* ThemeTVGResource.swift in Sources */, + 399DD7C722A691BC00B45EB2 /* UnshareAction.swift in Sources */, 6E37F48B2188B27D00CF16CA /* Action.swift in Sources */, DC3BE0DE2077CC14002A0AC0 /* ClientQueryViewController.swift in Sources */, 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */, @@ -2325,7 +2403,10 @@ DCF4F17B20519F9D00189B9A /* StaticTableViewSection.swift in Sources */, 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */, 23C56537212167BE00BD4B47 /* CardPresentationController.swift in Sources */, + 39B289A8226F1EE000BE0E11 /* MessageView.swift in Sources */, 4C464BF22187AF1500D30602 /* PDFSearchViewController.swift in Sources */, + DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */, + 392557FE2278703300E83F60 /* UISearchBar+Extension.swift in Sources */, DC7DBA2B207F71E400E7337D /* VectorImageView.swift in Sources */, DCE974BC207EACA60069FC2B /* UIImage+Extension.swift in Sources */, DC1B2707209CF0D3004715E1 /* IssuesDismissalAnimator.swift in Sources */, @@ -2337,6 +2418,7 @@ DCF4F17F2051A0D000189B9A /* StaticTableViewRow.swift in Sources */, 4C6B78102226B83300C5F3DB /* PhotoAlbumTableViewController.swift in Sources */, 23EC77592137F3DD0032D4E6 /* DisplayExtension.swift in Sources */, + 39AFC3D8225E79CD00A6D3AE /* GroupSharingEditTableViewController.swift in Sources */, DC297967226E4D3100E01BC7 /* PushPresentationController.swift in Sources */, DC321261207EB01B00DB171D /* ThemeImage.swift in Sources */, DC7DBA54207FA80C00E7337D /* TVGImage.swift in Sources */, @@ -2345,8 +2427,10 @@ DC4FEAE7209E3A7700D4476B /* OCIssue+Extension.swift in Sources */, DC434D1320D7A8F100740056 /* UIAlertController+OCIssue.swift in Sources */, 39E2FDED21FDEC7500F0117F /* ServerListTableHeaderView.swift in Sources */, + DC29F09022974AEA00F77349 /* QueryFileListTableViewController.swift in Sources */, 6E586D002199A78E00F680C4 /* DeleteAction.swift in Sources */, DC3317CE2084966700E36C8F /* ThemeTableViewCell.swift in Sources */, + 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */, DC0196AB20F7690C00C41B78 /* OCBookmark+FileProvider.m in Sources */, 4C6B78122226B86300C5F3DB /* PhotoAlbumTableViewCell.swift in Sources */, 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */, @@ -2913,7 +2997,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 9B5WD74GWJ; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -2946,7 +3030,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 9B5WD74GWJ; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloudScreenshotsTests.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloudScreenshotsTests.xcscheme index 083357066..58edcabb8 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloudScreenshotsTests.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloudScreenshotsTests.xcscheme @@ -1,6 +1,6 @@ Void)? = nil) -> UIViewController { + + class func cardViewController(for item: OCItem, with context: ActionContext, progressHandler: ActionProgressHandler? = nil, completionHandler: ((Action, Error?) -> Void)? = nil) -> UIViewController? { + guard let core = context.core else { return nil } let tableViewController = MoreStaticTableViewController(style: .grouped) - let header = MoreViewHeader(for: item, with: context.core!) - let moreViewController = MoreViewController(item: item, core: context.core!, header: header, viewController: tableViewController) + let header = MoreViewHeader(for: item, with: core) + let moreViewController = MoreViewController(item: item, core: core, header: header, viewController: tableViewController) + + if core.connectionStatus == .online { + if core.connection.capabilities?.sharingAPIEnabled == 1 { + OnMainThread { + if item.isSharedWithUser || item.isShared { + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + progressView.startAnimating() + + let row = StaticTableViewRow(rowWithAction: nil, title: "Searching Shares…".localized, alignment: .left, accessoryView: progressView, identifier: "share-searching") + let placeholderRow = StaticTableViewRow(rowWithAction: nil, title: "", alignment: .left, identifier: "share-empty-searching") + self.updateSharingSection(sectionIdentifier: "share-section", rows: [placeholderRow, row], tableViewController: tableViewController, contentViewController: moreViewController) + + core.unifiedShares(for: item, completionHandler: { (shares) in + OnMainThread { + let shareRows = self.shareRows(shares: shares, item: item, presentingController: moreViewController, context: context) + self.updateSharingSection(sectionIdentifier: "share-section", rows: shareRows, tableViewController: tableViewController, contentViewController: moreViewController) + } + }) + } + } + } else { + if let publicLinkRow = self.shareAsPublicLinkRow(item: item, presentingController: moreViewController, context: context) { + tableViewController.insertSection(StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "share-section", rows: [publicLinkRow]), at: 0, animated: false) + } + } + } let title = NSAttributedString(string: "Actions".localized, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .heavy)]) let actions = Action.sortedApplicableActions(for: context) actions.forEach({ - $0.actionWillRunHandler = { - moreViewController.dismiss(animated: true) + $0.actionWillRunHandler = { [weak moreViewController] in + moreViewController?.dismiss(animated: true) } $0.progressHandler = progressHandler @@ -243,11 +271,13 @@ class Action : NSObject { } // MARK: - Action UI elements + private static let staticRowImageWidth : CGFloat = 32 + func provideStaticRow() -> StaticTableViewRow? { return StaticTableViewRow(buttonWithAction: { (_ row, _ sender) in self.willRun() self.run() - }, title: actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .plain, identifier: actionExtension.identifier.rawValue) + }, title: actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .plain, image: self.icon, imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: actionExtension.identifier.rawValue) } func provideContextualAction() -> UIContextualAction? { @@ -259,10 +289,20 @@ class Action : NSObject { } func provideAlertAction() -> UIAlertAction? { - return UIAlertAction(title: self.actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .default, handler: { (_ alertAction) in + let alertAction = UIAlertAction(title: self.actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .default, handler: { (_ alertAction) in self.willRun() self.run() }) + + let image = self.icon + if alertAction.responds(to: NSSelectorFromString("setImage:")) { + alertAction.setValue(image, forKey: "image") + } + if alertAction.responds(to: NSSelectorFromString("_setTitleTextAlignment:")) { + alertAction.setValue(CATextLayerAlignmentMode.left, forKey: "titleTextAlignment") + } + + return alertAction } // MARK: - Action metadata @@ -272,7 +312,7 @@ class Action : NSObject { var icon : UIImage? { if let locationIdentifier = context.location?.identifier { - return Action.iconForLocation(locationIdentifier) + return type(of: self).iconForLocation(locationIdentifier) } return nil @@ -281,4 +321,135 @@ class Action : NSObject { var position : ActionPosition { return type(of: self).applicablePosition(forContext: context) } + +} + +// MARK: - Sharing + +private extension Action { + + class func shareRows(shares: [OCShare], item: OCItem, presentingController: UIViewController, context: ActionContext) -> [StaticTableViewRow] { + var shareRows: [StaticTableViewRow] = [] + + var userTitle = "" + var linkTitle = "" + var hasUserGroupSharing = false + var hasLinkSharing = false + + if item.isSharedWithUser { + // find shares by others + if let itemOwner = item.owner, itemOwner.isRemote, let ownerName = itemOwner.displayName ?? itemOwner.userName { + // - remote shares + userTitle = String(format: "Shared by %@".localized, ownerName) + hasUserGroupSharing = true + } else { + // - local shares + for share in shares { + if let ownerName = share.itemOwner?.displayName { + userTitle = String(format: "Shared by %@".localized, ownerName) + hasUserGroupSharing = true + break + } + } + } + } else { + // find Shares by me + let privateShares = shares.filter { (share) -> Bool in + return share.type != .link + } + + if privateShares.count > 0 { + let title = ((privateShares.count > 1) ? "Recipients" : "Recipient").localized + + userTitle = "\(privateShares.count) \(title)" + hasUserGroupSharing = true + } + } + + // find Public link shares + let linkShares = shares.filter { (share) -> Bool in + return share.type == .link + } + if linkShares.count > 0 { + let title = ((linkShares.count > 1) ? "Links" : "Link").localized + + linkTitle.append("\(linkShares.count) \(title)") + hasLinkSharing = true + } + + if hasUserGroupSharing { + let addGroupRow = StaticTableViewRow(rowWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController, let core = context.core { + let sharingViewController = GroupSharingTableViewController(core: core, item: item) + sharingViewController.shares = shares + + self.dismiss(presentingController: presentingController, andPresent: sharingViewController, on: context.viewController) + } + }, title: userTitle, subtitle: nil, image: UIImage(named: "group"), imageWidth: Action.staticRowImageWidth, alignment: .left, accessoryType: .disclosureIndicator) + shareRows.append(addGroupRow) + } else if item.isShareable { + shareRows.append(self.shareAsGroupRow(item: item, presentingController: presentingController, context: context)) + } + + if hasLinkSharing, let core = context.core, core.connection.capabilities?.publicSharingEnabled == true { + let addGroupRow = StaticTableViewRow(rowWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController { + let sharingViewController = PublicLinkTableViewController(core: core, item: item) + sharingViewController.shares = shares + + self.dismiss(presentingController: presentingController, andPresent: sharingViewController, on: context.viewController) + } + }, title: linkTitle, subtitle: nil, image: UIImage(named: "link"), imageWidth: Action.staticRowImageWidth, alignment: .left, accessoryType: .disclosureIndicator) + shareRows.append(addGroupRow) + } else if let publicLinkRow = self.shareAsPublicLinkRow(item: item, presentingController: presentingController, context: context) { + shareRows.append(publicLinkRow) + } + + return shareRows + } + + private class func updateSharingSection(sectionIdentifier: String, rows: [StaticTableViewRow], tableViewController: MoreStaticTableViewController, contentViewController: MoreViewController) { + if let section = tableViewController.sectionForIdentifier(sectionIdentifier) { + tableViewController.removeSection(section) + } + if rows.count > 0 { + tableViewController.insertSection(MoreStaticTableViewSection(identifier: "share-section", rows: rows), at: 0, animated: false) + } + } + + private class func shareAsGroupRow(item : OCItem, presentingController: UIViewController, context: ActionContext) -> StaticTableViewRow { + let title = ((item.type == .collection) ? "Share this folder" : "Share this file").localized + + let addGroupRow = StaticTableViewRow(buttonWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController, let core = context.core { + self.dismiss(presentingController: presentingController, + andPresent: GroupSharingTableViewController(core: core, item: item), + on: context.viewController) + } + }, title: title, style: .plain, image: UIImage(named: "group"), imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: "share-add-group") + + return addGroupRow + } + + private class func shareAsPublicLinkRow(item : OCItem, presentingController: UIViewController, context: ActionContext) -> StaticTableViewRow? { + let addGroupRow = StaticTableViewRow(buttonWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController, let core = context.core { + self.dismiss(presentingController: presentingController, + andPresent: PublicLinkTableViewController(core: core, item: item), + on: context.viewController) + } + }, title: "Links".localized, style: .plain, image: UIImage(named: "link"), imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: "share-add-group") + + return addGroupRow + } + + private class func dismiss(presentingController: UIViewController, andPresent viewController: UIViewController, on hostViewController: UIViewController?) { + presentingController.dismiss(animated: true) + + guard let hostViewController = hostViewController else { return } + + let navigationController = ThemeNavigationController(rootViewController: viewController) + + hostViewController.present(navigationController, animated: true, completion: nil) + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift index 1c4e429e4..ddbb2f8d2 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift @@ -25,17 +25,6 @@ class CopyAction : Action { override class var name : String? { return "Copy".localized } override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } - // MARK: - Extension matching - override class func applicablePosition(forContext: ActionContext) -> ActionPosition { - if forContext.items.contains(where: {$0.type == .file}), - let path = forContext.query?.queryPath, path.isRootPath, - let containsFolder = forContext.preferences?["containsFolders"] as? Bool, !containsFolder { - return .none - } - // Examine items in context - return .middle - } - // MARK: - Action implementation override func run() { guard context.items.count > 0, let viewController = context.viewController, let core = self.core else { @@ -45,8 +34,7 @@ class CopyAction : Action { let items = context.items - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, path: "/", selectButtonTitle: "Copy here", completion: { (selectedDirectory) in - + let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, path: "/", selectButtonTitle: "Copy here".localized, avoidConflictsWith: items, choiceHandler: { (selectedDirectory) in if let targetDirectory = selectedDirectory { items.forEach({ (item) in @@ -68,4 +56,12 @@ class CopyAction : Action { let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) viewController.present(pickerNavigationController, animated: true) } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "copy-file") + } + + return nil + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift index d2081d005..9cb6b846a 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CreateFolderAction.swift @@ -88,7 +88,7 @@ class CreateFolderAction : Action { } override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { - if location == .toolbar { + if location == .toolbar || location == .plusButton { return Theme.shared.image(for: "folder-create", size: CGSize(width: 30.0, height: 30.0))!.withRenderingMode(.alwaysTemplate) } diff --git a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift index cb3de4dae..ddba13769 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift @@ -26,7 +26,16 @@ class DeleteAction : Action { // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { - // Examine items in context + let sharedWithUser = forContext.items.sharedWithUser + + if let core = forContext.core { + for sharedItem in sharedWithUser { + if sharedItem.isShareRootItem(from: core) { + return .none + } + } + } + return .last } @@ -84,4 +93,12 @@ class DeleteAction : Action { viewController.present(alertController, animated: true) } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "trash") + } + + return nil + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift index 4239eb771..6b5aea3d7 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DuplicateAction.swift @@ -25,7 +25,6 @@ class DuplicateAction : Action { override class var category : ActionCategory? { return .normal } override class var name : String? { return "Duplicate".localized } override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } - var remainingItems : [OCItem] = [] // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { @@ -106,4 +105,12 @@ class DuplicateAction : Action { self.completed() } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "duplicate-file") + } + + return nil + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift index c0b6066d6..4831b9c9c 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift @@ -24,17 +24,6 @@ class MoveAction : Action { override class var name : String? { return "Move".localized } override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreFolder, .toolbar] } - // MARK: - Extension matching - override class func applicablePosition(forContext: ActionContext) -> ActionPosition { - if forContext.items.contains(where: {$0.type == .file}), - let path = forContext.query?.queryPath, path.isRootPath, - let containsFolder = forContext.preferences?["containsFolders"] as? Bool, !containsFolder { - return .none - } - // Examine items in context - return .middle - } - // MARK: - Action implementation override func run() { guard context.items.count > 0, let viewController = context.viewController, let core = self.core else { @@ -44,7 +33,7 @@ class MoveAction : Action { let items = context.items - let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, path: "/", completion: { (selectedDirectory) in + let directoryPickerViewController = ClientDirectoryPickerViewController(core: core, path: "/", selectButtonTitle: "Move here".localized, avoidConflictsWith: items, choiceHandler: { (selectedDirectory) in guard let selectedDirectory = selectedDirectory else { self.completed(with: NSError(ocError: OCError.cancelled)) return @@ -57,7 +46,7 @@ class MoveAction : Action { if let progress = self.core?.move(item, to: selectedDirectory, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in if error != nil { - Log.log("Error \(String(describing: error)) moving \(String(describing: itemName))") + Log.error("Error \(String(describing: error)) moving \(String(describing: itemName))") } }) { self.publish(progress: progress) @@ -70,4 +59,12 @@ class MoveAction : Action { let pickerNavigationController = ThemeNavigationController(rootViewController: directoryPickerViewController) viewController.present(pickerNavigationController, animated: true) } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "folder") + } + + return nil + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift index ea510bebc..41c643fe0 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift @@ -133,6 +133,14 @@ class OpenInAction: Action { context.viewController?.present(activityController, animated: true, completion: nil) } } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "open-in") + } + + return nil + } } extension OpenInAction: UIDocumentInteractionControllerDelegate { diff --git a/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift b/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift index 61bcce3a4..76cb6128e 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/RenameAction.swift @@ -77,4 +77,12 @@ class RenameAction : Action { viewController.present(navigationController, animated: true) } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "folder") + } + + return nil + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift new file mode 100644 index 000000000..ccd68c6ac --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/UnshareAction.swift @@ -0,0 +1,138 @@ +// +// UnshareAction.swift +// ownCloud +// +// Created by Matthias Hühne on 04/04/2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 ownCloudSDK + +extension Array where Element: OCItem { + var sharedWithUser : [OCItem] { + return self.filter({ (item) -> Bool in return item.isSharedWithUser }) + } +} + +class UnshareAction : Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.unshare") } + override class var category : ActionCategory? { return .destructive } + override class var name : String? { return "Unshare".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .tableRow, .moreFolder, .toolbar] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + let sharedWithUser = forContext.items.sharedWithUser + + if forContext.items.count != sharedWithUser.count { + return .none + } + + if let core = forContext.core { + for sharedItem in sharedWithUser { + if !sharedItem.isShareRootItem(from: core) { + return .none + } + } + } + + return .last + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let viewController = context.viewController else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + let items = context.items + + let message: String + if items.count > 1 { + message = "Are you sure you want to unshare these items?".localized + } else { + message = "Are you sure you want to unshare this item?".localized + } + + let itemDescripton: String? + if items.count > 1 { + itemDescripton = "Multiple items".localized + } else { + itemDescripton = items.first?.name + } + + guard let name = itemDescripton else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + let unshareItemAndPublishProgress = { (items: [OCItem]) in + for item in items { + if let owner = item.owner { + if !owner.isRemote { + _ = self.core?.sharesSharedWithMe(for: item, initialPopulationHandler: { (shares) in + let userGroupShares = shares.filter { (share) -> Bool in + return share.type != .link + } + if let share = userGroupShares.first, let progress = self.core?.makeDecision(on: share, accept: false, completionHandler: { (error) in + if error != nil { + Log.log("Error \(String(describing: error)) unshare \(String(describing: item.path))") + } + }) { + self.publish(progress: progress) + } + + }, keepRunning: false) + } else { + _ = self.core?.acceptedCloudShares(for: item, initialPopulationHandler: { (shares) in + let userGroupShares = shares.filter { (share) -> Bool in + return share.type != .link + } + if let share = userGroupShares.first, let progress = self.core?.makeDecision(on: share, accept: false, completionHandler: { (error) in + if error != nil { + Log.log("Error \(String(describing: error)) unshare \(String(describing: item.path))") + } + }) { + self.publish(progress: progress) + } + + }, keepRunning: false) + } + } + } + + self.completed() + } + + let alertController = UIAlertController( + with: name, + message: message, + destructiveLabel: "Unshare".localized, + preferredStyle: UIDevice.current.isIpad() ? UIAlertController.Style.alert : UIAlertController.Style.actionSheet, + destructiveAction: { + unshareItemAndPublishProgress(items) + }) + + viewController.present(alertController, animated: true) + + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem { + return UIImage(named: "trash") + } + + return nil + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift index 7e288c20e..0d041f236 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadFileAction.swift @@ -47,6 +47,15 @@ class UploadFileAction: UploadBaseAction { viewController.present(documentPickerViewController, animated: true) } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .plusButton { + Theme.shared.add(tvgResourceFor: "text") + return Theme.shared.image(for: "text", size: CGSize(width: 30.0, height: 30.0))!.withRenderingMode(.alwaysTemplate) + } + + return nil + } } // MARK: - UIDocumentPickerDelegate diff --git a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift index f9ff2d854..03f52cf02 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/UploadMediaAction.swift @@ -289,4 +289,13 @@ class UploadMediaAction: UploadBaseAction { } } } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .plusButton { + Theme.shared.add(tvgResourceFor: "image") + return Theme.shared.image(for: "image", size: CGSize(width: 30.0, height: 30.0))!.withRenderingMode(.alwaysTemplate) + } + + return nil + } } diff --git a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift index dd5525984..007477084 100644 --- a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift +++ b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift @@ -19,21 +19,52 @@ import UIKit import ownCloudSDK +typealias ClientDirectoryPickerPathFilter = (_ path: String) -> Bool +typealias ClientDirectoryPickerChoiceHandler = (_ chosenItem: OCItem?) -> Void + class ClientDirectoryPickerViewController: ClientQueryViewController { private let SELECT_BUTTON_HEIGHT: CGFloat = 44.0 // MARK: - Instance Properties - private var selectButton: UIBarButtonItem! - private var selectButtonTitle: String - private var cancelBarButton: UIBarButtonItem! - private var completion: (OCItem?) -> Void + private var selectButton: UIBarButtonItem? + private var selectButtonTitle: String? + private var cancelBarButton: UIBarButtonItem? + + var directoryPath : String? + + var choiceHandler: ClientDirectoryPickerChoiceHandler? + var allowedPathFilter : ClientDirectoryPickerPathFilter? + var navigationPathFilter : ClientDirectoryPickerPathFilter? // MARK: - Init & deinit - init(core inCore: OCCore, path: String, selectButtonTitle: String = "Move here".localized, completion: @escaping (OCItem?) -> Void) { - self.selectButtonTitle = selectButtonTitle - self.completion = completion + convenience init(core inCore: OCCore, path: String, selectButtonTitle: String, avoidConflictsWith items: [OCItem], choiceHandler: @escaping ClientDirectoryPickerChoiceHandler) { + let folderItemPaths = items.filter({ (item) -> Bool in + return item.type == .collection && item.path != nil && !item.isRoot + }).map { (item) -> String in + return item.path! + } + let itemParentPaths = items.filter({ (item) -> Bool in + return item.path?.parentPath != nil + }).map { (item) -> String in + return item.path!.parentPath + } + + var navigationPathFilter : ClientDirectoryPickerPathFilter? + + if folderItemPaths.count > 0 { + navigationPathFilter = { (targetPath) in + return !folderItemPaths.contains(targetPath) + } + } + + self.init(core: inCore, path: path, selectButtonTitle: selectButtonTitle, allowedPathFilter: { (targetPath) in + // Disallow all paths as target that are parent of any of the items + return !itemParentPaths.contains(targetPath) + }, navigationPathFilter: navigationPathFilter, choiceHandler: choiceHandler) + } + init(core inCore: OCCore, path: String, selectButtonTitle: String, allowedPathFilter: ClientDirectoryPickerPathFilter? = nil, navigationPathFilter: ClientDirectoryPickerPathFilter? = nil, choiceHandler: @escaping ClientDirectoryPickerChoiceHandler) { let targetDirectoryQuery = OCQuery(forPath: path) // Sort folders first @@ -53,8 +84,19 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { super.init(core: inCore, query: targetDirectoryQuery) + self.directoryPath = path + + self.choiceHandler = choiceHandler + + self.selectButtonTitle = selectButtonTitle + self.allowedPathFilter = allowedPathFilter + self.navigationPathFilter = navigationPathFilter + // Force disable sorting options self.shallShowSortBar = false + + // Disable pull to refresh + allowPullToRefresh = false } required init?(coder aDecoder: NSCoder) { @@ -65,13 +107,16 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { override func viewDidLoad() { super.viewDidLoad() - // Remove pull to refresh - queryRefreshControl?.removeFromSuperview() + // Adapt to disabled pull-to-refresh self.tableView.alwaysBounceVertical = false // Select button creation selectButton = UIBarButtonItem(title: selectButtonTitle, style: .plain, target: self, action: #selector(selectButtonPressed)) - selectButton.title = selectButtonTitle + selectButton?.title = selectButtonTitle + + if let allowedPathFilter = allowedPathFilter, let directoryPath = directoryPath { + selectButton?.isEnabled = allowedPathFilter(directoryPath) + } // Cancel button creation cancelBarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelBarButtonPressed)) @@ -79,9 +124,12 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(true) - navigationItem.rightBarButtonItems = [cancelBarButton] - if let navController = self.navigationController { + if let cancelBarButton = cancelBarButton { + navigationItem.rightBarButtonItems = [cancelBarButton] + } + + if let navController = self.navigationController, let selectButton = selectButton { navController.isToolbarHidden = false navController.toolbar.isTranslucent = false let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) @@ -89,34 +137,51 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { } } + private func allowNavigationFor(item: OCItem?) -> Bool { + guard let item = item else { return false } + + var allowNavigation = item.type == .collection + + if allowNavigation, let navigationPathFilter = navigationPathFilter, let itemPath = item.path { + allowNavigation = navigationPathFilter(itemPath) + } + + return allowNavigation + } + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = super.tableView(tableView, cellForRowAt: indexPath) if let clientItemCell = cell as? ClientItemCell { clientItemCell.isMoreButtonPermanentlyHidden = true - clientItemCell.isActive = (clientItemCell.item?.type == OCItemType.collection) ? true : false + clientItemCell.isActive = self.allowNavigationFor(item: clientItemCell.item) } return cell } + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if let item : OCItem = itemAt(indexPath: indexPath), allowNavigationFor(item: item) { + return true + } + + return false + } + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - let item: OCItem = itemAtIndexPath(indexPath) - if item.type != OCItemType.collection { - return nil - } else { + if let item : OCItem = itemAt(indexPath: indexPath), allowNavigationFor(item: item) { return indexPath } + + return nil } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item: OCItem = itemAtIndexPath(indexPath) - - guard item.type == OCItemType.collection, let core = self.core, let path = item.path else { + guard let item : OCItem = itemAt(indexPath: indexPath), item.type == OCItemType.collection, let core = self.core, let path = item.path, let selectButtonTitle = selectButtonTitle, let choiceHandler = choiceHandler else { return } - self.navigationController?.pushViewController(ClientDirectoryPickerViewController(core: core, path: path, selectButtonTitle: selectButtonTitle, completion: completion), animated: true) + self.navigationController?.pushViewController(ClientDirectoryPickerViewController(core: core, path: path, selectButtonTitle: selectButtonTitle, allowedPathFilter: allowedPathFilter, navigationPathFilter: navigationPathFilter, choiceHandler: choiceHandler), animated: true) } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { @@ -128,15 +193,19 @@ class ClientDirectoryPickerViewController: ClientQueryViewController { } // MARK: - Actions + func userChose(item: OCItem?) { + self.choiceHandler?(item) + } + @objc private func cancelBarButtonPressed() { - self.dismiss(animated: true, completion: { - self.completion(nil) + dismiss(animated: true, completion: { + self.userChose(item: nil) }) } @objc private func selectButtonPressed() { - self.dismiss(animated: true, completion: { - self.completion(self.query.rootItem) + dismiss(animated: true, completion: { + self.userChose(item: self.query.rootItem) }) } } diff --git a/ownCloud/Client/Actions/MoreStaticTableViewController.swift b/ownCloud/Client/Actions/MoreStaticTableViewController.swift index 8dd78ea66..23f5c4b2c 100644 --- a/ownCloud/Client/Actions/MoreStaticTableViewController.swift +++ b/ownCloud/Client/Actions/MoreStaticTableViewController.swift @@ -64,6 +64,21 @@ class MoreStaticTableViewController: StaticTableViewController { return nil } + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + if (sections[section] as? MoreStaticTableViewSection)?.headerAttributedTitle != nil { + return UITableView.automaticDimension + } + return 0.0 + } + + override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + return CGFloat.leastNormalMagnitude + } + + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return nil + } + override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { super.applyThemeCollection(theme: theme, collection: collection, event: event) self.tableView.separatorColor = self.tableView.backgroundColor @@ -74,7 +89,7 @@ class MoreStaticTableViewSection : StaticTableViewSection { public var headerAttributedTitle : NSAttributedString? public var footerAttributedTitle : NSAttributedString? - convenience init(headerAttributedTitle theHeaderTitle: NSAttributedString, footerAttributedTitle theFooterTitle: NSAttributedString? = nil, identifier : String? = nil, rows rowsToAdd: [StaticTableViewRow] = Array()) { + convenience init(headerAttributedTitle theHeaderTitle: NSAttributedString? = nil, footerAttributedTitle theFooterTitle: NSAttributedString? = nil, identifier : String? = nil, rows rowsToAdd: [StaticTableViewRow] = Array()) { self.init() self.headerAttributedTitle = theHeaderTitle diff --git a/ownCloud/Client/Actions/MoreViewController.swift b/ownCloud/Client/Actions/MoreViewController.swift index d250d3d2e..c9e033daa 100644 --- a/ownCloud/Client/Actions/MoreViewController.swift +++ b/ownCloud/Client/Actions/MoreViewController.swift @@ -109,6 +109,10 @@ class MoreViewController: UIViewController { return size } + + override func viewDidLayoutSubviews() { + self.preferredContentSize = moreLayoutSizeFitting(CGSize(width: UIView.layoutFittingExpandedSize.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultHigh) + } } extension MoreViewController: Themeable { diff --git a/ownCloud/Client/Actions/MoreViewHeader.swift b/ownCloud/Client/Actions/MoreViewHeader.swift index 41e088050..446f9dc11 100644 --- a/ownCloud/Client/Actions/MoreViewHeader.swift +++ b/ownCloud/Client/Actions/MoreViewHeader.swift @@ -24,20 +24,26 @@ class MoreViewHeader: UIView { private var labelContainerView : UIView private var titleLabel: UILabel private var detailLabel: UILabel + private var favoriteButton: UIButton var thumbnailSize = CGSize(width: 60, height: 60) + let favoriteSize = CGSize(width: 24, height: 24) + + var showFavoriteButton: Bool var item: OCItem weak var core: OCCore? - init(for item: OCItem, with core: OCCore) { + init(for item: OCItem, with core: OCCore, favorite: Bool = true) { self.item = item self.core = core + self.showFavoriteButton = favorite iconView = UIImageView() titleLabel = UILabel() detailLabel = UILabel() labelContainerView = UIView() + favoriteButton = UIButton() super.init(frame: .zero) @@ -57,6 +63,7 @@ class MoreViewHeader: UIView { detailLabel.translatesAutoresizingMaskIntoConstraints = false iconView.translatesAutoresizingMaskIntoConstraints = false labelContainerView.translatesAutoresizingMaskIntoConstraints = false + favoriteButton.translatesAutoresizingMaskIntoConstraints = false iconView.contentMode = .scaleAspectFit titleLabel.font = UIFont.systemFont(ofSize: 17, weight: UIFont.Weight.semibold) @@ -94,17 +101,38 @@ class MoreViewHeader: UIView { iconView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -20).with(priority: .defaultHigh), labelContainerView.leftAnchor.constraint(equalTo: iconView.rightAnchor, constant: 15), - labelContainerView.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -20), labelContainerView.centerYAnchor.constraint(equalTo: self.centerYAnchor), - labelContainerView.topAnchor.constraint(greaterThanOrEqualTo: self.safeAreaLayoutGuide.topAnchor, constant: 20), - labelContainerView.bottomAnchor.constraint(lessThanOrEqualTo: self.safeAreaLayoutGuide.bottomAnchor, constant: -20) + labelContainerView.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor, constant: 20), + labelContainerView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -20).with(priority: .defaultHigh) ]) + if showFavoriteButton { + updateFavoriteButtonImage() + favoriteButton.addTarget(self, action: #selector(toogleFavoriteState), for: UIControl.Event.touchUpInside) + self.addSubview(favoriteButton) + + NSLayoutConstraint.activate([ + favoriteButton.widthAnchor.constraint(equalToConstant: favoriteSize.width), + favoriteButton.heightAnchor.constraint(equalToConstant: favoriteSize.height), + favoriteButton.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -15), + favoriteButton.centerYAnchor.constraint(equalTo: self.centerYAnchor), + favoriteButton.leftAnchor.constraint(equalTo: labelContainerView.rightAnchor, constant: 10) + ]) + } else { + NSLayoutConstraint.activate([ + labelContainerView.rightAnchor.constraint(equalTo: self.safeAreaLayoutGuide.rightAnchor, constant: -20) + ]) + } + titleLabel.attributedText = NSAttributedString(string: item.name ?? "", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) let byteCountFormatter = ByteCountFormatter() byteCountFormatter.countStyle = .file - let size = byteCountFormatter.string(fromByteCount: Int64(item.size)) + var size = byteCountFormatter.string(fromByteCount: Int64(item.size)) + + if item.size < 0 { + size = "Pending".localized + } let dateString = item.lastModifiedLocalized @@ -137,6 +165,32 @@ class MoreViewHeader: UIView { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + @objc func toogleFavoriteState() { + if item.isFavorite == true { + item.isFavorite = false + } else { + item.isFavorite = true + } + self.updateFavoriteButtonImage() + core?.update(item, properties: [OCItemPropertyName.isFavorite], options: nil, resultHandler: { (error, _, _, _) in + if error == nil { + OnMainThread { + self.updateFavoriteButtonImage() + } + } + }) + } + + func updateFavoriteButtonImage() { + if item.isFavorite == true { + favoriteButton.setImage(UIImage(named: "star"), for: .normal) + favoriteButton.tintColor = Theme.shared.activeCollection.favoriteEnabledColor + } else { + favoriteButton.setImage(UIImage(named: "unstar"), for: .normal) + favoriteButton.tintColor = Theme.shared.activeCollection.favoriteDisabledColor + } + } } extension MoreViewHeader: Themeable { diff --git a/ownCloud/Client/BreadCrumbTableViewController.swift b/ownCloud/Client/BreadCrumbTableViewController.swift index 784547583..e0ae2b7b9 100644 --- a/ownCloud/Client/BreadCrumbTableViewController.swift +++ b/ownCloud/Client/BreadCrumbTableViewController.swift @@ -53,13 +53,19 @@ class BreadCrumbTableViewController: StaticTableViewController { self.preferredContentSize = CGSize(width: contentWidth, height: contentHeight) for (_, currentPath) in pathComp.enumerated().reversed() { - let stackIndex = stackViewControllers.count - currentViewContollerIndex + var stackIndex = stackViewControllers.count - currentViewContollerIndex + if stackIndex < 0 { + stackIndex = 0 + } var pathTitle = currentPath if currentPath.isRootPath, let shortName = self.bookmarkShortName { pathTitle = shortName } - let aRow = StaticTableViewRow(rowWithAction: { (_, _) in - self.parentNavigationController?.popToViewController((stackViewControllers[stackIndex]), animated: true) + let aRow = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + guard let self = self else { return } + if stackViewControllers.indices.contains(stackIndex) { + self.parentNavigationController?.popToViewController((stackViewControllers[stackIndex]), animated: true) + } self.dismiss(animated: false, completion: nil) }, title: pathTitle, image: Theme.shared.image(for: "folder", size: CGSize(width: imageWidth, height: imageHeight))) diff --git a/ownCloud/Client/ClientFilelistTableViewController.swift b/ownCloud/Client/ClientFilelistTableViewController.swift new file mode 100644 index 000000000..49663e68a --- /dev/null +++ b/ownCloud/Client/ClientFilelistTableViewController.swift @@ -0,0 +1,41 @@ +// +// FilelistTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 21.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +import UIKit +import ownCloudSDK + +class FilelistTableViewController: UITableViewController { + + var progressSummarizer : ProgressSummarizer? + private var _actionProgressHandler : ActionProgressHandler? + + func makeActionProgressHandler() -> ActionProgressHandler { + if _actionProgressHandler == nil { + _actionProgressHandler = { [weak self] (progress, publish) in + if publish { + self?.progressSummarizer?.startTracking(progress: progress) + } else { + self?.progressSummarizer?.stopTracking(progress: progress) + } + } + } + + return _actionProgressHandler! + } +} + +extension FilelistTableViewController : SortBarDelegate { + func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { + sortMethod = didUpdateSortMethod + query.sortComparator = sortMethod.comparator() + } + + func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { + self.present(presentViewController, animated: animated, completion: completionHandler) + } +} diff --git a/ownCloud/Client/ClientItemCell.swift b/ownCloud/Client/ClientItemCell.swift index 7a207a7c9..c90cf84ff 100644 --- a/ownCloud/Client/ClientItemCell.swift +++ b/ownCloud/Client/ClientItemCell.swift @@ -7,14 +7,14 @@ // /* - * Copyright (C) 2018, 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 . - * - */ +* Copyright (C) 2018, 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 @@ -41,10 +41,14 @@ class ClientItemCell: ThemeTableViewCell { var detailLabel : UILabel = UILabel() var iconView : UIImageView = UIImageView() var cloudStatusIconView : UIImageView = UIImageView() + var sharedStatusIconView : UIImageView = UIImageView() + var publicLinkStatusIconView : UIImageView = UIImageView() var moreButton : UIButton = UIButton() var progressView : ProgressView? var moreButtonWidthConstraint : NSLayoutConstraint? + var sharedStatusIconViewRightMarginConstraint : NSLayoutConstraint? + var publicLinkStatusIconViewRightMarginConstraint : NSLayoutConstraint? var activeThumbnailRequestProgress : Progress? @@ -102,6 +106,12 @@ class ClientItemCell: ThemeTableViewCell { cloudStatusIconView.translatesAutoresizingMaskIntoConstraints = false cloudStatusIconView.contentMode = .center + sharedStatusIconView.translatesAutoresizingMaskIntoConstraints = false + sharedStatusIconView.contentMode = .center + + publicLinkStatusIconView.translatesAutoresizingMaskIntoConstraints = false + publicLinkStatusIconView.contentMode = .center + titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) titleLabel.adjustsFontForContentSizeCategory = true @@ -111,6 +121,8 @@ class ClientItemCell: ThemeTableViewCell { self.contentView.addSubview(titleLabel) self.contentView.addSubview(detailLabel) self.contentView.addSubview(iconView) + self.contentView.addSubview(sharedStatusIconView) + self.contentView.addSubview(publicLinkStatusIconView) self.contentView.addSubview(cloudStatusIconView) self.contentView.addSubview(moreButton) @@ -119,6 +131,16 @@ class ClientItemCell: ThemeTableViewCell { moreButton.addTarget(self, action: #selector(moreButtonTapped), for: .touchUpInside) + sharedStatusIconView.setContentHuggingPriority(.required, for: .vertical) + sharedStatusIconView.setContentHuggingPriority(.required, for: .horizontal) + sharedStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) + sharedStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) + + publicLinkStatusIconView.setContentHuggingPriority(.required, for: .vertical) + publicLinkStatusIconView.setContentHuggingPriority(.required, for: .horizontal) + publicLinkStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) + publicLinkStatusIconView.setContentCompressionResistancePriority(.required, for: .horizontal) + cloudStatusIconView.setContentHuggingPriority(.required, for: .vertical) cloudStatusIconView.setContentHuggingPriority(.required, for: .horizontal) cloudStatusIconView.setContentCompressionResistancePriority(.required, for: .vertical) @@ -130,6 +152,8 @@ class ClientItemCell: ThemeTableViewCell { detailLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) moreButtonWidthConstraint = moreButton.widthAnchor.constraint(equalToConstant: moreButtonWidth) + sharedStatusIconViewRightMarginConstraint = sharedStatusIconView.rightAnchor.constraint(equalTo: publicLinkStatusIconView.leftAnchor, constant: 0) + publicLinkStatusIconViewRightMarginConstraint = publicLinkStatusIconView.rightAnchor.constraint(equalTo: cloudStatusIconView.leftAnchor, constant: 0) NSLayoutConstraint.activate([ iconView.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: horizontalMargin), @@ -139,7 +163,9 @@ class ClientItemCell: ThemeTableViewCell { iconView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalIconMargin), iconView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -verticalIconMargin), - titleLabel.rightAnchor.constraint(equalTo: cloudStatusIconView.leftAnchor, constant: -horizontalSmallMargin), + titleLabel.rightAnchor.constraint(equalTo: sharedStatusIconView.leftAnchor, constant: -horizontalSmallMargin), + sharedStatusIconViewRightMarginConstraint!, + publicLinkStatusIconViewRightMarginConstraint!, detailLabel.rightAnchor.constraint(equalTo: moreButton.leftAnchor, constant: -horizontalMargin), titleLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: verticalLabelMargin), @@ -153,9 +179,13 @@ class ClientItemCell: ThemeTableViewCell { moreButtonWidthConstraint!, moreButton.rightAnchor.constraint(equalTo: self.contentView.rightAnchor), + sharedStatusIconView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + + publicLinkStatusIconView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + cloudStatusIconView.rightAnchor.constraint(lessThanOrEqualTo: moreButton.leftAnchor, constant: -horizontalSmallMargin), cloudStatusIconView.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor) - ]) + ]) } // MARK: - Present item @@ -195,8 +225,8 @@ class ClientItemCell: ThemeTableViewCell { let displayThumbnail = { (thumbnail: OCItemThumbnail?) in _ = thumbnail?.requestImage(for: thumbnailSize, scale: 0, withCompletionHandler: { (thumbnail, error, _, image) in if error == nil, - image != nil, - self.item?.itemVersionIdentifier == thumbnail?.itemVersionIdentifier { + image != nil, + self.item?.itemVersionIdentifier == thumbnail?.itemVersionIdentifier { OnMainThread { self.iconView.image = image } @@ -217,16 +247,31 @@ class ClientItemCell: ThemeTableViewCell { } } + if item.isSharedWithUser || item.sharedByUserOrGroup { + sharedStatusIconView.image = UIImage(named: "group") + sharedStatusIconViewRightMarginConstraint?.constant = -horizontalSmallMargin + } else { + sharedStatusIconView.image = nil + sharedStatusIconViewRightMarginConstraint?.constant = 0 + } + if item.sharedByPublicLink { + publicLinkStatusIconView.image = UIImage(named: "link") + publicLinkStatusIconViewRightMarginConstraint?.constant = -horizontalSmallMargin + } else { + publicLinkStatusIconView.image = nil + publicLinkStatusIconViewRightMarginConstraint?.constant = 0 + } + if item.type == .file { switch item.cloudStatus { - case .cloudOnly: - cloudStatusIconView.image = UIImage(named: "cloud-only") + case .cloudOnly: + cloudStatusIconView.image = UIImage(named: "cloud-only") - case .localCopy: - cloudStatusIconView.image = nil + case .localCopy: + cloudStatusIconView.image = nil - case .locallyModified, .localOnly: - cloudStatusIconView.image = UIImage(named: "cloud-local-only") + case .locallyModified, .localOnly: + cloudStatusIconView.image = UIImage(named: "cloud-local-only") } } else { cloudStatusIconView.image = nil @@ -253,7 +298,7 @@ class ClientItemCell: ThemeTableViewCell { didSet { if localID != nil { - NotificationCenter.default.addObserver(self, selector: #selector(progressChangedForItem(_:)), name: .OCCoreItemChangedProgress, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(progressChangedForItem(_:)), name: .OCCoreItemChangedProgress, object: nil) } } } @@ -289,7 +334,7 @@ class ClientItemCell: ThemeTableViewCell { progressView.rightAnchor.constraint(equalTo: moreButton.rightAnchor), progressView.topAnchor.constraint(equalTo: moreButton.topAnchor), progressView.bottomAnchor.constraint(equalTo: moreButton.bottomAnchor) - ]) + ]) self.progressView = progressView } @@ -311,6 +356,8 @@ class ClientItemCell: ThemeTableViewCell { titleLabel.applyThemeCollection(collection, itemStyle: .title, itemState: itemState) detailLabel.applyThemeCollection(collection, itemStyle: .message, itemState: itemState) + sharedStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor + publicLinkStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor cloudStatusIconView.tintColor = collection.tableRowColors.secondaryLabelColor detailLabel.textColor = collection.tableRowColors.secondaryLabelColor diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index 7f047b45f..929a8e59e 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -18,7 +18,6 @@ import UIKit import ownCloudSDK -import ownCloudApp import MobileCoreServices typealias ClientActionVieDidAppearHandler = () -> Void @@ -35,34 +34,11 @@ extension OCQueryState { } } -class ClientQueryViewController: UITableViewController, Themeable, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate { - weak var core : OCCore? - var query : OCQuery - - var items : [OCItem] = [] - +class ClientQueryViewController: QueryFileListTableViewController, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate { var selectedItemIds = Set() var actions : [Action]? - var queryProgressSummary : ProgressSummary? { - willSet { - if newValue != nil { - progressSummarizer?.pushFallbackSummary(summary: newValue!) - } - } - - didSet { - if oldValue != nil { - progressSummarizer?.popFallbackSummary(summary: oldValue!) - } - } - } - var progressSummarizer : ProgressSummarizer? - var queryRefreshControl: UIRefreshControl? - - var queryRefreshRateLimiter : OCRateLimiter = OCRateLimiter(minimumTime: 0.2) - let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) var deleteMultipleBarButtonItem: UIBarButtonItem? var moveMultipleBarButtonItem: UIBarButtonItem? @@ -77,60 +53,13 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac var quotaLabel = UILabel() var quotaObservation : NSKeyValueObservation? - - var shallShowSortBar = true + var titleButtonThemeApplierToken : ThemeApplierToken? private var _actionProgressHandler : ActionProgressHandler? - var currentPathContainsFolders : Bool { - let folders = items.filter { (item) -> Bool in - if item.type == .collection { - return true - } - return false - } - if folders.count > 0 { - return true - } - return false - } - - func makeActionProgressHandler() -> ActionProgressHandler { - if _actionProgressHandler == nil { - _actionProgressHandler = { [weak self] (progress, publish) in - if publish { - self?.progressSummarizer?.startTracking(progress: progress) - } else { - self?.progressSummarizer?.stopTracking(progress: progress) - } - } - } - - return _actionProgressHandler! - } - // MARK: - Init & Deinit - public init(core inCore: OCCore, query inQuery: OCQuery) { - - core = inCore - query = inQuery - - super.init(style: .plain) - - NotificationCenter.default.addObserver(self, selector: #selector(ClientQueryViewController.displaySettingsChanged), name: .DisplaySettingsChanged, object: nil) - self.displaySettingsChanged() - - progressSummarizer = ProgressSummarizer.shared(forCore: inCore) - - if query.sortComparator == nil { - query.sortComparator = self.sortMethod.comparator() - } - - query.delegate = self - - query.addObserver(self, forKeyPath: "state", options: .initial, context: nil) - - core?.start(query) + public override init(core inCore: OCCore, query inQuery: OCQuery) { + super.init(core: inCore, query: inQuery) let lastPathComponent = (query.queryPath as NSString?)!.lastPathComponent @@ -146,7 +75,7 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac titleButton.accessibilityIdentifier = "show-paths-button" titleButton.semanticContentAttribute = (titleButton.effectiveUserInterfaceLayoutDirection == .leftToRight) ? .forceRightToLeft : .forceLeftToRight titleButton.setImage(UIImage(named: "chevron-small-light"), for: .normal) - messageThemeApplierToken = Theme.shared.add(applier: { (_, collection, _) in + titleButtonThemeApplierToken = Theme.shared.add(applier: { (_, collection, _) in titleButton.setTitleColor(collection.navigationBarColors.labelColor, for: .normal) titleButton.tintColor = collection.navigationBarColors.labelColor }) @@ -187,95 +116,29 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac } deinit { - NotificationCenter.default.removeObserver(self, name: .DisplaySettingsChanged, object: nil) - - query.removeObserver(self, forKeyPath: "state", context: nil) - - core?.stop(query) - Theme.shared.unregister(client: self) - - if messageThemeApplierToken != nil { - Theme.shared.remove(applierForToken: messageThemeApplierToken) - messageThemeApplierToken = nil - } - - self.queryProgressSummary = nil - + queryStateObservation = nil quotaObservation = nil - } - - // MARK: - Display settings - @objc func displaySettingsChanged() { - DisplaySettings.shared.updateQuery(withDisplaySettings: query) - } - - // MARK: - Actions - @objc func refreshQuery() { - if core?.connectionStatus == OCCoreConnectionStatus.online { - UIImpactFeedbackGenerator().impactOccurred() - core?.reload(query) - } else { - if self.queryRefreshControl?.isRefreshing == true { - self.queryRefreshControl?.endRefreshing() - } - } - } - // swiftlint:disable block_based_kvo - // Would love to use the block-based KVO, but it doesn't seem to work when used on the .state property of the query :-( - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if (object as? OCQuery) === query { - self.updateQueryProgressSummary() + if titleButtonThemeApplierToken != nil { + Theme.shared.remove(applierForToken: titleButtonThemeApplierToken) + titleButtonThemeApplierToken = nil } } - // swiftlint:enable block_based_kvo // MARK: - View controller events - private let estimatedTableRowHeight : CGFloat = 80 - override func viewDidLoad() { super.viewDidLoad() - self.tableView.register(ClientItemCell.self, forCellReuseIdentifier: "itemCell") - searchController = UISearchController(searchResultsController: nil) - searchController?.searchResultsUpdater = self - searchController?.obscuresBackgroundDuringPresentation = false - searchController?.hidesNavigationBarDuringPresentation = true - searchController?.searchBar.placeholder = "Search this folder".localized - - navigationItem.searchController = searchController - navigationItem.hidesSearchBarWhenScrolling = false - - self.definesPresentationContext = true - - if shallShowSortBar { - sortBar = SortBar(frame: CGRect(x: 0, y: 0, width: self.tableView.frame.width, height: 40), sortMethod: sortMethod) - sortBar?.delegate = self - sortBar?.sortMethod = self.sortMethod - - tableView.tableHeaderView = sortBar - } - - queryRefreshControl = UIRefreshControl() - queryRefreshControl?.addTarget(self, action: #selector(self.refreshQuery), for: .valueChanged) - self.tableView.insertSubview(queryRefreshControl!, at: 0) - tableView.contentOffset = CGPoint(x: 0, y: searchController!.searchBar.frame.height) - tableView.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) - - Theme.shared.register(client: self, applyImmediately: true) - self.tableView.dragDelegate = self self.tableView.dropDelegate = self self.tableView.dragInteractionEnabled = true self.tableView.allowsMultipleSelectionDuringEditing = true - self.tableView.estimatedRowHeight = estimatedTableRowHeight - plusBarButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(plusBarButtonPressed)) plusBarButton?.accessibilityIdentifier = "client.file-add" selectBarButton = UIBarButtonItem(title: "Select".localized, style: .done, target: self, action: #selector(multipleSelectionButtonPressed)) selectBarButton?.isEnabled = false - selectBarButton?.accessibilityIdentifier = "select-button" + selectBarButton?.accessibilityIdentifier = "select-button" self.navigationItem.rightBarButtonItems = [selectBarButton!, plusBarButton!] selectDeselectAllButtonItem = UIBarButtonItem(title: "Select All".localized, style: .done, target: self, action: #selector(selectAllItems)) @@ -297,8 +160,6 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac openMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named: "open-in"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: OpenInAction.identifier!) openMultipleBarButtonItem?.isEnabled = false - self.addThemableBackgroundView() - quotaLabel.textAlignment = .center quotaLabel.font = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize) quotaLabel.numberOfLines = 0 @@ -309,24 +170,9 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - self.queryProgressSummary = nil - searchController?.searchBar.text = "" - searchController?.dismiss(animated: true, completion: nil) - - viewControllerVisible = false leaveMultipleSelection() } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - updateQueryProgressSummary() - - viewControllerVisible = true - - self.reloadTableData(ifNeeded: true) - } - private func updateFooter(text:String?) { let labelText = text ?? "" @@ -340,141 +186,18 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac self.tableView.tableFooterView = quotaLabel } - func updateQueryProgressSummary() { - var summary : ProgressSummary = ProgressSummary(indeterminate: true, progress: 1.0, message: nil, progressCount: 1) - - switch query.state { - case .stopped: - summary.message = "Stopped".localized - - case .started: - summary.message = "Started…".localized - - case .contentsFromCache: - summary.message = "Contents from cache.".localized - - case .waitingForServerReply: - summary.message = "Waiting for server response…".localized - - case .targetRemoved: - summary.message = "This folder no longer exists.".localized - - case .idle: - summary.message = "Everything up-to-date.".localized - summary.progressCount = 0 - - default: - summary.message = "Please wait…".localized - } - - if let refreshControl = self.queryRefreshControl { - if query.state == .idle { - OnMainThread { - if refreshControl.isRefreshing { - refreshControl.beginRefreshing() - } - } - } else if query.state.isFinal { - OnMainThread { - if refreshControl.isRefreshing { - refreshControl.endRefreshing() - } - } - } - } - - self.queryProgressSummary = summary - } - // MARK: - Theme support + override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) - func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - self.tableView.applyThemeCollection(collection) self.quotaLabel.textColor = collection.tableRowColors.secondaryLabelColor - self.searchController?.searchBar.applyThemeCollection(collection) - if event == .update { - self.reloadTableData() - } - } - - // MARK: - Table view data source - func itemAtIndexPath(_ indexPath : IndexPath) -> OCItem { - return items[indexPath.row] - } - - override func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.items.count } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as? ClientItemCell - let newItem = itemAtIndexPath(indexPath) - - cell?.accessibilityIdentifier = newItem.name - cell?.core = self.core - - if cell?.delegate == nil { - cell?.delegate = self - } - - // UITableView can call this method several times for the same cell, and .dequeueReusableCell will then return the same cell again. - // Make sure we don't request the thumbnail multiple times in that case. - if (cell?.item?.itemVersionIdentifier != newItem.itemVersionIdentifier) || (cell?.item?.name != newItem.name) || (cell?.item?.syncActivity != newItem.syncActivity) || (cell?.item?.cloudStatus != newItem.cloudStatus) { - cell?.item = newItem - } - - return cell! - } - - var lastTappedItemLocalID : String? - + // MARK: - Table view delegate override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // If not in multiple-selection mode, just navigate to the file or folder (collection) if !self.tableView.isEditing { - let rowItem : OCItem = itemAtIndexPath(indexPath) - - if let core = self.core { - switch rowItem.type { - case .collection: - if let path = rowItem.path { - self.navigationController?.pushViewController(ClientQueryViewController(core: core, query: OCQuery(forPath: path)), animated: true) - } - - case .file: - if lastTappedItemLocalID != rowItem.localID { - lastTappedItemLocalID = rowItem.localID - - if let progress = core.downloadItem(rowItem, options: [ .returnImmediatelyIfOfflineOrUnavailable : true ], resultHandler: { [weak self, query] (error, core, item, _) in - - guard let self = self else { return } - OnMainThread { [weak core] in - if (error == nil) || (error as NSError?)?.isOCError(withCode: .itemNotAvailableOffline) == true { - if let item = item, let core = core { - if item.localID == self.lastTappedItemLocalID { - let itemViewController = DisplayHostViewController(core: core, selectedItem: item, query: query) - itemViewController.hidesBottomBarWhenPushed = true - itemViewController.progressSummarizer = self.progressSummarizer - self.navigationController?.pushViewController(itemViewController, animated: true) - } - } - } - - if self.lastTappedItemLocalID == item?.localID { - self.lastTappedItemLocalID = nil - } - } - }) { - progressSummarizer?.startTracking(progress: progress) - } - } - } - } - - tableView.deselectRow(at: indexPath, animated: true) + super.tableView(tableView, didSelectRowAt: indexPath) } else { updateMultiSelectionUI() } @@ -487,11 +210,7 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac } override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - if tableView.isEditing { - return true - } else { - return true - } + return true } func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { @@ -504,12 +223,10 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let core = self.core else { + guard let core = self.core, let item : OCItem = itemAt(indexPath: indexPath) else { return nil } - let item: OCItem = itemAtIndexPath(indexPath) - let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .tableRow) let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) let actions = Action.sortedApplicableActions(for: actionContext) @@ -548,7 +265,7 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac let uniqueItems = Array(Set(items)) // Get possible associated actions let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .toolbar) - let actionContext = ActionContext(viewController: self, core: core, query: query, items: uniqueItems, location: actionsLocation, preferences: ["containsFolders" : currentPathContainsFolders]) + let actionContext = ActionContext(viewController: self, core: core, query: query, items: uniqueItems, location: actionsLocation) self.actions = Action.sortedApplicableActions(for: actionContext) // Enable / disable tool-bar items depending on action availability @@ -601,194 +318,7 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac removeToolbar() } - // MARK: - Message - var messageView : UIView? - var messageContainerView : UIView? - var messageImageView : VectorImageView? - var messageTitleLabel : UILabel? - var messageMessageLabel : UILabel? - var messageThemeApplierToken : ThemeApplierToken? - var messageShowsSortBar : Bool = false - - func message(show: Bool, imageName : String? = nil, title : String? = nil, message : String? = nil, showSortBar : Bool = false) { - if !show || (show && (messageShowsSortBar != showSortBar)) { - if messageView?.superview != nil { - messageView?.removeFromSuperview() - } - if !show { - return - } - } - - if messageView == nil { - var rootView : UIView - var containerView : UIView - var imageView : VectorImageView - var titleLabel : UILabel - var messageLabel : UILabel - - rootView = UIView() - rootView.translatesAutoresizingMaskIntoConstraints = false - - containerView = UIView() - containerView.translatesAutoresizingMaskIntoConstraints = false - - imageView = VectorImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - - titleLabel = UILabel() - titleLabel.translatesAutoresizingMaskIntoConstraints = false - - messageLabel = UILabel() - messageLabel.translatesAutoresizingMaskIntoConstraints = false - messageLabel.numberOfLines = 0 - messageLabel.textAlignment = .center - - containerView.addSubview(imageView) - containerView.addSubview(titleLabel) - containerView.addSubview(messageLabel) - - containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[imageView]-(20)-[titleLabel]-[messageLabel]|", - options: NSLayoutConstraint.FormatOptions(rawValue: 0), - metrics: nil, - views: ["imageView" : imageView, "titleLabel" : titleLabel, "messageLabel" : messageLabel]) - ) - - imageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true - imageView.widthAnchor.constraint(equalToConstant: 96).isActive = true - imageView.heightAnchor.constraint(equalToConstant: 96).isActive = true - - titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true - titleLabel.leftAnchor.constraint(greaterThanOrEqualTo: containerView.leftAnchor).isActive = true - titleLabel.rightAnchor.constraint(lessThanOrEqualTo: containerView.rightAnchor).isActive = true - - messageLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true - messageLabel.leftAnchor.constraint(greaterThanOrEqualTo: containerView.leftAnchor).isActive = true - messageLabel.rightAnchor.constraint(lessThanOrEqualTo: containerView.rightAnchor).isActive = true - - rootView.addSubview(containerView) - - containerView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor).isActive = true - containerView.centerYAnchor.constraint(equalTo: rootView.centerYAnchor).isActive = true - - containerView.leftAnchor.constraint(greaterThanOrEqualTo: rootView.leftAnchor, constant: 20).isActive = true - containerView.rightAnchor.constraint(lessThanOrEqualTo: rootView.rightAnchor, constant: -20).isActive = true - containerView.topAnchor.constraint(greaterThanOrEqualTo: rootView.topAnchor, constant: 20).isActive = true - containerView.bottomAnchor.constraint(lessThanOrEqualTo: rootView.bottomAnchor, constant: -20).isActive = true - - messageView = rootView - messageContainerView = containerView - messageImageView = imageView - messageTitleLabel = titleLabel - messageMessageLabel = messageLabel - - messageThemeApplierToken = Theme.shared.add(applier: { [weak self] (_, collection, _) in - self?.messageView?.backgroundColor = collection.tableBackgroundColor - - self?.messageTitleLabel?.applyThemeCollection(collection, itemStyle: .bigTitle) - self?.messageMessageLabel?.applyThemeCollection(collection, itemStyle: .bigMessage) - }) - } - - if messageView?.superview == nil { - if let rootView = self.messageView, let containerView = self.messageContainerView { - containerView.alpha = 0 - containerView.transform = CGAffineTransform(translationX: 0, y: 15) - - rootView.alpha = 0 - - self.view.addSubview(rootView) - - rootView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor).isActive = true - rootView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor).isActive = true - - if shallShowSortBar { - if showSortBar { - rootView.topAnchor.constraint(equalTo: (sortBar?.bottomAnchor)!).isActive = true - } else { - rootView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true - } - rootView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true - - messageShowsSortBar = showSortBar - } - - UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseOut, animations: { - rootView.alpha = 1 - }, completion: { (_) in - UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseOut, animations: { - containerView.alpha = 1 - containerView.transform = CGAffineTransform.identity - }) - }) - } - } - - if imageName != nil { - messageImageView?.vectorImage = Theme.shared.tvgImage(for: imageName!) - } - if title != nil { - messageTitleLabel?.text = title! - } - if message != nil { - messageMessageLabel?.text = message! - } - } - - // MARK: - Sorting - private var sortBar: SortBar? - private var sortMethod: SortMethod { - - set { - UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-method") - } - - get { - let sort = SortMethod(rawValue: UserDefaults.standard.integer(forKey: "sort-method")) ?? SortMethod.alphabeticallyDescendant - return sort - } - } - - // MARK: - Reload Data - private var tableReloadNeeded = false - - func reloadTableData(ifNeeded: Bool = false) { - /* - This is a workaround to cope with the fact that: - - UITableView.reloadData() does nothing if the view controller is not currently visible (via viewWillDisappear/viewWillAppear), so cells may hold references to outdated OCItems - - OCQuery may signal updates at any time, including when the view controller is not currently visible - - This workaround effectively makes sure reloadData() is called in viewWillAppear if a reload has been signalled to the tableView while it wasn't visible. - */ - if !viewControllerVisible { - tableReloadNeeded = true - } - - if !ifNeeded || (ifNeeded && tableReloadNeeded) { - self.tableView.reloadData() - - if viewControllerVisible { - tableReloadNeeded = false - } - - // Restore previously selected items - if tableView.isEditing && selectedItemIds.count > 0 { - var selectedItems = [OCItem]() - for row in 0.. 0 { if let core = self.core { // Get possible associated actions let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .toolbar) - let actionContext = ActionContext(viewController: self, core: core, query: query, items: selectedItems, location: actionsLocation, preferences: ["containsFolders" : containsFolders]) + let actionContext = ActionContext(viewController: self, core: core, query: query, items: selectedItems, location: actionsLocation) self.actions = Action.sortedApplicableActions(for: actionContext) @@ -868,10 +397,12 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac // Get array of OCItems from selected table view index paths selectedItemIds.removeAll() for indexPath in selectedIndexPaths { - let item = itemAtIndexPath(indexPath) - selectedItems.append(item) - if let localID = item.localID as OCLocalID? { - selectedItemIds.insert(localID) + if let item = itemAt(indexPath: indexPath) { + selectedItems.append(item) + + if let localID = item.localID as OCLocalID? { + selectedItemIds.insert(localID) + } } } } @@ -1005,131 +536,55 @@ class ClientQueryViewController: UITableViewController, Themeable, UIDropInterac present(tableViewController, animated: true, completion: nil) } - // MARK: - UIPopoverPresentationControllerDelegate - func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { - return .none - } -} - -// MARK: - Query Delegate -extension ClientQueryViewController : OCQueryDelegate { - func query(_ query: OCQuery, failedWithError error: Error) { - // Not applicable atm - } - - func queryHasChangesAvailable(_ query: OCQuery) { - queryRefreshRateLimiter.runRateLimitedBlock { - query.requestChangeSet(withFlags: OCQueryChangeSetRequestFlag(rawValue: 0)) { (query, changeSet) in - OnMainThread { - if query.state.isFinal { - OnMainThread { - if self.queryRefreshControl!.isRefreshing { - self.queryRefreshControl?.endRefreshing() - } - } - } - - let previousItemCount = self.items.count - - self.items = changeSet?.queryResult ?? [] - - switch query.state { - case .contentsFromCache, .idle, .waitingForServerReply: - if previousItemCount == 0, self.items.count == 0, query.state == .waitingForServerReply { - break - } - - if self.items.count == 0 { - if self.searchController?.searchBar.text != "" { - self.message(show: true, imageName: "icon-search", title: "No matches".localized, message: "There is no results for this search".localized) - } else { - self.message(show: true, imageName: "folder", title: "Empty folder".localized, message: "This folder contains no files or folders.".localized, showSortBar : true) - } - } else { - self.message(show: false) - } - - self.selectBarButton?.isEnabled = (self.items.count == 0) ? false : true - self.reloadTableData() - - case .targetRemoved: - self.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) - self.reloadTableData() - - default: - self.message(show: false) - } - - if let rootItem = self.query.rootItem { - if query.queryPath != "/" { - let totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) - self.updateFooter(text: totalSize) - } - } - } - } + // MARK: - ClientItemCell item resolution + override func item(for cell: ClientItemCell) -> OCItem? { + guard let indexPath = self.tableView.indexPath(for: cell) else { + return nil } - } -} -// MARK: - SortBar Delegate -extension ClientQueryViewController : SortBarDelegate { - func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { - sortMethod = didUpdateSortMethod - query.sortComparator = sortMethod.comparator() + return self.itemAt(indexPath: indexPath) } - func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { - self.present(presentViewController, animated: animated, completion: completionHandler) - } -} - -// MARK: - UISearchResultsUpdating Delegate -extension ClientQueryViewController: UISearchResultsUpdating { - func updateSearchResults(for searchController: UISearchController) { - let searchText = searchController.searchBar.text! + // MARK: - Updates + override func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { + switch query.state { + case .contentsFromCache, .idle, .waitingForServerReply: + self.selectBarButton?.isEnabled = (self.items.count == 0) ? false : true - let filterHandler: OCQueryFilterHandler = { (_, _, item) -> Bool in - if let itemName = item?.name { - return itemName.localizedCaseInsensitiveContains(searchText) - } - return false + default: break } - if searchText == "" { - if let filter = query.filter(withIdentifier: "text-search") { - query.removeFilter(filter) - } - } else { - if let filter = query.filter(withIdentifier: "text-search") { - query.updateFilter(filter, applyChanges: { filterToChange in - (filterToChange as? OCQueryFilter)?.filterHandler = filterHandler - }) - } else { - query.addFilter(OCQueryFilter.init(handler: filterHandler), withIdentifier: "text-search") + if let rootItem = self.query.rootItem { + if query.queryPath != "/" { + let totalSize = String(format: "Total: %@".localized, rootItem.sizeLocalized) + self.updateFooter(text: totalSize) } } } -} -// MARK: - ClientItemCell Delegate -extension ClientQueryViewController: ClientItemCellDelegate { - func moreButtonTapped(cell: ClientItemCell) { - guard let indexPath = self.tableView.indexPath(for: cell), let core = self.core else { - return + // MARK: - Reloads + override func restoreSelectionAfterTableReload() { + // Restore previously selected items + if tableView.isEditing && selectedItemIds.count > 0 { + var selectedItems = [OCItem]() + for row in 0.. UIModalPresentationStyle { + return .none } } +// MARK: - Drag & Drop delegates extension ClientQueryViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { guard let core = self.core else { return } @@ -1201,7 +656,9 @@ extension ClientQueryViewController: UITableViewDragDelegate { if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { if selectedIndexPaths.count > 0 { for indexPath in selectedIndexPaths { - selectedItems.append(itemAtIndexPath(indexPath)) + if let selectedItem : OCItem = itemAt(indexPath: indexPath) { + selectedItems.append(selectedItem) + } } } } @@ -1210,12 +667,16 @@ extension ClientQueryViewController: UITableViewDragDelegate { selectedItems.append(item) } - let item: OCItem = itemAtIndexPath(indexPath) - selectedItems.append(item) - updateToolbarItemsForDropping(selectedItems) + if let item: OCItem = itemAt(indexPath: indexPath) { + selectedItems.append(item) + + updateToolbarItemsForDropping(selectedItems) + + guard let dragItem = itemForDragging(item: item) else { return [] } + return [dragItem] + } - guard let dragItem = itemForDragging(item: item) else { return [] } - return [dragItem] + return [] } func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { @@ -1225,12 +686,16 @@ extension ClientQueryViewController: UITableViewDragDelegate { selectedItems.append(item) } - let item: OCItem = itemAtIndexPath(indexPath) - selectedItems.append(item) - updateToolbarItemsForDropping(selectedItems) + if let item: OCItem = itemAt(indexPath: indexPath) { + selectedItems.append(item) + + updateToolbarItemsForDropping(selectedItems) + + guard let dragItem = itemForDragging(item: item) else { return [] } + return [dragItem] + } - guard let dragItem = itemForDragging(item: item) else { return [] } - return [dragItem] + return [] } func itemForDragging(item : OCItem) -> UIDragItem? { diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 41ee93fd6..caa6d9d30 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -31,6 +31,8 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega let emptyViewController = UIViewController() var activityNavigationController : ThemeNavigationController? var activityViewController : ClientActivityViewController? + var libraryNavigationController : ThemeNavigationController? + var libraryViewController : LibraryTableViewController? var progressBar : CollapsibleProgressBar? var progressBarHeightConstraint: NSLayoutConstraint? var progressSummarizer : ProgressSummarizer? @@ -171,6 +173,11 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega activityNavigationController?.tabBarItem.title = "Status".localized activityNavigationController?.tabBarItem.image = Theme.shared.image(for: "status-flash", size: CGSize(width: 25, height: 25)) + libraryViewController = LibraryTableViewController(style: .grouped) + libraryNavigationController = ThemeNavigationController(rootViewController: libraryViewController!) + libraryNavigationController?.tabBarItem.title = "Quick Access".localized + libraryNavigationController?.tabBarItem.image = Theme.shared.image(for: "owncloud-logo", size: CGSize(width: 25, height: 25)) + progressBar = CollapsibleProgressBar(frame: CGRect.zero) progressBar?.translatesAutoresizingMaskIntoConstraints = false @@ -198,14 +205,20 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega Theme.shared.register(client: self, applyImmediately: true) if let filesNavigationController = filesNavigationController, - let activityNavigationController = activityNavigationController { - self.viewControllers = [ filesNavigationController, activityNavigationController ] + let activityNavigationController = activityNavigationController, let libraryNavigationController = libraryNavigationController { + self.viewControllers = [ filesNavigationController, libraryNavigationController, activityNavigationController ] } } + var closeClientCompletionHandler : (() -> Void)? + func closeClient(completion: (() -> Void)? = nil) { self.dismiss(animated: true, completion: { - completion?() + if completion != nil { + OnMainThread { // Work-around to make sure the self.presentingViewController is ready to present something new. Immediately after .dismiss returns, it isn't, so we wait one runloop-cycle for it to complete + completion?() + } + } }) } @@ -234,6 +247,8 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega return (viewController != emptyViewController) } self.activityViewController?.core = core + self.libraryViewController?.core = core + self.libraryViewController?.setupQueries() } } } @@ -285,10 +300,10 @@ extension ClientRootViewController : OCCoreDelegate { queueCompletionHandler() - if let navigationController = self.navigationController { + if let navigationController = self.presentingViewController as? UINavigationController { self.closeClient(completion: { if let serverListTableViewController = navigationController.topViewController as? ServerListTableViewController { - serverListTableViewController.showBookmarkUI(edit: editBookmark) + serverListTableViewController.showBookmarkUI(edit: editBookmark) } }) } diff --git a/ownCloud/Client/Library/LibraryFilesTableViewController.swift b/ownCloud/Client/Library/LibraryFilesTableViewController.swift new file mode 100644 index 000000000..5679c129f --- /dev/null +++ b/ownCloud/Client/Library/LibraryFilesTableViewController.swift @@ -0,0 +1,53 @@ +// +// LibraryFilesTableViewController +// ownCloud +// +// Created by Matthias Hühne on 13.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class LibraryFilesTableViewController: QueryFileListTableViewController { + + // MARK: - Theme support + override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + + tableView.sectionIndexColor = collection.tintColor + } + + override func sectionIndexTitles(for tableView: UITableView) -> [String]? { + if sortMethod == .alphabeticallyAscendant || sortMethod == .alphabeticallyDescendant { + return Array( Set( self.items.map { String(( $0.name?.first!.uppercased())!) })).sorted() + } + + return [] + } + + override open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { + let firstItem = self.items.filter { (( $0.name?.uppercased().hasPrefix(title) ?? nil)! ) }.first + + if let firstItem = firstItem { + if let itemIndex = self.items.index(of: firstItem) { + OnMainThread { + tableView.scrollToRow(at: IndexPath(row: itemIndex, section: 0), at: UITableView.ScrollPosition.top, animated: false) + } + } + } + + return 0 + } + +} diff --git a/ownCloud/Client/Library/LibrarySharesTableViewController.swift b/ownCloud/Client/Library/LibrarySharesTableViewController.swift new file mode 100644 index 000000000..a7359273f --- /dev/null +++ b/ownCloud/Client/Library/LibrarySharesTableViewController.swift @@ -0,0 +1,67 @@ +// +// LibrarySharesTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 13.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class LibrarySharesTableViewController: FileListTableViewController { + + var shareView : LibraryShareView? + + var shares : [OCShare] = [] { + didSet { + OnMainThread { + self.reloadTableData() + } + } + } + + override func registerCellClasses() { + self.tableView.register(ShareClientItemCell.self, forCellReuseIdentifier: "itemCell") + } + + // MARK: - Table view data source + func shareAtIndexPath(_ indexPath : IndexPath) -> OCShare { + return shares[indexPath.row] + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.shares.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as? ShareClientItemCell + let newItem = shareAtIndexPath(indexPath) + + cell?.accessibilityIdentifier = newItem.name + cell?.core = self.core + cell?.share = newItem + + if cell?.delegate == nil { + cell?.delegate = self + } + + return cell! + } +} + +extension LibrarySharesTableViewController : LibraryShareList { + func updateWith(shares: [OCShare]) { + self.shares = shares + } +} diff --git a/ownCloud/Client/Library/LibraryTableViewController.swift b/ownCloud/Client/Library/LibraryTableViewController.swift new file mode 100644 index 000000000..802bb21ae --- /dev/null +++ b/ownCloud/Client/Library/LibraryTableViewController.swift @@ -0,0 +1,412 @@ +// +// LibraryTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 12.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +protocol LibraryShareList: UIViewController { + func updateWith(shares: [OCShare]) +} + +class LibraryShareView { + enum Identifier : String { + case sharedWithYou + case sharedWithOthers + case publicLinks + case pending + } + + var identifier : Identifier + + var title : String + var image : UIImage + + var showBadge : Bool { + return identifier == .pending + } + + var viewController : LibraryShareList? + + var row : StaticTableViewRow? + + var shares : [OCShare]? + + init(identifier: LibraryShareView.Identifier, title: String, image: UIImage) { + self.identifier = identifier + + self.title = title + self.image = image + } +} + +class LibraryTableViewController: StaticTableViewController { + + weak var core : OCCore? + + deinit { + self.stopQueries() + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.title = "Quick Access".localized + self.navigationController?.navigationBar.prefersLargeTitles = true + + shareSection = StaticTableViewSection(headerTitle: "Shares".localized, footerTitle: nil, identifier: "share-section") + self.addThemableBackgroundView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.navigationBar.prefersLargeTitles = true + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.navigationController?.navigationBar.prefersLargeTitles = false + } + + // MARK: - Share setup + var startedQueries : [OCCoreQuery] = [] + + var shareQueryWithUser : OCShareQuery? + var shareQueryByUser : OCShareQuery? + var shareQueryAcceptedCloudShares : OCShareQuery? + var shareQueryPendingCloudShares : OCShareQuery? + + private func start(query: OCCoreQuery) { + core?.start(query) + startedQueries.append(query) + } + + func reloadQueries() { + for query in startedQueries { + core?.reload(query) + } + } + + private func stopQueries() { + for query in startedQueries { + core?.stop(query) + } + startedQueries.removeAll() + } + + func setupQueries() { + // Shared with user + shareQueryWithUser = OCShareQuery(scope: .sharedWithUser, item: nil) + + if let shareQueryWithUser = shareQueryWithUser { + shareQueryWithUser.refreshInterval = 60 + + shareQueryWithUser.initialPopulationHandler = { [weak self] (_) in + self?.updateSharedWithYouResult() + self?.updatePendingSharesResult() + } + shareQueryWithUser.changesAvailableNotificationHandler = shareQueryWithUser.initialPopulationHandler + + start(query: shareQueryWithUser) + } + + // Accepted cloud shares + shareQueryAcceptedCloudShares = OCShareQuery(scope: .acceptedCloudShares, item: nil) + + if let shareQueryAcceptedCloudShares = shareQueryAcceptedCloudShares { + shareQueryAcceptedCloudShares.refreshInterval = 60 + + shareQueryAcceptedCloudShares.initialPopulationHandler = { [weak self] (_) in + self?.updateSharedWithYouResult() + self?.updatePendingSharesResult() + } + shareQueryAcceptedCloudShares.changesAvailableNotificationHandler = shareQueryAcceptedCloudShares.initialPopulationHandler + + start(query: shareQueryAcceptedCloudShares) + } + + // Pending cloud shares + shareQueryPendingCloudShares = OCShareQuery(scope: .pendingCloudShares, item: nil) + + if let shareQueryPendingCloudShares = shareQueryPendingCloudShares { + shareQueryPendingCloudShares.refreshInterval = 60 + + shareQueryPendingCloudShares.initialPopulationHandler = { [weak self] (query) in + if let library = self { + library.pendingCloudSharesCounter = query.queryResults.count + self?.updatePendingSharesResult() + } + } + shareQueryPendingCloudShares.changesAvailableNotificationHandler = shareQueryPendingCloudShares.initialPopulationHandler + + start(query: shareQueryPendingCloudShares) + } + + // Shared by user + shareQueryByUser = OCShareQuery(scope: .sharedByUser, item: nil) + + if let shareQueryByUser = shareQueryByUser { + shareQueryByUser.refreshInterval = 60 + + shareQueryByUser.initialPopulationHandler = { [weak self] (_) in + self?.updateSharedByUserResults() + } + shareQueryByUser.changesAvailableNotificationHandler = shareQueryByUser.initialPopulationHandler + + start(query: shareQueryByUser) + } + + setupViews() + setupCollectionSection() + } + + // MARK: - Share views + var viewsByIdentifier : [LibraryShareView.Identifier : LibraryShareView] = [ : ] + + func add(view: LibraryShareView) { + viewsByIdentifier[view.identifier] = view + } + + func setupViews() { + self.add(view: LibraryShareView(identifier: .sharedWithOthers, title: "Shared with others".localized, image: UIImage(named: "group")!)) + self.add(view: LibraryShareView(identifier: .sharedWithYou, title: "Shared with you".localized, image: UIImage(named: "group")!)) + self.add(view: LibraryShareView(identifier: .publicLinks, title: "Public Links".localized, image: UIImage(named: "link")!)) + self.add(view: LibraryShareView(identifier: .pending, title: "Pending Invites".localized, image: UIImage(named: "group")!)) + } + + func updateView(identifier: LibraryShareView.Identifier, with shares: [OCShare]?, badge: Int? = 0) { + if let view = viewsByIdentifier[identifier] { + let shares = shares ?? [] + + view.shares = shares + view.viewController?.updateWith(shares: shares) + + if shares.count > 0 { + if view.row == nil, let core = core { + var badgeLabel : RoundedLabel? + + if view.showBadge, let badge = badge, badge > 0 { + badgeLabel = RoundedLabel() + badgeLabel?.update(text: "\(badge)", textColor: UIColor.white, backgroundColor: UIColor.red) + } + + view.row = StaticTableViewRow(rowWithAction: { [weak self, weak view] (_, _) in + guard let view = view else { return } + + var viewController : LibraryShareList? = view.viewController + + if viewController == nil { + if view.identifier == .pending { + let pendingSharesController = PendingSharesTableViewController(style: .grouped) + + pendingSharesController.title = view.title + pendingSharesController.core = core + pendingSharesController.libraryViewController = self + + viewController = pendingSharesController + } else { + let sharesFileListController = LibrarySharesTableViewController(core: core) + + sharesFileListController.title = view.title + + viewController = sharesFileListController + } + + view.viewController = viewController + } + + if let viewController = viewController { + viewController.updateWith(shares: view.shares ?? []) + + self?.navigationController?.pushViewController(viewController, animated: true) + } + }, title: view.title, image: view.image, accessoryType: .disclosureIndicator, accessoryView: badgeLabel, identifier: identifier.rawValue) + + if let row = view.row { + shareSection?.add(row: row, animated: true) + } + } else if view.showBadge, let badge = badge, badge > 0 { + guard let accessoryView = view.row?.additionalAccessoryView as? RoundedLabel else { return } + accessoryView.update(text: "\(badge)", textColor: UIColor.white, backgroundColor: UIColor.red) + } + } else { + if let row = view.row { + shareSection?.remove(rows: [row], animated: true) + view.row = nil + } + } + + self.updateShareSectionVisibility() + } + } + + // MARK: - Handle sharing updates + var pendingSharesCounter : Int = 0 { + didSet { + OnMainThread { + if self.pendingSharesCounter > 0 { + self.navigationController?.tabBarItem.badgeValue = String(self.pendingSharesCounter) + } else { + self.navigationController?.tabBarItem.badgeValue = nil + } + } + } + } + var pendingLocalSharesCounter : Int = 0 { + didSet { + pendingSharesCounter = pendingCloudSharesCounter + pendingLocalSharesCounter + } + } + var pendingCloudSharesCounter : Int = 0 { + didSet { + pendingSharesCounter = pendingCloudSharesCounter + pendingLocalSharesCounter + } + } + + func updateSharedWithYouResult() { + var shareResults : [OCShare] = [] + + if let queryResults = shareQueryWithUser?.queryResults { + shareResults.append(contentsOf: queryResults) + } + + if let queryResults = shareQueryAcceptedCloudShares?.queryResults { + shareResults.append(contentsOf: queryResults) + } + + let uniqueShares = shareResults.unique { $0.itemPath } + + let sharedWithUserAccepted = uniqueShares.filter({ (share) -> Bool in + return ((share.type == .remote) && (share.accepted == true)) || + ((share.type != .remote) && (share.state == .accepted)) + }) + + OnMainThread { + self.updateView(identifier: .sharedWithYou, with: sharedWithUserAccepted) + } + } + + func updatePendingSharesResult() { + var shareResults : [OCShare] = [] + + if let queryResults = shareQueryWithUser?.queryResults { + shareResults.append(contentsOf: queryResults) + } + if let queryResults = shareQueryPendingCloudShares?.queryResults { + shareResults.append(contentsOf: queryResults) + } + + let sharedWithUserPending = shareResults.filter({ (share) -> Bool in + return ((share.type == .remote) && (share.accepted == false)) || + ((share.type != .remote) && (share.state != .accepted)) + }) + pendingLocalSharesCounter = sharedWithUserPending.filter({ (share) -> Bool in + return (share.type != .remote) && (share.state == .pending) + }).count + + OnMainThread { + self.updateView(identifier: .pending, with: sharedWithUserPending, badge: self.pendingSharesCounter) + } + } + + func updateSharedByUserResults() { + guard let shares = shareQueryByUser?.queryResults else { return} + + let sharedByUserLinks = shares.filter({ (share) -> Bool in + return share.type == .link + }) + + let sharedByUser = shares.filter({ (share) -> Bool in + return share.type != .link + }) + + OnMainThread { + self.updateView(identifier: .sharedWithOthers, with: sharedByUser.unique { $0.itemPath }) + self.updateView(identifier: .publicLinks, with: sharedByUserLinks.unique { $0.itemPath }) + } + } + + // MARK: - Sharing Section Updates + var shareSection : StaticTableViewSection? + + func updateShareSectionVisibility() { + if let shareSection = shareSection { + if shareSection.rows.count > 0 { + if !shareSection.attached { + self.insertSection(shareSection, at: 0, animated: false) + } + } else { + if shareSection.attached { + self.removeSection(shareSection, animated: false) + } + } + } + } + + // MARK: - Collection Section + func setupCollectionSection() { + if self.sectionForIdentifier("collection-section") == nil { + let section = StaticTableViewSection(headerTitle: "Collection".localized, footerTitle: nil, identifier: "collection-section") + self.addSection(section) + + let lastWeekDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date())! + let recentsQuery = OCQuery(condition: .require([ + .where(.lastUsed, isGreaterThan: lastWeekDate), + .where(.name, isNotEqualTo: "/") + ]), inputFilter:nil) + addCollectionRow(to: section, title: "Recents".localized, image: UIImage(named: "recents")!, query: recentsQuery, actionHandler: nil) + + let favoriteQuery = OCQuery(condition: .where(.isFavorite, isEqualTo: true), inputFilter:nil) + addCollectionRow(to: section, title: "Favorites".localized, image: UIImage(named: "star")!, query: favoriteQuery, actionHandler: { [weak self] (completion) in + self?.core?.refreshFavorites(completionHandler: { (_, _) in + completion() + }) + }) + + let imageQuery = OCQuery(condition: .where(.mimeType, contains: "image"), inputFilter:nil) + addCollectionRow(to: section, title: "Images".localized, image: Theme.shared.image(for: "image", size: CGSize(width: 25, height: 25))!, query: imageQuery, actionHandler: nil) + + let pdfQuery = OCQuery(condition: .where(.mimeType, contains: "pdf"), inputFilter:nil) + addCollectionRow(to: section, title: "PDF Documents".localized, image: Theme.shared.image(for: "application-pdf", size: CGSize(width: 25, height: 25))!, query: pdfQuery, actionHandler: nil) + } + } + + func addCollectionRow(to section: StaticTableViewSection, title: String, image: UIImage, query: OCQuery?, actionHandler: ((_ completion: @escaping () -> Void) -> Void)?) { + let identifier = String(format:"%@-collection-row", title) + if section.row(withIdentifier: identifier) == nil, let core = core { + let row = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + + if let query = query { + let customFileListController = LibraryFilesTableViewController(core: core, query: query) + customFileListController.title = title + customFileListController.pullToRefreshAction = actionHandler + self?.navigationController?.pushViewController(customFileListController, animated: true) + } + + actionHandler?({}) + }, title: title, image: image, accessoryType: .disclosureIndicator, identifier: identifier) + section.add(row: row) + } + } + + // MARK: - Theming + override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + + self.navigationController?.view.backgroundColor = theme.activeCollection.navigationBarColors.backgroundColor + } +} diff --git a/ownCloud/Client/Sharing/GroupSharingEditTableViewController.swift b/ownCloud/Client/Sharing/GroupSharingEditTableViewController.swift new file mode 100644 index 000000000..9a894a111 --- /dev/null +++ b/ownCloud/Client/Sharing/GroupSharingEditTableViewController.swift @@ -0,0 +1,341 @@ +// +// GroupSharingEditTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 10.04.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class GroupSharingEditTableViewController: StaticTableViewController { + + // MARK: - Instance Variables + var share : OCShare? + var item : OCItem? + var reshares : [OCShare]? + weak var core : OCCore? + var showSubtitles : Bool = false + var createShare : Bool = false + var permissionMask : OCSharePermissionsMask? + var defaultPermissionMask : OCSharePermissionsMask + + // MARK: - Init & Deinit + + public init(core inCore: OCCore, item inItem: OCItem, share inShare: OCShare, defaultPermissions: OCSharePermissionsMask, reshares inReshares: [OCShare]? = nil) { + + core = inCore + item = inItem + share = inShare + reshares = inReshares + defaultPermissionMask = defaultPermissions + + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + if createShare { + permissionMask = defaultPermissionMask + } + + let infoButton = UIButton(type: .infoLight) + infoButton.addTarget(self, action: #selector(showInfoSubtitles), for: .touchUpInside) + let infoBarButtonItem = UIBarButtonItem(customView: infoButton) + toolbarItems = [ UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), infoBarButtonItem] + navigationController?.toolbar.isTranslucent = false + navigationController?.isToolbarHidden = false + + addPermissionSection() + + if item?.type == .collection, hasAnyPermission(of: [.update, .create, .delete]) { + addPermissionEditSection() + } + addResharesSection() + + if createShare { + let cancel = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(dismissAnimated)) + self.navigationItem.leftBarButtonItem = cancel + + let save = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(createShareAndDismiss)) + self.navigationItem.rightBarButtonItem = save + } else { + addActionSection() + } + } + + // MARK: Create Share + + @objc func createShareAndDismiss() { + guard let share = share else { return } + + if let recipient = share.recipient, let permissionMask = permissionMask { + let newShare = OCShare(recipient: recipient, path: share.itemPath, permissions: permissionMask, expiration: nil) + self.core?.createShare(newShare, options: nil, completionHandler: { (error, _) in + if error == nil { + OnMainThread { + self.dismissAnimated() + } + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Adding User to Share failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } + } + + // MARK: Permission Section + + func addPermissionSection() { + let section = StaticTableViewSection(headerTitle: "Permissions".localized, footerTitle: nil, identifier: "permission-section") + var canEdit = false + if hasAnyPermission(of: [.update, .create, .delete]) { + canEdit = true + } + var canShare = false + if hasAnyPermission(of: [.share]) { + canShare = true + } + + if core?.connection.capabilities?.sharingResharing == true { + section.add(row: StaticTableViewRow(toggleItemWithAction: { [weak self] (row, _) in + if let selected = row.value as? Bool { + self?.changePermissions(enabled: selected, permissions: [.share], completionHandler: {(_) in + }) + } + }, title: "Can Share".localized, subtitle: "", selected: canShare, identifier: "permission-section-share")) + } + + section.add(row: StaticTableViewRow(toggleItemWithAction: { [weak self] (row, _) in + guard let self = self, let item = self.item else { return } + if let selected = row.value as? Bool { + if item.type == .collection { + if selected { + self.addPermissionEditSection(animated: true, selected: true) + } else { + if let section = self.sectionForIdentifier("permission-edit-section") { + self.removeSection(section, animated: true) + } + } + self.changePermissions(enabled: selected, permissions: [.create, .update, .delete], completionHandler: { (_) in + }) + } else { + self.changePermissions(enabled: selected, permissions: [.update], completionHandler: { (_) in + }) + } + } + }, title: item?.type == .collection ? "Can Edit".localized : "Can Edit and Change".localized, subtitle: "", selected: canEdit, identifier: "permission-section-edit")) + + let subtitles = [ + "Allows the users you share with to re-share".localized, + "Allows the users you share with to edit your shared files, and to collaborate".localized + ] + updateSubtitles(subtitles: subtitles, section: section) + + self.insertSection(section, at: 0, animated: false) + } + + private func addPermissionRow(to section: StaticTableViewSection, with title: String, permission: OCSharePermissionsMask, selected: Bool, identifier: String) { + section.add(row: StaticTableViewRow(toggleItemWithAction: { [weak self] (row, _) in + if let self = self, let selected = row.value as? Bool { + self.changePermissions(enabled: selected, permissions: [ permission ], completionHandler: {(_) in + self.hidePermissionsIfNeeded() + }) + } + }, title: title.localized, subtitle: "", selected: selected, identifier: identifier)) + } + + func addPermissionEditSection(animated : Bool = false, selected : Bool = false) { + let section = StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "permission-edit-section") + + self.addPermissionRow(to: section, with: "Can Create", permission: .create, selected: (selected ? selected : hasAnyPermission(of: [.create])), identifier: "permission-section-edit-create") + self.addPermissionRow(to: section, with: "Can Change", permission: .update, selected: (selected ? selected : hasAnyPermission(of: [.update])), identifier: "permission-section-edit-change") + self.addPermissionRow(to: section, with: "Can Delete", permission: .delete, selected: (selected ? selected : hasAnyPermission(of: [.delete])), identifier: "permission-section-edit-delete") + + let subtitles = [ + "Allows the users you share with to create new files and add them to the share".localized, + "Allows uploading a new version of a shared file and replacing it".localized, + "Allows the users you share with to delete shared files".localized + ] + updateSubtitles(subtitles: subtitles, section: section) + + self.insertSection(section, at: 1, animated: animated) + } + + func hidePermissionsIfNeeded() { + if !hasAnyPermission(of: [.update, .create, .delete]) { + OnMainThread { + let section = self.sectionForIdentifier("permission-section") + let newRow = section?.row(withIdentifier: "permission-section-edit") + newRow?.cell?.accessoryType = .none + newRow?.value = false + + if let section = self.sectionForIdentifier("permission-edit-section") { + self.removeSection(section, animated: true) + } + } + } + } + + func hasAnyPermission(of permissions: OCSharePermissionsMask) -> Bool { + guard let share = share else { return false } + + var lookupPermissions = share.permissions + if createShare, let permissionMask = permissionMask { + lookupPermissions = permissionMask + } + + return !permissions.isDisjoint(with: lookupPermissions) + } + + func changePermissions(enabled: Bool, permissions : [OCSharePermissionsMask], completionHandler: @escaping (_ error : Error?) -> Void ) { + guard let share = share else { return } + + if createShare { + for permissionValue in permissions { + if enabled { + permissionMask?.insert(permissionValue) + } else { + permissionMask?.remove(permissionValue) + } + } + completionHandler(nil) + } else { + if let core = self.core { + core.update(share, afterPerformingChanges: {(share) in + for permissionValue in permissions { + if enabled { + share.permissions.insert(permissionValue) + } else { + share.permissions.remove(permissionValue) + } + } + }, completionHandler: { (error, share) in + if error == nil { + guard let changedShare = share else { return } + // Only set changed permissions and not the complete permission mask, otherwise other permission may be lost (race condition) + for permissionValue in permissions { + if enabled, changedShare.permissions.contains(permissionValue) { + self.share?.permissions.insert(permissionValue) + } else if !enabled, !changedShare.permissions.contains(permissionValue) { + self.share?.permissions.remove(permissionValue) + } + } + completionHandler(nil) + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Setting permission failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + completionHandler(shareError) + } + } + } + }) + } + } + } + + // MARK: - Reshares Section + + func addResharesSection() { + guard let core = core, let item = item else { return } + var shareRows: [StaticTableViewRow] = [] + + if let reshares = reshares, reshares.count > 0 { + for share in reshares { + shareRows.append( StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + guard let self = self else { return } + let editSharingViewController = GroupSharingEditTableViewController(core: core, item: item, share: share, defaultPermissions: self.defaultPermissionMask) + self.navigationController?.pushViewController(editSharingViewController, animated: true) + }, title: share.recipient!.displayName!, subtitle: share.permissionDescription(for: core.connection.capabilities), accessoryType: .disclosureIndicator) ) + } + + let section = StaticTableViewSection(headerTitle: "Shared with".localized, footerTitle: nil, rows: shareRows) + self.addSection(section) + } + } + + // MARK: - Action Section + + func addActionSection() { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + var footer = "" + if let date = share?.creationDate { + footer = String(format: "Invited: %@".localized, dateFormatter.string(from: date)) + } + + let section = StaticTableViewSection(headerTitle: nil, footerTitle: footer) + section.add(rows: [ + StaticTableViewRow(buttonWithAction: { [weak self] (row, _) in + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + progressView.startAnimating() + + row.cell?.accessoryView = progressView + if let core = self?.core, let share = self?.share { + core.delete(share, completionHandler: { (error) in + OnMainThread { + if error == nil { + self?.navigationController?.popViewController(animated: true) + } else { + if let shareError = error { + let alertController = UIAlertController(with: "Delete Recipient failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self?.present(alertController, animated: true) + } + } + } + }) + } + }, title: "Remove Recipient".localized, style: .destructive) + ]) + self.addSection(section) + } + + // MARK: Update Subtitles + + @objc func showInfoSubtitles() { + showSubtitles.toggle() + + guard let removeSection = self.sectionForIdentifier("permission-section") else { return } + self.removeSection(removeSection) + addPermissionSection() + + guard let removeEditSection = self.sectionForIdentifier("permission-edit-section") else { return } + self.removeSection(removeEditSection) + addPermissionEditSection(animated: false) + } + + func updateSubtitles(subtitles : [String], section : StaticTableViewSection) { + var subtitleIndex = 0 + for row in section.rows { + if showSubtitles { + row.cell?.detailTextLabel?.text = subtitles[subtitleIndex] + } else { + row.cell?.detailTextLabel?.text = "" + } + subtitleIndex += 1 + } + } +} diff --git a/ownCloud/Client/Sharing/GroupSharingTableViewController.swift b/ownCloud/Client/Sharing/GroupSharingTableViewController.swift new file mode 100644 index 000000000..fe344a716 --- /dev/null +++ b/ownCloud/Client/Sharing/GroupSharingTableViewController.swift @@ -0,0 +1,459 @@ +// +// GroupSharingTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 10.04.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class GroupSharingTableViewController: SharingTableViewController, UISearchResultsUpdating, UISearchBarDelegate, OCRecipientSearchControllerDelegate { + + // MARK: - Instance Variables + override var shares : [OCShare] { + didSet { + let recipientShares = shares.filter { (share) -> Bool in + return (share.recipient?.user?.userName == core?.connection.loggedInUser?.userName && share.canShare) + } + if recipientShares.count > 0, item.isShareable, core?.connection.capabilities?.sharingResharing == true { + recipientCanShare = true + } + } + } + var ownerCanShare : Bool { + if item.isShareable { + if item.isSharedWithUser == false { + return true + } + } + + return false + } + var searchController : UISearchController? + var recipientSearchController : OCRecipientSearchController? + var recipientCanShare : Bool = false + var shouldStartSearch : Bool = false + var defaultPermissions : OCSharePermissionsMask { + let meShares = shares.filter { (share) -> Bool in + return (share.recipient?.user?.userName == core?.connection.loggedInUser?.userName) && share.canShare + } + if let share = meShares.first { + return share.permissions + } + + var defaultPermissions : OCSharePermissionsMask = .read + + if let capabilitiesDefaultPermission = self.core?.connection.capabilities?.sharingDefaultPermissions { + defaultPermissions = capabilitiesDefaultPermission + } + + return defaultPermissions + } + + // MARK: - Init & Deinit + + override public init(core inCore: OCCore, item inItem: OCItem) { + super.init(core: inCore, item: inItem) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + if ownerCanShare || recipientCanShare { + searchController = UISearchController(searchResultsController: nil) + searchController?.searchResultsUpdater = self + searchController?.hidesNavigationBarDuringPresentation = true + searchController?.dimsBackgroundDuringPresentation = false + searchController?.searchBar.placeholder = "Add email or name".localized + searchController?.searchBar.delegate = self + navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.searchController = searchController + definesPresentationContext = true + searchController?.searchBar.applyThemeCollection(Theme.shared.activeCollection) + recipientSearchController = core?.recipientSearchController(for: item) + recipientSearchController?.delegate = self + } + + messageView = MessageView(add: self.view) + + self.navigationItem.title = "Sharing".localized + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissAnimated)) + + addHeaderView() + + shareQuery = core?.sharesWithReshares(for: item, initialPopulationHandler: { [weak self] (sharesWithReshares) in + guard let item = self?.item else { return } + + if sharesWithReshares.count > 0 { + self?.shares = sharesWithReshares.filter { (share) -> Bool in + return share.type != .link + } + OnMainThread { + self?.addShareSections() + } + } + + self?.core?.sharesSharedWithMe(for: item, initialPopulationHandler: { [weak self] (sharesWithMe) in + OnMainThread { + if sharesWithMe.count > 0 { + var shares : [OCShare] = [] + shares.append(contentsOf: sharesWithMe) + shares.append(contentsOf: sharesWithReshares) + self?.shares = shares + self?.removeShareSections() + self?.addShareSections() + } + + self?.addActionShareSection() + } + }) + }, changesAvailableNotificationHandler: { [weak self] (sharesWithReshares) in + let sharesWithReshares = sharesWithReshares.filter { (share) -> Bool in + return share.type != .link + } + self?.shares = sharesWithReshares + OnMainThread { + self?.removeShareSections() + self?.addShareSections() + + self?.addActionShareSection() + } + }, keepRunning: true) + + shareQuery?.refreshInterval = 2 + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if shouldStartSearch { + shouldStartSearch = false + // Setting search bar to first responder does only work, if view did appeared + activateRecipienSearch() + } + } + + // MARK: - Action Section + var actionSection : StaticTableViewSection? + + func addActionShareSection() { + if let share = shares.first { + + if let section = actionSection { + self.removeSection(section) + } + + OnMainThread { + if self.item.isSharedWithUser { + let section = StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "action-section") + var rows : [StaticTableViewRow] = [] + + self.actionSection = section + + let declineRow = StaticTableViewRow(buttonWithAction: { (row, _) in + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + progressView.startAnimating() + + row.cell?.accessoryView = progressView + self.core?.makeDecision(on: share, accept: false, completionHandler: { [weak self] (error) in + guard let self = self else { return } + OnMainThread { + if error == nil { + self.dismissAnimated() + } else { + if let shareError = error { + let alertController = UIAlertController(with: "Unshare failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + }, title: "Unshare".localized, style: StaticTableViewRowButtonStyle.destructive) + rows.append(declineRow) + section.add(rows: rows) + self.addSection(section) + } + } + } else { + OnMainThread { + let title = ((self.item.type == .collection) ? "Share this folder" : "Share this file").localized + let shareRow = StaticTableViewRow(buttonWithAction: { [weak self] (_, _) in + self?.activateRecipienSearch() + if let actionSection = self?.sectionForIdentifier("action-section") { + self?.removeSection(actionSection) + } + }, title: title, style: StaticTableViewRowButtonStyle.plain) + + let section : StaticTableViewSection = StaticTableViewSection(headerTitle: " ", footerTitle: nil, identifier: "action-section", rows: [shareRow]) + + self.actionSection = section + self.addSection(section, animated: true) + } + } + } + + // MARK: - Sharing UI + var searchResultsSection : StaticTableViewSection? + + func addSectionFor(shares sharesOfType: [OCShare], with title: String, identifier: OCShareType) { + var shareRows: [StaticTableViewRow] = [] + + if sharesOfType.count > 0 { + + for share in sharesOfType { + let resharedUsers = shares.filter { (filterShare) -> Bool in + return share.recipient?.user?.userName == filterShare.owner?.userName + } + + if let recipient = share.recipient, var displayName = recipient.displayName { + if recipient.user?.userName == core?.connection.loggedInUser?.userName { + displayName = "You".localized + } + + if canEdit(share: share) { + let shareRow = StaticTableViewRow(rowWithAction: { [weak self] (row, _) in + guard let self = self, let core = self.core else { return } + let editSharingViewController = GroupSharingEditTableViewController(core: core, item: self.item, share: share, defaultPermissions: self.defaultPermissions, reshares: resharedUsers) + + if share.recipient?.type == .user { + editSharingViewController.title = row.cell?.textLabel?.text + } else { + editSharingViewController.title = String(format:"%@ %@", row.cell?.textLabel?.text ?? "", "(Group)".localized) + } + + self.navigationController?.pushViewController(editSharingViewController, animated: true) + }, title: displayName, subtitle: share.permissionDescription(for: core?.connection.capabilities), image: recipient.user?.avatar, accessoryType: .disclosureIndicator) + + shareRow.representedObject = share + + shareRows.append(shareRow) + } else { + let shareRow = StaticTableViewRow(rowWithAction: nil, title: displayName, subtitle: share.permissionDescription(for: core?.connection.capabilities), image: recipient.user?.avatar, accessoryType: .none) + + shareRow.representedObject = share + + shareRows.append(shareRow) + } + } + } + let sectionIdentifier = "share-section-\(identifier.rawValue)" + if let section = self.sectionForIdentifier(sectionIdentifier) { + self.removeSection(section) + } + + let section : StaticTableViewSection = StaticTableViewSection(headerTitle: title, footerTitle: nil, identifier: sectionIdentifier, rows: shareRows) + self.addSection(section, animated: true) + } + } + + func addOwnerSection() { + if let share = shares.first, let owner = share.itemOwner, var ownerName = owner.displayName, self.sectionForIdentifier("owner-section") == nil { + if owner.displayName == core?.connection.loggedInUser?.userName { + ownerName = "You".localized + } + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + var footer : String? + if let date = share.creationDate { + footer = String(format: "Invited: %@".localized, dateFormatter.string(from: date)) + } + + let shareRow = StaticTableViewRow(rowWithAction: nil, title: String(format:"%@", ownerName), accessoryType: .none) + + let section : StaticTableViewSection = StaticTableViewSection(headerTitle: "Owner".localized, footerTitle: footer, identifier: "owner-section", rows: [shareRow]) + self.addSection(section, animated: true) + } + } + + func addShareSections() { + OnMainThread { + self.addOwnerSection() + self.addSectionFor(shares: self.shares(ofTypes: [.userShare, .remote]), with: "Users".localized, identifier: .userShare) + self.addSectionFor(shares: self.shares(ofTypes: [.groupShare]), with: "Groups".localized, identifier: .groupShare) + } + } + + func removeShareSections() { + OnMainThread { + let types : [OCShareType] = [.userShare, .groupShare] + for type in types { + let identifier = "share-section-\(type.rawValue)" + if let section = self.sectionForIdentifier(identifier) { + self.removeSection(section) + } + } + + if let section = self.sectionForIdentifier("owner-section") { + self.removeSection(section) + } + if let section = self.actionSection { + self.removeSection(section) + } + } + } + + func resetTable(showShares : Bool) { + removeShareSections() + messageView?.message(show: false) + if let section = searchResultsSection { + self.removeSection(section) + } + if showShares { + if shares.count > 0 { + self.addShareSections() + } + addActionShareSection() + } + } + + // MARK: - UISearchResultsUpdating Delegate + + func updateSearchResults(for searchController: UISearchController) { + guard let text = searchController.searchBar.text else { return } + if text.count > core?.connection.capabilities?.sharingSearchMinLength?.intValue ?? 1 { + if let recipients = recipientSearchController?.recipients, recipients.count > 0, + recipientSearchController?.searchTerm == text, + searchResultsSection == nil { + self.searchControllerHasNewResults(recipientSearchController!, error: nil) + } + + recipientSearchController?.searchTerm = text + } else if searchController.isActive { + resetTable(showShares: false) + } + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + self.resetTable(showShares: true) + self.searchController?.searchBar.isLoading = false + } + + func searchControllerHasNewResults(_ searchController: OCRecipientSearchController, error: Error?) { + OnMainThread { + guard let recipients = searchController.recipients, let core = self.core else { + self.messageView?.message(show: true, imageName: "icon-search", title: "No matches".localized, message: "There are no results for this search term".localized) + return + } + + var rows : [StaticTableViewRow] = [] + for recipient in recipients { + if !(self.shares.map { $0.recipient?.identifier == recipient.identifier }).contains(true) { + + guard let itemPath = self.item.path else { continue } + var title = "" + var image: UIImage? + if recipient.type == .user { + guard let displayName = recipient.displayName else { continue } + title = displayName + image = UIImage(named: "person") + } else { + guard let displayName = recipient.displayName else { continue } + let groupTitle = "(Group)".localized + title = "\(displayName) \(groupTitle)" + image = UIImage(named: "group") + } + + rows.append( + StaticTableViewRow(rowWithAction: { [weak self] (row, _) in + guard let self = self else { return } + let share = OCShare(recipient: recipient, path: itemPath, permissions: self.defaultPermissions, expiration: nil) + + OnMainThread { + self.searchController?.searchBar.text = "" + self.searchController?.dismiss(animated: true, completion: nil) + self.resetTable(showShares: true) + let editSharingViewController = GroupSharingEditTableViewController(core: core, item: self.item, share: share, defaultPermissions: self.defaultPermissions) + editSharingViewController.createShare = true + editSharingViewController.title = row.cell?.textLabel?.text + let navigationController = ThemeNavigationController(rootViewController: editSharingViewController) + self.navigationController?.present(navigationController, animated: true, completion: nil) + } + }, title: title, image: image) + ) + } + } + + if rows.count > 0 { + self.messageView?.message(show: false) + + self.removeShareSections() + if let section = self.searchResultsSection { + self.removeSection(section) + } + let searchResultsSection = StaticTableViewSection(headerTitle: "Invite Recipient".localized, footerTitle: nil, identifier: "search-results", rows: rows) + self.searchResultsSection = searchResultsSection + + self.addSection(searchResultsSection) + } else { + self.messageView?.message(show: true, imageName: "icon-search", title: "No matches".localized, message: "There are no results for this search term".localized) + } + } + } + + func searchController(_ searchController: OCRecipientSearchController, isWaitingForResults isSearching: Bool) { + if isSearching { + self.searchController?.searchBar.isLoading = true + } else { + self.searchController?.searchBar.isLoading = false + } + } + + func activateRecipienSearch() { + self.searchController?.isActive = true + self.searchController?.searchBar.becomeFirstResponder() + } + + // MARK: TableView Delegate + + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + if let shareAtPath = share(at: indexPath), self.canEdit(share: shareAtPath) { + return [ + UITableViewRowAction(style: .destructive, title: "Delete".localized, handler: { (_, _) in + var presentationStyle: UIAlertController.Style = .actionSheet + if UIDevice.current.isIpad() { + presentationStyle = .alert + } + + let alertController = UIAlertController(title: "Remove Recipient".localized, message: nil, preferredStyle: presentationStyle) + + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: "Delete".localized, style: .destructive, handler: { (_) in + self.core?.delete(shareAtPath, completionHandler: { (error) in + OnMainThread { + if error == nil { + self.navigationController?.popViewController(animated: true) + } else { + if let shareError = error { + let alertController = UIAlertController(with: "Remove Recipient failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + })) + + self.present(alertController, animated: true, completion: nil) + }) + ] + } + + return [] + } +} diff --git a/ownCloud/Client/Sharing/PendingSharesTableViewController.swift b/ownCloud/Client/Sharing/PendingSharesTableViewController.swift new file mode 100644 index 000000000..82199739e --- /dev/null +++ b/ownCloud/Client/Sharing/PendingSharesTableViewController.swift @@ -0,0 +1,235 @@ +// +// PendingSharesTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 12.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class PendingSharesTableViewController: StaticTableViewController { + + var shares : [OCShare]? { + didSet { + OnMainThread { + self.handleSharesUpdate() + } + } + } + weak var core : OCCore? + weak var libraryViewController : LibraryTableViewController? + var messageView : MessageView? + private static let imageWidth : CGFloat = 50 + private static let imageHeight : CGFloat = 50 + + private var didLoad : Bool = false + + let dateFormatter = DateFormatter() + + override func viewDidLoad() { + super.viewDidLoad() + + self.navigationController?.navigationBar.prefersLargeTitles = false + self.tableView.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + didLoad = true + handleSharesUpdate() + } + + func handleSharesUpdate() { + guard let shares = shares, didLoad else { return } + let pendingShares = shares.filter { (share) -> Bool in + return ((share.type == .remote) && (share.accepted == false)) || // Federated share (pending) + ((share.type != .remote) && (share.state == .pending)) // Local share (pending) + } + + let rejectedShares = shares.filter { (share) -> Bool in + return ((share.type != .remote) && (share.state == .rejected)) // Local share (rejected) + } + + updateSection(for: pendingShares, title: "Pending".localized, sectionID: "pending", placeAtTop: true) + updateSection(for: rejectedShares, title: "Declined".localized, sectionID: "declined", placeAtTop: false) + + if (pendingShares.count == 0) && (rejectedShares.count == 0) && self.presentedViewController == nil { + // Pop back to the Library when there are no longer any shares to present and no alert is active + self.navigationController?.popViewController(animated: true) + } + } + + func updateSection(for shares: [OCShare], title: String, sectionID: String, placeAtTop: Bool) { + var section : StaticTableViewSection? = sectionForIdentifier(sectionID) + + if shares.count == 0 { + if let section = section { + removeSection(section, animated: true) + } + return + } + + if section == nil { + section = StaticTableViewSection(headerTitle: title, footerTitle: nil, identifier: sectionID) + } + + if let section = section { + // Clear existing rows + section.remove(rows: section.rows) + + // Create new rows + for share in shares { + var ownerName : String? + if share.itemOwner?.displayName != nil { + ownerName = share.itemOwner?.displayName + } else if share.owner?.userName != nil { + ownerName = share.owner?.userName + } + + if let displayName = ownerName { + var itemImageType = "file" + if share.itemType == .collection { + itemImageType = "folder" + } + var footer = String(format: "Shared by %@".localized, displayName) + if let date = share.creationDate { + footer = footer.appendingFormat("\n%@", dateFormatter.string(from: date)) + } + + var itemName = share.name + if share.itemPath.count > 0 { + itemName = (share.itemPath as NSString).lastPathComponent + } + + let row = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + guard let self = self else { return } + var presentationStyle: UIAlertController.Style = .actionSheet + if UIDevice.current.isIpad() { + presentationStyle = .alert + } + + let alertController = UIAlertController(title: String(format: "Accept Invite %@".localized, itemName ?? ""), + message: nil, + preferredStyle: presentationStyle) + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) + + alertController.addAction(UIAlertAction(title: "Accept".localized, style: .default, handler: { [weak self] (_) in + self?.handleDecision(on: share, accept: true) + })) + + if share.state != .rejected { + alertController.addAction(UIAlertAction(title: "Decline".localized, style: .destructive, handler: { [weak self] (_) in + self?.handleDecision(on: share, accept: false) + })) + } + + self.present(alertController, animated: true, completion: nil) + }, title: itemName ?? "Share".localized, subtitle: footer, image: Theme.shared.image(for: itemImageType, size: CGSize(width: PendingSharesTableViewController.imageWidth, height: PendingSharesTableViewController.imageHeight)), identifier: "row") + + row.representedObject = share + + section.add(row: row) + + if share.itemPath.count > 0 { + if let itemTracker = core?.trackItem(atPath: share.itemPath, trackingHandler: { (error, item, isInitial) in + if error == nil, isInitial { + OnMainThread { + row.cell?.imageView?.image = item?.icon(fitInSize: CGSize(width: PendingSharesTableViewController.imageWidth, height: PendingSharesTableViewController.imageHeight)) + } + } + }) { + row.representedObject = itemTracker // End tracking when the row is deallocated + } + } + } + } + } + + if let section = section, !section.attached { + if placeAtTop { + insertSection(section, at: 0) + } else { + addSection(section) + } + } + } + + // MARK: - TableView Delegate + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + let row = self.staticRowForIndexPath(indexPath) + guard let share = row.representedObject as? OCShare else { return [] } + + let acceptAction = UITableViewRowAction(style: .normal, title: "Accept".localized, handler: { [weak self] (_, _) in + self?.handleDecision(on: share, accept: true) + }) + let declineAction = UITableViewRowAction(style: .destructive, title: "Decline".localized, handler: { [weak self] (_, _) in + self?.handleDecision(on: share, accept: false) + }) + + if share.state != .rejected { + return [acceptAction, declineAction] + } else { + return [acceptAction] + } + } + + // MARK: - Decision handling + func makeDecision(on share: OCShare, accept: Bool) { + if let core = core { + core.makeDecision(on: share, accept: accept, completionHandler: { [weak self] (error) in + guard let strongSelf = self else { return } + + OnMainThread { + if error != nil { + if let shareError = error { + let alertController = UIAlertController(with: (accept ? "Accept Share failed".localized : "Decline Share failed".localized), message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + strongSelf.present(alertController, animated: true) + } + } else if let libraryViewController = strongSelf.libraryViewController { + libraryViewController.reloadQueries() + } + } + }) + } + } + + func handleDecision(on share: OCShare, accept: Bool) { + if accept { + makeDecision(on: share, accept: accept) + } else { + if share.type == .remote { + var itemName = share.name + if share.itemPath.count > 0 { + itemName = (share.itemPath as NSString).lastPathComponent + } + + let alertController = UIAlertController(title: String(format: "Decline Invite %@".localized, itemName ?? ""), message: "Decline cannot be undone.", preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: "Decline".localized, style: .destructive, handler: { [weak self] (_) in + self?.makeDecision(on: share, accept: accept) + })) + self.present(alertController, animated: true, completion: nil) + } else { + makeDecision(on: share, accept: accept) + } + } + } +} + +extension PendingSharesTableViewController : LibraryShareList { + func updateWith(shares: [OCShare]) { + self.shares = shares + } +} diff --git a/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift b/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift new file mode 100644 index 000000000..387ad1531 --- /dev/null +++ b/ownCloud/Client/Sharing/PublicLinkEditTableViewController.swift @@ -0,0 +1,657 @@ +// +// PublicLinkEditTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 01.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class PublicLinkEditTableViewController: StaticTableViewController { + + // MARK: - Instance Variables + var share : OCShare + var core : OCCore + var item : OCItem + var defaultLinkName : String + + var showSubtitles : Bool = false + var createLink : Bool = false + var permissionMask : OCSharePermissionsMask? + var activeTextField: UITextField? + var currentPermissionIndex: Int { + if createLink { + return 0 + } else { + if share.canUpdate { + return 1 + } else if share.canCreate, share.canUpdate == false { + return 2 + } + return 0 + } + } + + lazy var inputToolbar: UIToolbar = { + var toolbar = UIToolbar() + toolbar.barStyle = .default + toolbar.isTranslucent = true + toolbar.sizeToFit() + let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(resignTextField)) + let flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbar.setItems([flexibleSpaceBarButtonItem, doneBarButtonItem], animated: false) + toolbar.isUserInteractionEnabled = true + return toolbar + }() + + // MARK: - Init + + internal init(share: OCShare, core: OCCore, item: OCItem, defaultLinkName: String) { + self.share = share + self.core = core + self.item = item + self.defaultLinkName = defaultLinkName + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + if let linkName = share.name { + self.navigationItem.title = linkName + } + + let shareBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareLinkURL)) + if item.type == .collection { + let infoButton = UIButton(type: .infoLight) + infoButton.addTarget(self, action: #selector(showInfoSubtitles), for: .touchUpInside) + let infoBarButtonItem = UIBarButtonItem(customView: infoButton) + let flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + if createLink { + self.toolbarItems = [flexibleSpaceBarButtonItem, infoBarButtonItem] + } else { + self.toolbarItems = [shareBarButtonItem, flexibleSpaceBarButtonItem, infoBarButtonItem] + } + } else if !createLink { + self.toolbarItems = [shareBarButtonItem] + } + + self.navigationController?.toolbar.isTranslucent = false + self.navigationController?.isToolbarHidden = false + + addNameSection() + addPermissionsSection() + preparePasswordSection(for: currentPermissionIndex) + addExpireDateSection() + + if createLink { + let cancel = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(dismissAnimated)) + self.navigationItem.leftBarButtonItem = cancel + + let create = UIBarButtonItem(title: "Create".localized, style: .done, target: self, action: #selector(createPublicLink)) + self.navigationItem.rightBarButtonItem = create + + permissionMask = .read + } else { + addActionSection() + } + } + + // MARK: - Name Section + + func addNameSection() { + let section = StaticTableViewSection(headerTitle: "Name".localized, footerTitle: nil, identifier: "name-section") + + var linkName = "" + if let savedLinkName = share.name { + linkName = savedLinkName + } + + let nameRow = StaticTableViewRow(textFieldWithAction: { [weak self] (row, sender, action) in + if let self = self, !self.createLink, action == .changed { + guard let newName = row.textField?.text else { return } + var linkName = newName + if linkName.count == 0 { + linkName = self.defaultLinkName + row.textField?.text = linkName + } + self.core.update(self.share, afterPerformingChanges: {(share) in + share.name = linkName + }, completionHandler: { (error, share) in + if error == nil { + guard let changedShare = share else { return } + self.share.name = changedShare.name + self.title = changedShare.name + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Setting name failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } else if action == .didBegin { + if let textField = sender as? UITextField { + self?.activeTextField = textField + } + } + }, placeholder: "Public Link".localized, value: linkName, secureTextEntry: false, keyboardType: .default, autocorrectionType: .default, enablesReturnKeyAutomatically: true, returnKeyType: .done, inputAccessoryView: inputToolbar, identifier: "name-text-row", actionEvent: UIControl.Event.editingDidEnd) + + section.add(row: nameRow) + self.addSection(section) + } + + // MARK: - Permission Section + + func addPermissionsSection() { + let section = StaticTableViewSection(headerTitle: "Permissions".localized, footerTitle: nil, identifier: "permission-section") + + if item.type == .collection { + + let values = [ + ["Download / View".localized : 0], + ["Download / View / Upload".localized : 1], + ["Upload only (File Drop)".localized : 2] + ] + + section.add(radioGroupWithArrayOfLabelValueDictionaries: values, radioAction: { [weak self] (row, _) in + if let self = self { + guard let selectedValueFromSection = row.section?.selectedValue(forGroupIdentifier: "permission-group") as? Int else { return } + + var newPermissions : OCSharePermissionsMask = OCSharePermissionsMask(rawValue: 0) + switch selectedValueFromSection { + case 0: + newPermissions = .read + case 1: + newPermissions = [.read, .update, .create, .delete] + case 2: + newPermissions = .create + default: + break + } + + if self.createLink { + self.permissionMask = newPermissions + self.preparePasswordSection(for: selectedValueFromSection) + } else { + if self.canPerformPermissionChange(for: selectedValueFromSection) { + + self.preparePasswordSection(for: selectedValueFromSection) + + self.core.update(self.share, afterPerformingChanges: {(share) in + share.permissions = newPermissions + }, completionHandler: { [weak self] (error, share) in + guard let self = self else { return } + if error == nil { + guard let changedShare = share else { return } + self.share.permissions = changedShare.permissions + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Setting permission failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } else { + // set selection back to previous selected value + row.section?.setSelected(self.currentPermissionIndex, groupIdentifier: "permission-group") + let permissionName = Array(values[selectedValueFromSection])[0].key + + let alertController = UIAlertController(with: "Cannot change permission".localized, message: String(format: "Before you can set the permission\n%@,\n you must enter a password.".localized, permissionName), okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }, groupIdentifier: "permission-group", selectedValue: currentPermissionIndex) + + let subtitles = [ + "Recipients can view or download contents.".localized, + "Recipients can view, download, edit, delete and upload contents.".localized, + "Receive files from multiple recipients without revealing the contents of the folder.".localized + ] + + var subtitleIndex = 0 + for row in section.rows { + if showSubtitles { + row.cell?.detailTextLabel?.text = subtitles[subtitleIndex] + } else { + row.cell?.detailTextLabel?.text = "" + } + subtitleIndex += 1 + } + + self.insertSection(section, at: 1) + } + } + + // MARK: - Password Section + + func preparePasswordSection(for selectionIndex : Int) { + let needsPassword = passwordRequired(for: selectionIndex) + var hasPassword = false + if share.protectedByPassword { + hasPassword = true + } + + if self.sectionForIdentifier("password-section") == nil { + let passwordSection = StaticTableViewSection(headerTitle: "Options".localized, footerTitle: nil, identifier: "password-section") + self.addSection(passwordSection) + } + + if let passwordSection = self.sectionForIdentifier("password-section") { + if needsPassword { + if let passwordSwitchRow = passwordSection.row(withIdentifier: "password-switch-row") { + passwordSection.remove(rows: [passwordSwitchRow], animated: false) + } + + if passwordSection.row(withIdentifier: "password-field-row") == nil { + self.passwordRow(passwordSection) + } + } else { + if let passwordSwitchRow = passwordSection.row(withIdentifier: "password-switch-row") { + if let switchView = passwordSwitchRow.cell?.accessoryView as? UISwitch { + switchView.isOn = hasPassword + } + } else { + self.passwordSwitchRow(hasPassword, passwordSection) + } + + if hasPassword == false, let passwordFieldRow = passwordSection.row(withIdentifier: "password-field-row") { + passwordSection.remove(rows: [passwordFieldRow], animated: false) + } + } + } + } + + func passwordSwitchRow(_ hasPassword : Bool, _ passwordSection : StaticTableViewSection) { + let passwordRow = StaticTableViewRow(switchWithAction: { [weak self, weak passwordSection] (_, sender) in + if let self = self, let passwordSwitch = sender as? UISwitch { + if passwordSwitch.isOn == false, let passwordSection = passwordSection, let passwordFieldRow = passwordSection.row(withIdentifier: "password-field-row") { + passwordSection.remove(rows: [passwordFieldRow], animated: true) + + // delete password + if !self.createLink { + self.core.update(self.share, afterPerformingChanges: { (share) in + share.protectedByPassword = false + }, completionHandler: { (error, share) in + if error == nil { + guard let changedShare = share else { return } + self.share.protectedByPassword = changedShare.protectedByPassword + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Deleting password failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } + } else if passwordSwitch.isOn, let passwordSection = passwordSection { + self.passwordRow(passwordSection) + } + } + }, title: "Password".localized, value: hasPassword, identifier: "password-switch-row") + passwordSection.insert(row: passwordRow, at: 0, animated: true) + } + + func passwordRow(_ passwordSection : StaticTableViewSection) { + var passwordValue = "" + if let password = share.password { + passwordValue = password + } + + let expireDateRow = StaticTableViewRow(secureTextFieldWithAction: { [weak self] (_, sender, action) in + if action == .changed, let self = self { + guard let textField = sender as? UITextField else { return } + if !self.createLink { + self.core.update(self.share, afterPerformingChanges: {(share) in + share.password = textField.text + share.protectedByPassword = true + }, completionHandler: { [weak self] (error, share) in + guard let self = self else { return } + if error == nil { + guard let changedShare = share else { return } + self.share.password = changedShare.password + self.share.protectedByPassword = changedShare.protectedByPassword + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Setting password failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } + } else if action == .didBegin { + if let textField = sender as? UITextField { + self?.activeTextField = textField + } + } + + }, placeholder: "Type to update password".localized, value: passwordValue, keyboardType: .default, enablesReturnKeyAutomatically: true, returnKeyType: .done, inputAccessoryView: inputToolbar, identifier: "password-field-row", actionEvent: UIControl.Event.editingDidEnd) + passwordSection.add(row: expireDateRow) + } + + // MARK: - Expire Date Section + + func addExpireDateSection() { + var hasExpireDate = false + if share.expirationDate != nil || core.connection.capabilities?.publicSharingExpireDateEnforced == true { + hasExpireDate = true + } + var needsExpireDate = false + if self.core.connection.capabilities?.publicSharingExpireDateEnforced == true { + needsExpireDate = true + } + + let expireSection = StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "expire-section") + + if needsExpireDate == false { + let expireDateRow = StaticTableViewRow(switchWithAction: { [weak self, weak expireSection] (_, sender) in + if let self = self, let expireSection = expireSection, let expireDateSwitch = sender as? UISwitch { + if expireDateSwitch.isOn == false, let expireDateRow = expireSection.row(withIdentifier: "expire-date-row") { + var rows : [StaticTableViewRow] = [expireDateRow] + if let expireDatePickerRow = expireSection.row(withIdentifier: "date-picker-row") { + rows.append(expireDatePickerRow) + } + expireSection.remove(rows: rows, animated: true) + } else if expireDateSwitch.isOn, expireSection.row(withIdentifier: "expire-date-row") == nil { + self.expireDateRow(expireSection) + } + + if !self.createLink { + self.core.update(self.share, afterPerformingChanges: {(share) in + if expireDateSwitch.isOn { + if let datePicker = sender as? UIDatePicker { + share.expirationDate = datePicker.date + } else { + share.expirationDate = self.defaultExpireDate + } + } else { + share.expirationDate = nil + } + }, completionHandler: { [weak self] (error, share) in + guard let self = self else { return } + if error == nil { + guard let changedShare = share else { return } + self.share.expirationDate = changedShare.expirationDate + + if let expireDateRow = expireSection.row(withIdentifier: "expire-date-row"), let expirationDate = changedShare.expirationDate { + OnMainThread { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + expireDateRow.cell?.textLabel?.text = dateFormatter.string(from: expirationDate) + } + } + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Setting expiration date failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } else if self.createLink { + if let row = self.rowInSection(expireSection, rowIdentifier: "expire-date-row") { + row.representedObject = self.defaultExpireDate + } + } + } + }, title: "Expiration date".localized, value: hasExpireDate, identifier: "expire-row") + expireSection.add(row: expireDateRow) + } + + if hasExpireDate || needsExpireDate { + self.expireDateRow(expireSection) + } + self.addSection(expireSection) + } + + func expireDateRow(_ expireSection : StaticTableViewSection) { + let expireDate = defaultExpireDate + + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .none + let expireDateRow = StaticTableViewRow(buttonWithAction: { [weak self, weak expireSection] (_, _) in + guard let expireSection = expireSection else { return } + + if expireSection.row(withIdentifier: "date-picker-row") == nil { + + let datePickerRow = StaticTableViewRow(datePickerWithAction: { [weak self] (row, sender) in + + guard let datePicker = sender as? UIDatePicker else { return } + if let expireDateRow = expireSection.row(withIdentifier: "expire-date-row") { + OnMainThread { + expireDateRow.cell?.textLabel?.text = dateFormatter.string(from: datePicker.date) + expireDateRow.representedObject = datePicker.date + } + } + + if let self = self, !self.createLink { + self.core.update(self.share, afterPerformingChanges: { (share) in + share.expirationDate = datePicker.date + }, completionHandler: { [weak self] (error, share) in + guard let self = self else { return } + if error == nil { + guard let changedShare = share else { return } + self.share.expirationDate = changedShare.expirationDate + + if let expireDateRow = expireSection.row(withIdentifier: "expire-date-row") { + OnMainThread { + expireDateRow.cell?.textLabel?.text = dateFormatter.string(from: datePicker.date) + expireDateRow.representedObject = datePicker.date + } + } + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Setting expiration date failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } + }, date: expireDate, identifier: "date-picker-row") + expireSection.add(row: datePickerRow, animated: true) + if let indexPath = datePickerRow.indexPath { + self?.tableView.scrollToRow(at: indexPath, at: .middle, animated: true) + } + } else { + if let datePickerRow = expireSection.row(withIdentifier: "date-picker-row") { + expireSection.remove(rows: [datePickerRow], animated: true) + } + } + }, title: dateFormatter.string(from: expireDate), style: .plain, alignment: .left, identifier: "expire-date-row") + + expireSection.add(row: expireDateRow) + } + + // MARK: - Action Section + + func addActionSection() { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + var footer = "" + if let date = share.creationDate { + footer = String(format: "Created: %@".localized, dateFormatter.string(from: date)) + } + + let deleteSection = StaticTableViewSection(headerTitle: "Public Link".localized, footerTitle: footer) + + if let shareURL = share.url { + deleteSection.add(rows: [ + StaticTableViewRow(buttonWithAction: { (row, _) in + UIPasteboard.general.url = shareURL + row.cell?.textLabel?.text = shareURL.absoluteString + row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 15.0) + row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowColors.secondaryLabelColor + row.cell?.textLabel?.numberOfLines = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + row.cell?.textLabel?.text = "Copy Public Link".localized + row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 17.0) + row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tintColor + row.cell?.textLabel?.numberOfLines = 1 + } + }, title: "Copy Public Link".localized, style: .plain) + ]) + } + + deleteSection.add(rows: [ + StaticTableViewRow(buttonWithAction: { [weak self] (row, _) in + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + progressView.startAnimating() + + row.cell?.accessoryView = progressView + if let self = self { + self.core.delete(self.share, completionHandler: { [weak self] (error) in + guard let self = self else { return } + OnMainThread { + if error == nil { + self.navigationController?.popViewController(animated: true) + } else { + if let shareError = error { + let alertController = UIAlertController(with: "Deleting Public Link failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } + }, title: "Delete".localized, style: StaticTableViewRowButtonStyle.destructive) + ]) + + self.addSection(deleteSection) + } + + // MARK: - Actions + + @objc func showInfoSubtitles() { + showSubtitles.toggle() + guard let removeSection = self.sectionForIdentifier("permission-section") else { return } + self.removeSection(removeSection) + addPermissionsSection() + } + + @objc func shareLinkURL() { + guard let shareURL = share.url, let capabilities = self.core.connection.capabilities else { return } + + let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil) + if capabilities.publicSharingSocialShare == false { + activityViewController.excludedActivityTypes = [ + UIActivity.ActivityType.postToTwitter, + UIActivity.ActivityType.postToVimeo, + UIActivity.ActivityType.postToWeibo, + UIActivity.ActivityType.postToFlickr, + UIActivity.ActivityType.postToFacebook, + UIActivity.ActivityType.postToTencentWeibo, + UIActivity.ActivityType("com.facebook.Facebook") + ] + } + activityViewController.popoverPresentationController?.sourceView = self.view + self.present(activityViewController, animated: true, completion: nil) + } + + // MARK: - Permission Helper + + func canPerformPermissionChange(for selectionIndex : Int) -> Bool { + if share.protectedByPassword == false, passwordRequired(for: selectionIndex) { + return false + } + + return true + } + + // MARK: - Password Helper + + func passwordRequired(for selectionIndex: Int) -> Bool { + guard let capabilities = self.core.connection.capabilities else { return false } + + if selectionIndex == 0, capabilities.publicSharingPasswordEnforcedForReadOnly == true { + return true + } else if selectionIndex == 1, capabilities.publicSharingPasswordEnforcedForReadWrite == true { + return true + } else if selectionIndex == 2, capabilities.publicSharingPasswordEnforcedForUploadOnly == true { + return true + } + + return false + } + + // MARK: Add New Link Share + + @objc func createPublicLink() { + let nameRow = rowInSection(sectionForIdentifier("name-section"), rowIdentifier: "name-text-row") + let shareName = nameRow?.textField?.text + let passwordRow = rowInSection(sectionForIdentifier("password-section"), rowIdentifier: "password-field-row") + let password = passwordRow?.textField?.text + let dateRow = rowInSection(sectionForIdentifier("expire-section"), rowIdentifier: "expire-date-row") + var expiration : Date? + if let date = dateRow?.representedObject as? Date { + expiration = date + } + + if let permissionMask = permissionMask { + let share = OCShare(publicLinkToPath: self.share.itemPath, linkName: shareName, permissions: permissionMask, password: password, expiration: expiration) + self.core.createShare(share, options: nil, completionHandler: { (error, _) in + if error == nil { + OnMainThread { + self.dismissAnimated() + } + } else { + if let shareError = error { + OnMainThread { + let alertController = UIAlertController(with: "Creating public link failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + } + } + + @objc func resignTextField (_ sender: UIBarButtonItem) { + activeTextField?.resignFirstResponder() + } + + var defaultExpireDate : Date { + var expireDate = Date() + if let newDate = Calendar.current.date(byAdding: .day, value: 7, to: expireDate) { + expireDate = newDate + } + if let date = share.expirationDate { + expireDate = date + } else if self.core.connection.capabilities?.publicSharingExpireDateEnabled == true, let defaultDays = self.core.connection.capabilities?.publicSharingDefaultExpireDateDays { + if let newDate = Calendar.current.date(byAdding: .day, value: defaultDays.intValue, to: Date()) { + expireDate = newDate + } + } + + return expireDate + } +} diff --git a/ownCloud/Client/Sharing/PublicLinkTableViewController.swift b/ownCloud/Client/Sharing/PublicLinkTableViewController.swift new file mode 100644 index 000000000..4d7fd73c0 --- /dev/null +++ b/ownCloud/Client/Sharing/PublicLinkTableViewController.swift @@ -0,0 +1,285 @@ +// +// PublicLinkTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 01.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class PublicLinkTableViewController: SharingTableViewController { + + var publicLinkSharingEnabled : Bool { + if let core = core, core.connectionStatus == .online, core.connection.capabilities?.sharingAPIEnabled == true, core.connection.capabilities?.publicSharingEnabled == true, item.isShareable { return true } + return false + } + + // MARK: - Instance Variables + override func viewDidLoad() { + super.viewDidLoad() + + messageView = MessageView(add: self.view) + + self.navigationItem.title = "Links".localized + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissAnimated)) + + addHeaderView() + addPrivateLinkSection() + + if publicLinkSharingEnabled { + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addPublicLink)) + shareQuery = core?.sharesWithReshares(for: item, initialPopulationHandler: { [weak self] (sharesWithReshares) in + if let self = self, sharesWithReshares.count > 0 { + self.shares = sharesWithReshares.filter { (share) -> Bool in + return share.type == .link + } + OnMainThread { + self.addShareSections() + } + } + }, changesAvailableNotificationHandler: { [weak self] (sharesWithReshares) in + guard let self = self else { return } + let sharesWithReshares = sharesWithReshares.filter { (share) -> Bool in + return share.type == .link + } + self.shares = sharesWithReshares + OnMainThread { + self.removeShareSections() + self.addShareSections() + self.handleEmptyShares() + } + }, keepRunning: true) + shareQuery?.refreshInterval = 2 + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + handleEmptyShares() + } + + // MARK: - Sharing UI + func addSectionFor(shares sharesOfType: [OCShare], with title: String, identifier: OCShareType) { + var shareRows: [StaticTableViewRow] = [] + + if sharesOfType.count > 0 { + for share in sharesOfType { + if canEdit(share: share) { + var linkName = "" + if let shareName = share.name { + linkName = shareName + } else if let token = share.token { + linkName = token + } + let shareRow = StaticTableViewRow(rowWithAction: { [weak self] (_, _) in + guard let self = self, let core = self.core else { return } + let editPublicLinkViewController = PublicLinkEditTableViewController(share: share, core: core, item: self.item, defaultLinkName: self.defaultLinkName()) + self.navigationController?.pushViewController(editPublicLinkViewController, animated: true) + }, title: linkName, subtitle: share.permissionDescription(for: core?.connection.capabilities), accessoryType: .disclosureIndicator) + + shareRow.representedObject = share + + shareRows.append(shareRow) + } else { + let shareRow = StaticTableViewRow(rowWithAction: nil, title: share.name!, subtitle: share.permissionDescription(for: core?.connection.capabilities), accessoryType: .none) + + shareRow.representedObject = share + + shareRows.append(shareRow) + } + } + } + + shareRows.append( StaticTableViewRow(buttonWithAction: { [weak self] (_, _) in + self?.addPublicLink() + }, title: "Create Public Link".localized, style: StaticTableViewRowButtonStyle.plain)) + + let sectionIdentifier = "share-section-\(identifier.rawValue)" + if let section = self.sectionForIdentifier(sectionIdentifier) { + self.removeSection(section) + } + let section : StaticTableViewSection = StaticTableViewSection(headerTitle: title, footerTitle: nil, identifier: sectionIdentifier, rows: shareRows) + self.addSection(section, animated: false) + } + + func addShareSections() { + if publicLinkSharingEnabled { + OnMainThread { + self.addSectionFor(shares: self.shares(ofTypes: [.link]), with: "Public Links".localized, identifier: .link) + } + } + } + + func removeShareSections() { + OnMainThread { + let types : [OCShareType] = [.link] + for type in types { + let identifier = "share-section-\(type.rawValue)" + if let section = self.sectionForIdentifier(identifier) { + self.removeSection(section) + } + } + } + } + + func resetTable(showShares : Bool) { + removeShareSections() + if shares.count > 0 && showShares { + messageView?.message(show: false) + } + self.addShareSections() + } + + func handleEmptyShares() { + if shares.count == 0 { + OnMainThread { + self.resetTable(showShares: false) + } + } + } + + // MARK: - Private Link Section + + func addPrivateLinkSection() { + let identifier = "private-link-section" + if let section = self.sectionForIdentifier(identifier) { + self.removeSection(section) + } + + let footer = "Only recipients can use this link. Use it as a permanent link to point to this resource".localized + + OnMainThread { + let section = StaticTableViewSection(headerTitle: "Private Link".localized, footerTitle: footer, identifier: "private-link-section") + var rows : [StaticTableViewRow] = [] + + self.core?.retrievePrivateLink(for: self.item, completionHandler: { (error, url) in + + guard let url = url else { return } + if error == nil { + OnMainThread { + let privateLinkRow = StaticTableViewRow(buttonWithAction: { (row, _) in + UIPasteboard.general.url = url + row.cell?.textLabel?.text = url.absoluteString + row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 15.0) + row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tableRowColors.secondaryLabelColor + row.cell?.textLabel?.numberOfLines = 0 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + row.cell?.textLabel?.text = "Copy Private Link".localized + row.cell?.textLabel?.font = UIFont.systemFont(ofSize: 17.0) + row.cell?.textLabel?.textColor = Theme.shared.activeCollection.tintColor + row.cell?.textLabel?.numberOfLines = 1 + } + }, title: "Copy Private Link".localized, style: .plain) + rows.append(privateLinkRow) + + section.add(rows: rows) + self.insertSection(section, at: 0) + } + } + }) + } + } + + func retrievePrivateLink(for item: OCItem, in row: StaticTableViewRow) { + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + progressView.startAnimating() + row.cell?.accessoryView = progressView + + self.core?.retrievePrivateLink(for: item, completionHandler: { (error, url) in + OnMainThread { + row.cell?.accessoryView = nil + } + if error == nil { + guard let url = url else { return } + UIPasteboard.general.url = url + } + }) + } + + // MARK: TableView Delegate + + override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + if let share = share(at: indexPath), self.canEdit(share: share) { + return [ + UITableViewRowAction(style: .destructive, title: "Delete".localized, handler: { (_, _) in + var presentationStyle: UIAlertController.Style = .actionSheet + if UIDevice.current.isIpad() { + presentationStyle = .alert + } + + let alertController = UIAlertController(title: "Delete Public Link".localized, + message: nil, + preferredStyle: presentationStyle) + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) + + alertController.addAction(UIAlertAction(title: "Delete".localized, style: .destructive, handler: { (_) in + self.core?.delete(share, completionHandler: { (error) in + OnMainThread { + if error == nil { + self.navigationController?.popViewController(animated: true) + } else { + if let shareError = error { + let alertController = UIAlertController(with: "Delete Public Link failed".localized, message: shareError.localizedDescription, okLabel: "OK".localized, action: nil) + self.present(alertController, animated: true) + } + } + } + }) + })) + + self.present(alertController, animated: true, completion: nil) + }), + + UITableViewRowAction(style: .normal, title: "Copy".localized, handler: { (_, _) in + if let shareURL = share.url { + UIPasteboard.general.url = shareURL + } + }) + ] + } + + return [] + } + + // MARK: Add New Link Share + + @objc func addPublicLink() { + if let path = item.path, let core = core { + var permissions : OCSharePermissionsMask = .create + if item.type == .file { + permissions = .read + } + + let share = OCShare(publicLinkToPath: path, linkName: defaultLinkName(), permissions: permissions, password: nil, expiration: nil) + let editPublicLinkViewController = PublicLinkEditTableViewController(share: share, core: core, item: self.item, defaultLinkName: defaultLinkName()) + editPublicLinkViewController.createLink = true + let navigationController = ThemeNavigationController(rootViewController: editPublicLinkViewController) + self.navigationController?.present(navigationController, animated: true, completion: nil) + } + } + + func defaultLinkName() -> String { + guard let name = item.name else { return "" } + var linkName = String(format:"%@ %@", name, "Link".localized) + if let defaultLinkName = core?.connection.capabilities?.publicSharingDefaultLinkName { + linkName = defaultLinkName + } + if shares.count >= 1 { + linkName = String(format:"%@ (%ld)", linkName, shares.count) + } + + return linkName + } +} diff --git a/ownCloud/Client/Sharing/ShareClientItemCell.swift b/ownCloud/Client/Sharing/ShareClientItemCell.swift new file mode 100644 index 000000000..9f053d1d6 --- /dev/null +++ b/ownCloud/Client/Sharing/ShareClientItemCell.swift @@ -0,0 +1,53 @@ +// +// ShareClientItemCell.swift +// ownCloud +// +// Created by Matthias Hühne on 16.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class ShareClientItemCell: ClientItemCell { + + var itemTracker : OCCoreItemTracking? + var iconSize : CGSize = CGSize(width: 40, height: 40) + + // MARK: - Share Item + + var share : OCShare? { + didSet { + if let share = share { + self.titleLabel.text = share.itemPath + if share.itemType == .collection { + self.iconView.image = Theme.shared.image(for: "folder", size: iconSize) + } else { + self.iconView.image = Theme.shared.image(for: "file", size: iconSize) + } + itemTracker = core?.trackItem(atPath: share.itemPath, trackingHandler: { (error, item, isInitial) in + if error == nil, let item = item, isInitial { + OnMainThread { + self.item = item + } + } + }) + } + } + } + + deinit { + itemTracker = nil + } + +} diff --git a/ownCloud/Client/Sharing/SharingTableViewController.swift b/ownCloud/Client/Sharing/SharingTableViewController.swift new file mode 100644 index 000000000..0338e2a10 --- /dev/null +++ b/ownCloud/Client/Sharing/SharingTableViewController.swift @@ -0,0 +1,95 @@ +// +// SharingTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 20.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class SharingTableViewController : StaticTableViewController { + + // MARK: - Instance Variables + weak var core : OCCore? + var item : OCItem + + var shareQuery : OCShareQuery? + var shares : [OCShare] = [] + + var messageView : MessageView? + + // MARK: - Init & Deinit + + public init(core inCore: OCCore, item inItem: OCItem) { + core = inCore + item = inItem + + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + if let shareQuery = shareQuery { + core?.stop(shareQuery) + } + } + + // MARK: - View events + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.isToolbarHidden = true + } + + // MARK: - Header View + func addHeaderView() { + guard let core = core else { return } + + let headerView = MoreViewHeader(for: item, with: core, favorite: false) + self.tableView.tableHeaderView = headerView + self.tableView.layoutTableHeaderView() + self.tableView.tableHeaderView?.backgroundColor = Theme.shared.activeCollection.tableBackgroundColor + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + if size.width != self.view.frame.size.width { + DispatchQueue.main.async { + self.tableView.layoutTableHeaderView() + } + } + } + + // MARK: - Sharing Helper + func canEdit(share: OCShare) -> Bool { + if core?.connection.loggedInUser?.userName == share.owner?.userName || core?.connection.loggedInUser?.userName == share.itemOwner?.userName { + return true + } + + return false + } + + func shares(ofTypes types: [OCShareType]) -> [OCShare] { + return shares.filter { (share) -> Bool in + return types.contains(share.type) + } + } + + func share(at indexPath: IndexPath) -> OCShare? { + return staticRowForIndexPath(indexPath).representedObject as? OCShare + } +} diff --git a/ownCloud/Client/SortMethod.swift b/ownCloud/Client/SortMethod.swift index c62bd13b2..84784ae40 100644 --- a/ownCloud/Client/SortMethod.swift +++ b/ownCloud/Client/SortMethod.swift @@ -28,8 +28,9 @@ public enum SortMethod: Int { case type = 2 case size = 3 case date = 4 + case shared = 5 - static var all: [SortMethod] = [alphabeticallyAscendant, alphabeticallyDescendant, type, size, date] + static var all: [SortMethod] = [alphabeticallyAscendant, alphabeticallyDescendant, type, size, date, shared] func localizedName() -> String { var name = "" @@ -45,6 +46,8 @@ public enum SortMethod: Int { name = "size".localized case .date: name = "date".localized + case .shared: + name = "shared".localized } return name @@ -109,6 +112,21 @@ public enum SortMethod: Int { return leftMimeType!.compare(rightMimeType!) } + case .shared: + comparator = { (left, right) in + guard let leftItem = left as? OCItem else { return .orderedSame } + guard let rightItem = right as? OCItem else { return .orderedSame } + + let leftShared = leftItem.isSharedWithUser || leftItem.isShared + let rightShared = rightItem.isSharedWithUser || rightItem.isShared + + if leftShared == rightShared { + return .orderedSame + } else if leftShared { + return .orderedAscending + } + return .orderedDescending + } case .date: comparator = { (left, right) in diff --git a/ownCloud/Client/Viewer/DisplayHostViewController.swift b/ownCloud/Client/Viewer/DisplayHostViewController.swift index e1085d98b..d0fc8b091 100644 --- a/ownCloud/Client/Viewer/DisplayHostViewController.swift +++ b/ownCloud/Client/Viewer/DisplayHostViewController.swift @@ -37,6 +37,7 @@ class DisplayHostViewController: UIPageViewController { } } private var query: OCQuery + private var queryStarted : Bool = false private weak var viewControllerToTansition: DisplayViewController? private var queryObservation : NSKeyValueObservation? @@ -50,12 +51,20 @@ class DisplayHostViewController: UIPageViewController { super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + if query.state == .stopped { + core.start(query) + queryStarted = true + } + queryObservation = query.observe(\OCQuery.hasChangesAvailable, options: [.initial, .new]) { [weak self] (query, _) in query.requestChangeSet(withFlags: .onlyResults) { ( _, changeSet) in guard let changeSet = changeSet else { return } - self?.items = self?.applyImageFilesFilter(items: changeSet.queryResult) + if let queryResult = changeSet.queryResult, let items = self?.applyImageFilesFilter(items: queryResult) { + self?.items = items + } } } + Theme.shared.register(client: self) } @@ -64,6 +73,11 @@ class DisplayHostViewController: UIPageViewController { } deinit { + if queryStarted { + core?.stop(query) + queryStarted = false + } + queryObservation?.invalidate() Theme.shared.unregister(client: self) } diff --git a/ownCloud/Client/Viewer/DisplayViewController.swift b/ownCloud/Client/Viewer/DisplayViewController.swift index e3c95dac7..f3e7fe9b1 100644 --- a/ownCloud/Client/Viewer/DisplayViewController.swift +++ b/ownCloud/Client/Viewer/DisplayViewController.swift @@ -352,13 +352,13 @@ class DisplayViewController: UIViewController, OCQueryDelegate { let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreItem) let actionContext = ActionContext(viewController: self, core: core, items: [item], location: actionsLocation) - let moreViewController = Action.cardViewController(for: item, with: actionContext, completionHandler: { [weak self] (action, _) in + if let moreViewController = Action.cardViewController(for: item, with: actionContext, completionHandler: { [weak self] (action, _) in if !(action is OpenInAction) { self?.navigationController?.popViewController(animated: true) } - }) - - self.present(asCard: moreViewController, animated: true) + }) { + self.present(asCard: moreViewController, animated: true) + } } // MARK: - Query management diff --git a/ownCloud/FileLists/FileListTableViewController.swift b/ownCloud/FileLists/FileListTableViewController.swift new file mode 100644 index 000000000..8cbd151dd --- /dev/null +++ b/ownCloud/FileLists/FileListTableViewController.swift @@ -0,0 +1,277 @@ +// +// FileListTableViewController.swift +// ownCloud +// +// Created by Matthias Hühne on 21.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 + +class FileListTableViewController: UITableViewController, ClientItemCellDelegate, Themeable { + weak var core : OCCore? + + let estimatedTableRowHeight : CGFloat = 80 + + var progressSummarizer : ProgressSummarizer? + private var _actionProgressHandler : ActionProgressHandler? + + public init(core inCore: OCCore) { + core = inCore + super.init(style: .plain) + + progressSummarizer = ProgressSummarizer.shared(forCore: inCore) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + Theme.shared.unregister(client: self) + } + + func makeActionProgressHandler() -> ActionProgressHandler { + if _actionProgressHandler == nil { + _actionProgressHandler = { [weak self] (progress, publish) in + if publish { + self?.progressSummarizer?.startTracking(progress: progress) + } else { + self?.progressSummarizer?.stopTracking(progress: progress) + } + } + } + + return _actionProgressHandler! + } + + // MARK: - Item retrieval + func item(for cell: ClientItemCell) -> OCItem? { + return cell.item + } + + func itemAt(indexPath : IndexPath) -> OCItem? { + return (self.tableView.cellForRow(at: indexPath) as? ClientItemCell)?.item + } + + // MARK: - ClientItemCellDelegate + func moreButtonTapped(cell: ClientItemCell) { + guard let item = self.item(for: cell), let core = core, let query = query(forItem: item) else { + return + } + + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .moreItem) + let actionContext = ActionContext(viewController: self, core: core, query: query, items: [item], location: actionsLocation) + + if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: makeActionProgressHandler()) { + self.present(asCard: moreViewController, animated: true) + } + } + + // MARK: - Visibility handling + private var viewControllerVisible : Bool = false + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + viewControllerVisible = false + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewControllerVisible = true + self.reloadTableData(ifNeeded: true) + } + + // MARK: - View setup + override func viewDidLoad() { + super.viewDidLoad() + + self.navigationController?.navigationBar.prefersLargeTitles = false + Theme.shared.register(client: self, applyImmediately: true) + self.tableView.estimatedRowHeight = estimatedTableRowHeight + + self.registerCellClasses() + + if allowPullToRefresh { + pullToRefreshControl = UIRefreshControl() + pullToRefreshControl?.addTarget(self, action: #selector(self.pullToRefreshTriggered), for: .valueChanged) + self.tableView.insertSubview(pullToRefreshControl!, at: 0) + tableView.contentOffset = CGPoint(x: 0, y: self.pullToRefreshVerticalOffset) + tableView.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) + } + + self.addThemableBackgroundView() + } + + func registerCellClasses() { + self.tableView.register(ClientItemCell.self, forCellReuseIdentifier: "itemCell") + } + + // MARK: - Pull-to-refresh handling + var allowPullToRefresh : Bool = false + + var pullToRefreshControl: UIRefreshControl? + var pullToRefreshAction: ((_ completion: @escaping () -> Void) -> Void)? + + var pullToRefreshVerticalOffset : CGFloat { + return 0 + } + + @objc func pullToRefreshTriggered() { + if core?.connectionStatus == OCCoreConnectionStatus.online { + UIImpactFeedbackGenerator().impactOccurred() + performPullToRefreshAction() + } else { + pullToRefreshEnded() + } + } + + func performPullToRefreshAction() { + if pullToRefreshAction != nil { + pullToRefreshBegan() + + pullToRefreshAction?({ [weak self] in + self?.pullToRefreshEnded() + }) + } + } + + func pullToRefreshBegan() { + if let refreshControl = pullToRefreshControl { + OnMainThread { + if refreshControl.isRefreshing { + refreshControl.beginRefreshing() + } + } + } + } + + func pullToRefreshEnded() { + if let refreshControl = pullToRefreshControl { + OnMainThread { + if refreshControl.isRefreshing == true { + refreshControl.endRefreshing() + } + } + } + } + + // MARK: - Reload Data + private var tableReloadNeeded = false + + func reloadTableData(ifNeeded: Bool = false) { + /* + This is a workaround to cope with the fact that: + - UITableView.reloadData() does nothing if the view controller is not currently visible (via viewWillDisappear/viewWillAppear), so cells may hold references to outdated OCItems + - OCQuery may signal updates at any time, including when the view controller is not currently visible + + This workaround effectively makes sure reloadData() is called in viewWillAppear if a reload has been signalled to the tableView while it wasn't visible. + */ + if !viewControllerVisible { + tableReloadNeeded = true + } + + if !ifNeeded || (ifNeeded && tableReloadNeeded) { + self.tableView.reloadData() + + if viewControllerVisible { + tableReloadNeeded = false + } + + self.restoreSelectionAfterTableReload() + } + } + + func restoreSelectionAfterTableReload() { + } + + // MARK: - Single item query creation + func query(forItem: OCItem) -> OCQuery? { + if let path = forItem.path { + return OCQuery(forPath: path) + } + + return nil + } + + // MARK: - Table view data source + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + // MARK: - Table view delegate + var lastTappedItemLocalID : String? + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if !self.tableView.isEditing { + guard let rowItem : OCItem = itemAt(indexPath: indexPath) else { + return + } + + if let core = self.core { + switch rowItem.type { + case .collection: + if let path = rowItem.path { + self.navigationController?.pushViewController(ClientQueryViewController(core: core, query: OCQuery(forPath: path)), animated: true) + } + + case .file: + guard let query = self.query(forItem: rowItem) else { + return + } + + if lastTappedItemLocalID != rowItem.localID { + lastTappedItemLocalID = rowItem.localID + + if let progress = core.downloadItem(rowItem, options: [ .returnImmediatelyIfOfflineOrUnavailable : true ], resultHandler: { [weak self, query] (error, core, item, _) in + + guard let self = self else { return } + OnMainThread { [weak core] in + if (error == nil) || (error as NSError?)?.isOCError(withCode: .itemNotAvailableOffline) == true { + if let item = item, let core = core { + if item.localID == self.lastTappedItemLocalID { + let itemViewController = DisplayHostViewController(core: core, selectedItem: item, query: query) + itemViewController.hidesBottomBarWhenPushed = true + itemViewController.progressSummarizer = self.progressSummarizer + self.navigationController?.pushViewController(itemViewController, animated: true) + } + } + } + + if self.lastTappedItemLocalID == item?.localID { + self.lastTappedItemLocalID = nil + } + } + }) { + progressSummarizer?.startTracking(progress: progress) + } + } + } + } + + tableView.deselectRow(at: indexPath, animated: true) + } + } + + // MARK: - Themable + func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + self.tableView.applyThemeCollection(collection) + + if event == .update { + self.reloadTableData() + } + } +} diff --git a/ownCloud/FileLists/QueryFileListTableViewController.swift b/ownCloud/FileLists/QueryFileListTableViewController.swift new file mode 100644 index 000000000..ebb152812 --- /dev/null +++ b/ownCloud/FileLists/QueryFileListTableViewController.swift @@ -0,0 +1,333 @@ +// +// QueryFileListTableViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 23.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 QueryFileListTableViewController: FileListTableViewController, SortBarDelegate, OCQueryDelegate, UISearchResultsUpdating { + var query : OCQuery + + var queryRefreshRateLimiter : OCRateLimiter = OCRateLimiter(minimumTime: 0.2) + + var messageView : MessageView? + + var items : [OCItem] = [] + + public init(core inCore: OCCore, query inQuery: OCQuery) { + query = inQuery + + super.init(core: inCore) + + allowPullToRefresh = true + + NotificationCenter.default.addObserver(self, selector: #selector(QueryFileListTableViewController.displaySettingsChanged), name: .DisplaySettingsChanged, object: nil) + self.displaySettingsChanged() + + query.delegate = self + + if query.sortComparator == nil { + query.sortComparator = self.sortMethod.comparator() + } + + core?.start(query) + + queryStateObservation = query.observe(\OCQuery.state, options: .initial, changeHandler: { [weak self] (_, _) in + self?.updateQueryProgressSummary() + }) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self, name: .DisplaySettingsChanged, object: nil) + + queryProgressSummary = nil + + core?.stop(query) + } + + // MARK: - Display settings + @objc func displaySettingsChanged() { + DisplaySettings.shared.updateQuery(withDisplaySettings: query) + } + + // MARK: - Sorting + var sortBar: SortBar? + var sortMethod: SortMethod { + + set { + UserDefaults.standard.setValue(newValue.rawValue, forKey: "sort-method") + } + + get { + let sort = SortMethod(rawValue: UserDefaults.standard.integer(forKey: "sort-method")) ?? SortMethod.alphabeticallyDescendant + return sort + } + } + + // MARK: - Search + var searchController: UISearchController? + + // MARK: - Search: UISearchResultsUpdating Delegate + func updateSearchResults(for searchController: UISearchController) { + let searchText = searchController.searchBar.text! + + let filterHandler: OCQueryFilterHandler = { (_, _, item) -> Bool in + if let itemName = item?.name { + return itemName.localizedCaseInsensitiveContains(searchText) + } + return false + } + + if searchText == "" { + if let filter = query.filter(withIdentifier: "text-search") { + query.removeFilter(filter) + } + } else { + if let filter = query.filter(withIdentifier: "text-search") { + query.updateFilter(filter, applyChanges: { filterToChange in + (filterToChange as? OCQueryFilter)?.filterHandler = filterHandler + }) + } else { + query.addFilter(OCQueryFilter.init(handler: filterHandler), withIdentifier: "text-search") + } + } + } + + // MARK: - Query progress reporting + var showQueryProgress : Bool = true + + var queryProgressSummary : ProgressSummary? { + willSet { + if newValue != nil, showQueryProgress { + progressSummarizer?.pushFallbackSummary(summary: newValue!) + } + } + + didSet { + if oldValue != nil, showQueryProgress { + progressSummarizer?.popFallbackSummary(summary: oldValue!) + } + } + } + + var queryStateObservation : NSKeyValueObservation? + + // MARK: - Pull-to-refresh handling + override var pullToRefreshVerticalOffset: CGFloat { + return searchController?.searchBar.frame.height ?? 0 + } + + override func performPullToRefreshAction() { + super.performPullToRefreshAction() + core?.reload(query) + } + + func updateQueryProgressSummary() { + var summary : ProgressSummary = ProgressSummary(indeterminate: true, progress: 1.0, message: nil, progressCount: 1) + + switch query.state { + case .stopped: + summary.message = "Stopped".localized + + case .started: + summary.message = "Started…".localized + + case .contentsFromCache: + summary.message = "Contents from cache.".localized + + case .waitingForServerReply: + summary.message = "Waiting for server response…".localized + + case .targetRemoved: + summary.message = "This folder no longer exists.".localized + + case .idle: + summary.message = "Everything up-to-date.".localized + summary.progressCount = 0 + + default: + summary.message = "Please wait…".localized + } + + if pullToRefreshControl != nil { + if query.state == .idle { + self.pullToRefreshBegan() + } else if query.state.isFinal { + self.pullToRefreshEnded() + } + } + + self.queryProgressSummary = summary + } + + // MARK: - SortBarDelegate + var shallShowSortBar = true + + func sortBar(_ sortBar: SortBar, didUpdateSortMethod: SortMethod) { + sortMethod = didUpdateSortMethod + query.sortComparator = sortMethod.comparator() + } + + func sortBar(_ sortBar: SortBar, presentViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) { + self.present(presentViewController, animated: animated, completion: completionHandler) + } + + // MARK: - Query Delegate + func query(_ query: OCQuery, failedWithError error: Error) { + // Not applicable atm + } + + func queryHasChangesAvailable(_ query: OCQuery) { + queryRefreshRateLimiter.runRateLimitedBlock { + query.requestChangeSet(withFlags: OCQueryChangeSetRequestFlag(rawValue: 0)) { (query, changeSet) in + OnMainThread { + if query.state.isFinal { + OnMainThread { + if self.pullToRefreshControl?.isRefreshing == true { + self.pullToRefreshControl?.endRefreshing() + } + } + } + + let previousItemCount = self.items.count + + self.items = changeSet?.queryResult ?? [] + + switch query.state { + case .contentsFromCache, .idle, .waitingForServerReply: + if previousItemCount == 0, self.items.count == 0, query.state == .waitingForServerReply { + break + } + + if self.items.count == 0 { + if self.searchController?.searchBar.text != "" { + self.messageView?.message(show: true, imageName: "icon-search", title: "No matches".localized, message: "There is no results for this search".localized) + } else { + self.messageView?.message(show: true, imageName: "folder", title: "Empty folder".localized, message: "This folder contains no files or folders.".localized) + } + } else { + self.messageView?.message(show: false) + } + + self.tableView.reloadData() + case .targetRemoved: + self.messageView?.message(show: true, imageName: "folder", title: "Folder removed".localized, message: "This folder no longer exists on the server.".localized) + self.tableView.reloadData() + + default: + self.messageView?.message(show: false) + } + + self.performUpdatesWithQueryChanges(query: query, changeSet: changeSet) + } + } + } + } + + func performUpdatesWithQueryChanges(query: OCQuery, changeSet: OCQueryChangeSet?) { + } + + // MARK: - Themeable + override func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + super.applyThemeCollection(theme: theme, collection: collection, event: event) + + self.searchController?.searchBar.applyThemeCollection(collection) + } + + // MARK: - Events + override func viewDidLoad() { + super.viewDidLoad() + + searchController = UISearchController(searchResultsController: nil) + searchController?.searchResultsUpdater = self + searchController?.obscuresBackgroundDuringPresentation = false + searchController?.hidesNavigationBarDuringPresentation = true + searchController?.searchBar.placeholder = "Search this folder".localized + searchController?.searchBar.applyThemeCollection(Theme.shared.activeCollection) + + navigationItem.searchController = searchController + navigationItem.hidesSearchBarWhenScrolling = false + + self.definesPresentationContext = true + + if shallShowSortBar { + sortBar = SortBar(frame: CGRect(x: 0, y: 0, width: self.tableView.frame.width, height: 40), sortMethod: sortMethod) + sortBar?.delegate = self + sortBar?.sortMethod = self.sortMethod + + tableView.tableHeaderView = sortBar + } + + messageView = MessageView(add: self.view) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + updateQueryProgressSummary() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + queryProgressSummary = nil + + searchController?.searchBar.text = "" + searchController?.dismiss(animated: true, completion: nil) + } + + // MARK: - Item retrieval + override func itemAt(indexPath : IndexPath) -> OCItem? { + return items[indexPath.row] + } + + // MARK: - Single item query creation + override func query(forItem: OCItem) -> OCQuery? { + return query + } + + // MARK: - Table view data source + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as? ClientItemCell + if let newItem = itemAt(indexPath: indexPath) { + + cell?.accessibilityIdentifier = newItem.name + cell?.core = self.core + + if cell?.delegate == nil { + cell?.delegate = self + } + + // UITableView can call this method several times for the same cell, and .dequeueReusableCell will then return the same cell again. + // Make sure we don't request the thumbnail multiple times in that case. + if newItem.displaysDifferent(than: cell?.item) { + cell?.item = newItem + } + } + + return cell! + } +} diff --git a/ownCloud/Resources/Assets.xcassets/copy-file.imageset/copy-icon.pdf b/ownCloud/Resources/Assets.xcassets/copy-file.imageset/copy-icon.pdf index 08b143e42..fa097ea23 100644 Binary files a/ownCloud/Resources/Assets.xcassets/copy-file.imageset/copy-icon.pdf and b/ownCloud/Resources/Assets.xcassets/copy-file.imageset/copy-icon.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/duplicate-file.imageset/duplicate-icon.pdf b/ownCloud/Resources/Assets.xcassets/duplicate-file.imageset/duplicate-icon.pdf index b1200f763..121b3790f 100644 Binary files a/ownCloud/Resources/Assets.xcassets/duplicate-file.imageset/duplicate-icon.pdf and b/ownCloud/Resources/Assets.xcassets/duplicate-file.imageset/duplicate-icon.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json index 7ca136c06..88d588eb0 100644 --- a/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json +++ b/ownCloud/Resources/Assets.xcassets/folder.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "folder.pdf" + "filename" : "folder-icon.pdf" } ], "info" : { diff --git a/ownCloud/Resources/Assets.xcassets/folder.imageset/folder-icon.pdf b/ownCloud/Resources/Assets.xcassets/folder.imageset/folder-icon.pdf new file mode 100644 index 000000000..72cf66729 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/folder.imageset/folder-icon.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/folder.imageset/folder.pdf b/ownCloud/Resources/Assets.xcassets/folder.imageset/folder.pdf deleted file mode 100644 index b94f4b65f..000000000 Binary files a/ownCloud/Resources/Assets.xcassets/folder.imageset/folder.pdf and /dev/null differ diff --git a/ownCloud/Resources/Assets.xcassets/group.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/group.imageset/Contents.json new file mode 100644 index 000000000..bbc9b2911 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/group.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "baseline_group_black_24pt_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "baseline_group_black_24pt_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "baseline_group_black_24pt_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_1x.png b/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_1x.png new file mode 100644 index 000000000..51b043bca Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_1x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_2x.png b/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_2x.png new file mode 100644 index 000000000..43ef39f0b Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_2x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_3x.png b/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_3x.png new file mode 100644 index 000000000..b8e313f11 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/group.imageset/baseline_group_black_24pt_3x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/link.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/link.imageset/Contents.json new file mode 100644 index 000000000..d7e81cf2a --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/link.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "public-4.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/link.imageset/public-4.pdf b/ownCloud/Resources/Assets.xcassets/link.imageset/public-4.pdf new file mode 100644 index 000000000..0eaaed5b2 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/link.imageset/public-4.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/open-in.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/open-in.imageset/Contents.json index 0d5349521..7abee5cc9 100644 --- a/ownCloud/Resources/Assets.xcassets/open-in.imageset/Contents.json +++ b/ownCloud/Resources/Assets.xcassets/open-in.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "open-in-icon.pdf" + "filename" : "open-in.pdf" } ], "info" : { diff --git a/ownCloud/Resources/Assets.xcassets/open-in.imageset/open-in-icon.pdf b/ownCloud/Resources/Assets.xcassets/open-in.imageset/open-in-icon.pdf deleted file mode 100644 index 15651f735..000000000 Binary files a/ownCloud/Resources/Assets.xcassets/open-in.imageset/open-in-icon.pdf and /dev/null differ diff --git a/ownCloud/Resources/Assets.xcassets/open-in.imageset/open-in.pdf b/ownCloud/Resources/Assets.xcassets/open-in.imageset/open-in.pdf new file mode 100644 index 000000000..eed03834e Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/open-in.imageset/open-in.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/person.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/person.imageset/Contents.json new file mode 100644 index 000000000..402395b7d --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/person.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "baseline_person_black_24pt_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "baseline_person_black_24pt_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "baseline_person_black_24pt_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_1x.png b/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_1x.png new file mode 100644 index 000000000..48667badd Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_1x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_2x.png b/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_2x.png new file mode 100644 index 000000000..f4949d535 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_2x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_3x.png b/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_3x.png new file mode 100644 index 000000000..269cea974 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/person.imageset/baseline_person_black_24pt_3x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/recents.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/recents.imageset/Contents.json new file mode 100644 index 000000000..75ad43b3c --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/recents.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "baseline_access_time_black_24pt_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "baseline_access_time_black_24pt_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "baseline_access_time_black_24pt_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_1x.png b/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_1x.png new file mode 100644 index 000000000..785310d05 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_1x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_2x.png b/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_2x.png new file mode 100644 index 000000000..4399b1e07 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_2x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_3x.png b/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_3x.png new file mode 100644 index 000000000..1546cb5e4 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/recents.imageset/baseline_access_time_black_24pt_3x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/round-add-button.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/round-add-button.imageset/Contents.json new file mode 100644 index 000000000..f052e9c91 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/round-add-button.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "round-add-button-4.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/round-add-button.imageset/round-add-button-4.pdf b/ownCloud/Resources/Assets.xcassets/round-add-button.imageset/round-add-button-4.pdf new file mode 100644 index 000000000..5a212ac03 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/round-add-button.imageset/round-add-button-4.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/shared.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/shared.imageset/Contents.json new file mode 100644 index 000000000..c1c0ce992 --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/shared.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "shared-2.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/shared.imageset/shared-2.pdf b/ownCloud/Resources/Assets.xcassets/shared.imageset/shared-2.pdf new file mode 100644 index 000000000..602da84d9 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/shared.imageset/shared-2.pdf differ diff --git a/ownCloud/Resources/Assets.xcassets/star.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/star.imageset/Contents.json new file mode 100644 index 000000000..b9ed1b13f --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/star.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "baseline_star_black_24pt_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "baseline_star_black_24pt_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "baseline_star_black_24pt_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_1x.png b/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_1x.png new file mode 100644 index 000000000..679a47179 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_1x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_2x.png b/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_2x.png new file mode 100644 index 000000000..0e819e209 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_2x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_3x.png b/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_3x.png new file mode 100644 index 000000000..5a1b2e383 Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/star.imageset/baseline_star_black_24pt_3x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/unstar.imageset/Contents.json b/ownCloud/Resources/Assets.xcassets/unstar.imageset/Contents.json new file mode 100644 index 000000000..e06e7fc8a --- /dev/null +++ b/ownCloud/Resources/Assets.xcassets/unstar.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "baseline_star_border_black_24pt_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "baseline_star_border_black_24pt_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "baseline_star_border_black_24pt_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_1x.png b/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_1x.png new file mode 100644 index 000000000..30a54c31d Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_1x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_2x.png b/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_2x.png new file mode 100644 index 000000000..b2e94b50f Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_2x.png differ diff --git a/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_3x.png b/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_3x.png new file mode 100644 index 000000000..a3c6734ce Binary files /dev/null and b/ownCloud/Resources/Assets.xcassets/unstar.imageset/baseline_star_border_black_24pt_3x.png differ diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 288de2e04..adb34cf1c 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -115,7 +115,7 @@ "Authorization failed" = "Authorization failed"; "The server declined access with the credentials stored for this connection." = "The server declined access with the credentials stored for this connection."; "No matches" = "No matches"; -"There is no results for this search" = "There is no results for this search"; +"There are no results for this search term" = "There are no results for this search term"; "Status" = "Status"; /* Server List*/ @@ -245,7 +245,7 @@ "File name cannot contain / or \\" = "File name cannot contain / or \\"; "New Folder" = "New Folder"; "Folder name" = "Folder name"; -"Rename" =" Rename"; +"Rename" ="Rename"; "Create folder" =" Create folder"; "Duplicate" = "Duplicate"; "Move" = "Move"; @@ -284,6 +284,97 @@ "Albums" = "Albums"; "Importing from photo library" = "Importing from photo library"; +/* Sharing */ +"Searching Shares…" = "Searching Shares…"; +"Recipient" = "Recipient"; +"Recipients" = "Recipients"; +"Public Link" = "Public Link"; +"Public Links" = "Public Links"; +"Shared by %@" = "Shared by %@"; +"Invite Recipient" = "Invite Recipient"; +"Recipients" = "Recipients"; +"Add email or name" = "Add email or name"; +"Users" = "Users"; +"Groups" = "Groups"; +"Start typing to search users, groups and remote users." = "Start typing to search users, groups and remote users."; +"(Group)" = "(Group)"; +"Adding User to Share failed" = "Adding User to Share failed"; +"Permissions" = "Permissions"; +"Invited: %@" = "Invited: %@"; +"Created: %@" = "Created: %@"; +"Allows the users you share with to re-share" = "Allows the users you share with to re-share"; +"Allows the users you share with to edit your shared files, and to collaborate" = "Allows the users you share with to edit your shared files, and to collaborate"; +"Allows the users you share with to create new files and add them to the share" = "Allows the users you share with to create new files and add them to the share"; +"Allows uploading a new version of a shared file and replacing it" = "Allows uploading a new version of a shared file and replacing it"; +"Allows the users you share with to delete shared files" = "Allows the users you share with to delete shared files"; +"Setting permission failed" = "Setting permission failed"; +"Shared with" = "Shared with"; +"Remove Recipient failed" = "Remove Recipient failed"; +"Remove Recipient" = "Remove Recipient"; +"Create" = "Create"; +"Change" = "Change"; +"Recipients can view or download contents." = "Recipients can view or download contents."; +"Recipients can view, download, edit, delete and upload contents." = "Recipients can view, download, edit, delete and upload contents."; +"Receive files from multiple recipients without revealing the contents of the folder." = "Receive files from multiple recipients without revealing the contents of the folder."; +"Download / View" = "Download / View"; +"Download / View / Upload" = "Download / View / Upload"; +"Upload only (File Drop)" = "Upload only (File Drop)"; +"Creating public link failed" = "Creating public link failed"; +"Create Public Link" = "Create Public Link"; +"Links" = "Links"; +"Link" = "Link"; +"Setting expiration date failed" = "Setting expiration date failed"; +"Expiration date" = "Expiration date"; +"Copy Public Link" = "Copy Public Link"; +"Delete Public Link" = "Delete Public Link"; +"Deleting Public Link failed" = "Deleting Public Link failed"; +"Deleting password failed" = "Deleting password failed"; +"Setting password failed" = "Setting password failed"; +"Type to update password" = "Type to update password"; +"Cannot change permission" = "Cannot change permission"; +"Before you can set the permission\n%@,\n you must enter a password." = "Before you can set the permission\n%@,\n you must enter a password."; +"Password Protected" = "Password Protected"; +"Pending Federated Invites" = "Pending Federated Invites"; +"Pending Invites" = "Pending Invites"; +"Shared with you" = "Shared with you"; +"Shared with others" = "Shared with others"; +"Shares" = "Shares"; +"Copy Private Link" = "Copy Private Link"; +"Only recipients can use this link. Use it as a permanent link to point to this resource" = "Only recipients can use this link. Use it as a permanent link to point to this resource"; +"Accept Share failed" = "Accept Share failed"; +"Decline Share failed" = "Decline Share failed"; +"Accept" = "Accept"; +"Decline" = "Decline"; +"Declined" = "Declined"; +"Decline Share" = "Decline Share"; +"Unshare" = "Unshare"; +"Unshare failed" = "Unshare failed"; +"Are you sure you want to unshare these items?" = "Are you sure you want to unshare these items?"; +"Are you sure you want to unshare this item?" = "Are you sure you want to unshare this item?"; +"Share" = "Share"; +"Read" = "Read"; +"Can Share" = "Can Share"; +"Can Edit" = "Can Edit"; +"Can Edit and Change" = "Can Edit and Change"; +"Can Create" = "Can Create"; +"Can Change" = "Can Change"; +"Can Delete" = "Can Delete"; +"Accept Invite %@" = "Accept Invite %@"; +"Decline Invite %@" = "Decline Invite %@"; +"Decline cannot be undone." = "Decline cannot be undone."; +"Sharing" = "Sharing"; +"You" = "You"; +"Share this file" = "Share this file"; +"Share this folder" = "Share this folder"; + +/* Quick Access view */ +"Quick Access" = "Quick Access"; +"Collection" = "Collection"; +"Recents" = "Recents"; +"Favorites"= "Favorites"; +"Images" = "Images"; +"PDF Documents" = "PDF Documents"; + /* Photo upload settings */ "Media Upload" = "Media Upload"; "Convert HEIC to JPEG" = "Convert HEIC to JPEG"; @@ -305,3 +396,4 @@ "Compacting of '%@' failed" = "Compacting of '%@' failed"; "Delete Offline Copies" = "Delete Offline Copies"; "Manage" = "Manage"; +"Storage" = "Storage"; diff --git a/ownCloud/SDK Extensions/OCCore+Extension.swift b/ownCloud/SDK Extensions/OCCore+Extension.swift new file mode 100644 index 000000000..fa61ca2a2 --- /dev/null +++ b/ownCloud/SDK Extensions/OCCore+Extension.swift @@ -0,0 +1,129 @@ +// +// OCCore+Extension.swift +// ownCloud +// +// Created by Matthias Hühne on 17.04.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 Foundation +import ownCloudSDK + +extension OCCore { + + func unifiedShares(for item: OCItem, completionHandler: @escaping (_ shares: [OCShare]) -> Void) { + let combinedShares : NSMutableArray = NSMutableArray() + let dispatchGroup = DispatchGroup() + + if let shareQuery = OCShareQuery(scope: .itemWithReshares, item: item) { + dispatchGroup.enter() + + shareQuery.initialPopulationHandler = { [weak self] query in + combinedShares.addObjects(from: query.queryResults) + dispatchGroup.leave() + self?.stop(query) + } + start(shareQuery) + } + + if let shareQuery = OCShareQuery(scope: .sharedWithUser, item: item) { + dispatchGroup.enter() + + shareQuery.initialPopulationHandler = { [weak self] query in + let sharesWithMe = query.queryResults.filter({ (share) -> Bool in + return share.itemPath == item.path + }) + + combinedShares.addObjects(from: sharesWithMe) + dispatchGroup.leave() + self?.stop(query) + } + start(shareQuery) + } + + if let shareQuery = OCShareQuery(scope: .acceptedCloudShares, item: item) { + dispatchGroup.enter() + + shareQuery.initialPopulationHandler = { [weak self] query in + combinedShares.addObjects(from: query.queryResults) + dispatchGroup.leave() + self?.stop(query) + } + start(shareQuery) + } + + dispatchGroup.notify(queue: .main, execute: { + completionHandler((combinedShares as? [OCShare])!) + }) + } + + @discardableResult func sharesSharedWithMe(for item: OCItem, initialPopulationHandler: @escaping (_ shares: [OCShare]) -> Void, keepRunning: Bool = false) -> OCShareQuery? { + if let shareQuery = OCShareQuery(scope: .sharedWithUser, item: item) { + shareQuery.initialPopulationHandler = { [weak self] query in + let shares = query.queryResults.filter({ (share) -> Bool in + return share.itemPath == item.path + }) + initialPopulationHandler(shares) + + if !keepRunning { + self?.stop(query) + } + } + start(shareQuery) + + return keepRunning ? shareQuery : nil + } + + return nil + } + + @discardableResult func acceptedCloudShares(for item: OCItem, initialPopulationHandler: @escaping (_ shares: [OCShare]) -> Void, keepRunning: Bool = false) -> OCShareQuery? { + if let shareQuery = OCShareQuery(scope: .acceptedCloudShares, item: item) { + shareQuery.initialPopulationHandler = { [weak self] query in + let shares = query.queryResults.filter({ (share) -> Bool in + return share.itemPath == item.path + }) + initialPopulationHandler(shares) + + if !keepRunning { + self?.stop(query) + } + } + start(shareQuery) + + return keepRunning ? shareQuery : nil + } + + return nil + } + + @discardableResult func sharesWithReshares(for item: OCItem, initialPopulationHandler: @escaping (_ shares: [OCShare]) -> Void, changesAvailableNotificationHandler: @escaping (_ shares: [OCShare]) -> Void, keepRunning: Bool) -> OCShareQuery? { + if let shareQuery = OCShareQuery(scope: .itemWithReshares, item: item) { + shareQuery.initialPopulationHandler = { [weak self] query in + initialPopulationHandler(query.queryResults) + + if !keepRunning { + self?.stop(query) + } + } + shareQuery.changesAvailableNotificationHandler = { query in + changesAvailableNotificationHandler(query.queryResults) + } + start(shareQuery) + + return keepRunning ? shareQuery : nil + } + + return nil + } +} diff --git a/ownCloud/SDK Extensions/OCItem+Extension.swift b/ownCloud/SDK Extensions/OCItem+Extension.swift index 19e02e08a..315abf0e1 100644 --- a/ownCloud/SDK Extensions/OCItem+Extension.swift +++ b/ownCloud/SDK Extensions/OCItem+Extension.swift @@ -191,7 +191,7 @@ extension OCItem { return iconName } - func iconName() -> String? { + var iconName : String? { var iconName = OCItem.iconName(for: self.mimeType) if iconName == nil { @@ -206,7 +206,7 @@ extension OCItem { } func icon(fitInSize: CGSize) -> UIImage? { - if let iconName = self.iconName() { + if let iconName = self.iconName { return Theme.shared.image(for: iconName, size: fitInSize) } @@ -246,6 +246,55 @@ extension OCItem { return dateFormatter }() + var sharedByPublicLink : Bool { + if self.shareTypesMask.contains(.link) { + return true + } + return false + } + + var isShared : Bool { + if self.shareTypesMask.isEmpty { + return false + } + return true + } + + var sharedByUserOrGroup : Bool { + if self.shareTypesMask.contains(.userShare) || self.shareTypesMask.contains(.groupShare) || self.shareTypesMask.contains(.remote) { + return true + } + return false + } + + func shareRootItem(from core: OCCore) -> OCItem? { + var shareRootItem : OCItem? + + if self.isSharedWithUser { + var parentItem : OCItem? = self + + shareRootItem = self + + repeat { + parentItem = parentItem?.parentItem(from: core) + + if parentItem != nil, parentItem?.isSharedWithUser == true { + shareRootItem = parentItem + } + } while ((parentItem != nil) && (parentItem?.isSharedWithUser == true)) + } + + return shareRootItem + } + + func isShareRootItem(from core: OCCore) -> Bool { + if let shareRootItem = shareRootItem(from: core) { + return shareRootItem.localID == localID + } + + return false + } + func parentItem(from core: OCCore, completionHandler: ((_ error: Error?, _ parentItem: OCItem?) -> Void)? = nil) -> OCItem? { var parentItem : OCItem? @@ -275,4 +324,31 @@ extension OCItem { return parentItem } + + func displaysDifferent(than item: OCItem?) -> Bool { + if item == nil { + return true + } + + return ( + // Different item + (item?.localID != localID) || + + // File contents (and therefore likely metadata) differs + (item?.itemVersionIdentifier != itemVersionIdentifier) || + + // File name differs + (item?.name != name) || + + // Upload/Download status differs + (item?.syncActivity != syncActivity) || + + // Cloud status differs + (item?.cloudStatus != cloudStatus) || + + // Sharing attributes differ + (item?.shareTypesMask != shareTypesMask) || + (item?.permissions != permissions) // these contain sharing info, too + ) + } } diff --git a/ownCloud/SDK Extensions/OCShare+Extension.swift b/ownCloud/SDK Extensions/OCShare+Extension.swift new file mode 100644 index 000000000..31792b768 --- /dev/null +++ b/ownCloud/SDK Extensions/OCShare+Extension.swift @@ -0,0 +1,70 @@ +// +// OCItem+Extension.swift +// ownCloud +// +// Created by Felix Schwarz on 13.04.18. +// Copyright © 2018 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +extension OCShare { + + func permissionDescription(for capabilities: OCCapabilities?) -> String { + var permissionsDescription : [String] = [] + + if self.type == .link { + if self.canRead { + permissionsDescription.append("Download / View".localized) + } + if self.canReadWrite { + permissionsDescription.append("Create".localized) + } + if self.canUpdate { + permissionsDescription.append("Upload".localized) + permissionsDescription.append("Edit".localized) + } + if self.canDelete { + permissionsDescription.append("Delete".localized) + } + if self.canCreate, self.canUpdate == false { + permissionsDescription.append("Upload (File Drop)".localized) + } + if self.expirationDate != nil { + permissionsDescription.append("Expiration date".localized) + } + if self.protectedByPassword { + permissionsDescription.append("Password".localized) + } + } else { + if self.canRead { + permissionsDescription.append("Read".localized) + } + if self.canShare, capabilities?.sharingResharing == true, capabilities?.sharingAPIEnabled == true, capabilities?.sharingAllowed == true { + permissionsDescription.append("Share".localized) + } + if self.canCreate { + permissionsDescription.append("Create".localized) + } + if self.canUpdate { + permissionsDescription.append("Change".localized) + } + if self.canDelete { + permissionsDescription.append("Delete".localized) + } + } + + return permissionsDescription.joined(separator:", ") + } +} diff --git a/ownCloud/Theming/ThemeCollection.swift b/ownCloud/Theming/ThemeCollection.swift index e0ae6b17d..062561874 100644 --- a/ownCloud/Theming/ThemeCollection.swift +++ b/ownCloud/Theming/ThemeCollection.swift @@ -114,9 +114,16 @@ class ThemeCollection : NSObject { // MARK: - Progress @objc var progressColors : ThemeColorPair + // MARK: - Activity View + @objc var activityIndicatorViewStyle : UIActivityIndicatorView.Style + @objc var searchBarActivityIndicatorViewStyle : UIActivityIndicatorView.Style + // MARK: - Icon colors @objc var iconColors : [String:String] + @objc var favoriteEnabledColor : UIColor? + @objc var favoriteDisabledColor : UIColor? + // MARK: - Default Collection static var defaultCollection : ThemeCollection = { let collection = ThemeCollection() @@ -195,6 +202,9 @@ class ThemeCollection : NSObject { filledColorPairCollection: ThemeColorPairCollection(fromPair: ThemeColorPair(foreground: UIColor.white, background: lightBrandColor)) ) + self.favoriteEnabledColor = UIColor(hex: 0xFFCC00) + self.favoriteDisabledColor = UIColor(hex: 0x7C7C7C) + // Styles switch style { case .dark: @@ -232,6 +242,10 @@ class ThemeCollection : NSObject { // Progress self.progressColors = ThemeColorPair(foreground: self.lightBrandColor, background: self.lightBrandColor.withAlphaComponent(0.3)) + // Activity + self.activityIndicatorViewStyle = .white + self.searchBarActivityIndicatorViewStyle = .white + // Logo fill color logoFillColor = UIColor.white @@ -255,6 +269,10 @@ class ThemeCollection : NSObject { // Progress self.progressColors = ThemeColorPair(foreground: self.lightBrandColor, background: UIColor.lightGray.withAlphaComponent(0.3)) + // Activity + self.activityIndicatorViewStyle = .gray + self.searchBarActivityIndicatorViewStyle = .gray + // Logo fill color logoFillColor = UIColor.lightGray @@ -270,6 +288,10 @@ class ThemeCollection : NSObject { // Progress self.progressColors = ThemeColorPair(foreground: self.lightBrandColor, background: UIColor.lightGray.withAlphaComponent(0.3)) + // Activity + self.activityIndicatorViewStyle = .gray + self.searchBarActivityIndicatorViewStyle = .white + // Logo fill color logoFillColor = UIColor.lightGray } diff --git a/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift b/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift index 61c70d8dd..8a94c8fa8 100644 --- a/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift +++ b/ownCloud/UI Elements/Card Presentation Controller/CardPresentationController.swift @@ -192,12 +192,18 @@ final class CardPresentationController: UIPresentationController { // MARK: - Layout private var animationOnGoing = false + override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { + cachedFittingSize = nil + } + override func containerViewWillLayoutSubviews() { if cardPanGestureRecognizer?.state != .began, cardPanGestureRecognizer?.state != .changed, !animationOnGoing { let presentedViewFrame = frameOfPresentedViewInContainerView - presentedView?.frame = presentedViewFrame - dimmingView.frame = windowFrame + UIView.animate(withDuration: 0.15, animations: { + self.presentedView?.frame = presentedViewFrame + }) + self.dimmingView.frame = self.windowFrame if let moreViewController = presentedViewController as? MoreViewController, let fittingSize = presentedViewFittingSize { diff --git a/ownCloud/UI Elements/MessageView.swift b/ownCloud/UI Elements/MessageView.swift new file mode 100644 index 000000000..d99f627b2 --- /dev/null +++ b/ownCloud/UI Elements/MessageView.swift @@ -0,0 +1,247 @@ +// +// MessageView.swift +// ownCloud +// +// Created by Matthias Hühne on 23.04.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class MessageView: UIView { + + var masterView : UIView + var messageView : UIView? + var messageContainerView : UIView? + var messageImageView : VectorImageView? + var messageTitleLabel : UILabel? + var messageMessageLabel : UILabel? + var messageThemeApplierToken : ThemeApplierToken? + var composeViewBottomConstraint: NSLayoutConstraint! + private var compactConstraints: [NSLayoutConstraint] = [] + private var regularConstraints: [NSLayoutConstraint] = [] + var keyboardHeight : CGFloat = 0 + + init(add to: UIView) { + masterView = to + super.init(frame: to.frame) + + // Observe keyboard change + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.rotated), name: UIDevice.orientationDidChangeNotification, object: nil) + } + + required init?(coder aDecoder: NSCoder) { + + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) + if messageThemeApplierToken != nil { + Theme.shared.remove(applierForToken: messageThemeApplierToken) + messageThemeApplierToken = nil + } + } + + func message(show: Bool, imageName : String? = nil, title : String? = nil, message : String? = nil) { + if !show { + if messageView?.superview != nil { + messageView?.removeFromSuperview() + } + if !show { + return + } + } + + if messageView == nil { + var rootView : UIView + var containerView : UIView + var imageView : VectorImageView + var titleLabel : UILabel + var messageLabel : UILabel + + rootView = UIView() + rootView.translatesAutoresizingMaskIntoConstraints = false + + containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + + imageView = VectorImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + + titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + messageLabel = UILabel() + messageLabel.translatesAutoresizingMaskIntoConstraints = false + messageLabel.numberOfLines = 0 + messageLabel.textAlignment = .center + + containerView.addSubview(imageView) + containerView.addSubview(titleLabel) + containerView.addSubview(messageLabel) + + rootView.addSubview(containerView) + + regularConstraints.append(contentsOf: [ + imageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + imageView.bottomAnchor.constraint(equalTo: containerView.centerYAnchor), + imageView.widthAnchor.constraint(equalToConstant: 96), + imageView.heightAnchor.constraint(equalToConstant: 96), + + titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + titleLabel.leftAnchor.constraint(greaterThanOrEqualTo: containerView.leftAnchor), + titleLabel.rightAnchor.constraint(lessThanOrEqualTo: containerView.rightAnchor), + titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20), + + messageLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + messageLabel.leftAnchor.constraint(greaterThanOrEqualTo: containerView.leftAnchor), + messageLabel.rightAnchor.constraint(lessThanOrEqualTo: containerView.rightAnchor), + messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), + + containerView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: rootView.centerYAnchor), + + containerView.leftAnchor.constraint(greaterThanOrEqualTo: rootView.leftAnchor, constant: 20), + containerView.rightAnchor.constraint(lessThanOrEqualTo: rootView.rightAnchor, constant: -20), + containerView.topAnchor.constraint(greaterThanOrEqualTo: rootView.topAnchor, constant: 20), + containerView.bottomAnchor.constraint(lessThanOrEqualTo: rootView.bottomAnchor, constant: -20) + ]) + + compactConstraints.append(contentsOf: [ + imageView.leftAnchor.constraint(equalTo: containerView.leftAnchor), + imageView.widthAnchor.constraint(equalToConstant: 96), + imageView.heightAnchor.constraint(equalToConstant: 96), + imageView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor), + + titleLabel.leftAnchor.constraint(equalTo: imageView.rightAnchor, constant: 20), + titleLabel.rightAnchor.constraint(equalTo: containerView.rightAnchor), + titleLabel.topAnchor.constraint(equalTo: imageView.topAnchor), + + messageLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + messageLabel.rightAnchor.constraint(equalTo: containerView.rightAnchor), + messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), + + containerView.centerXAnchor.constraint(equalTo: rootView.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: rootView.centerYAnchor), + + containerView.leftAnchor.constraint(greaterThanOrEqualTo: rootView.leftAnchor, constant: 20), + containerView.rightAnchor.constraint(lessThanOrEqualTo: rootView.rightAnchor, constant: -20), + containerView.topAnchor.constraint(greaterThanOrEqualTo: rootView.topAnchor, constant: 20), + containerView.bottomAnchor.constraint(lessThanOrEqualTo: rootView.bottomAnchor, constant: -20) + ]) + + render() + + messageView = rootView + messageContainerView = containerView + messageImageView = imageView + messageTitleLabel = titleLabel + messageMessageLabel = messageLabel + + messageThemeApplierToken = Theme.shared.add(applier: { [weak self] (_, collection, _) in + self?.messageView?.backgroundColor = collection.tableBackgroundColor + + self?.messageTitleLabel?.applyThemeCollection(collection, itemStyle: .bigTitle) + self?.messageMessageLabel?.applyThemeCollection(collection, itemStyle: .bigMessage) + }) + } + + if messageView?.superview == nil { + if let rootView = self.messageView, let containerView = self.messageContainerView { + containerView.alpha = 0 + containerView.transform = CGAffineTransform(translationX: 0, y: 15) + + rootView.alpha = 0 + + self.masterView.addSubview(rootView) + + self.composeViewBottomConstraint = rootView.bottomAnchor.constraint(equalTo: self.masterView.safeAreaLayoutGuide.bottomAnchor) + if keyboardHeight > 0 { + self.composeViewBottomConstraint.constant = self.masterView.safeAreaInsets.bottom - keyboardHeight + } + + NSLayoutConstraint.activate([ + rootView.leftAnchor.constraint(equalTo: self.masterView.leftAnchor), + rootView.widthAnchor.constraint(equalTo: self.masterView.widthAnchor), + rootView.topAnchor.constraint(equalTo: self.masterView.safeAreaLayoutGuide.topAnchor), + self.composeViewBottomConstraint + ]) + + UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseOut, animations: { + rootView.alpha = 1 + }, completion: { (_) in + UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseOut, animations: { + containerView.alpha = 1 + containerView.transform = CGAffineTransform.identity + }) + }) + } + } + + if imageName != nil { + messageImageView?.vectorImage = Theme.shared.tvgImage(for: imageName!) + } + if title != nil { + messageTitleLabel?.text = title! + } + if message != nil { + messageMessageLabel?.text = message! + } + } + + @objc func rotated() { + render() + } + + func render() { + switch UIScreen.main.traitCollection.verticalSizeClass { + case .regular: + NSLayoutConstraint.deactivate(compactConstraints) + NSLayoutConstraint.activate(regularConstraints) + case .compact: + NSLayoutConstraint.deactivate(regularConstraints) + NSLayoutConstraint.activate(compactConstraints) + default: + break + } + } + + @objc func keyboardWillShow(notification: Notification) { + let keyboardSize = (notification.userInfo? [UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue + keyboardHeight = keyboardSize?.height ?? 0 + + if self.composeViewBottomConstraint != nil { + self.composeViewBottomConstraint.constant = self.masterView.safeAreaInsets.bottom - keyboardHeight + + UIView.animate(withDuration: 0.5) { + self.masterView.layoutIfNeeded() + } + } + } + + @objc func keyboardWillHide(notification: Notification) { + if self.composeViewBottomConstraint != nil { + self.composeViewBottomConstraint.constant = 0 + + UIView.animate(withDuration: 0.5) { + self.masterView.layoutIfNeeded() + } + } + } + +} diff --git a/ownCloud/UI Elements/RoundedLabel.swift b/ownCloud/UI Elements/RoundedLabel.swift new file mode 100644 index 000000000..d50da350c --- /dev/null +++ b/ownCloud/UI Elements/RoundedLabel.swift @@ -0,0 +1,98 @@ +// +// RoundedLabel.swift +// ownCloud +// +// Created by Matthias Hühne on 13.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 + +class RoundedLabel: UIView { + + // MARK: - Constants + private let cornerRadius : CGFloat = 5.0 + private let borderWidth : CGFloat = 1.0 + private let horizontalPadding : CGFloat = 10.0 + private let verticalPadding : CGFloat = 20.0 + private let verticalLabelPadding : CGFloat = 5.0 + + // MARK: - Instance Variables + private var label = UILabel() + private var font : UIFont = UIFont.boldSystemFont(ofSize: 14) + public var labelText : String = "" { + didSet { + label.text = labelText + } + } + public var mainColor : UIColor = UIColor.black { + didSet { + self.backgroundColor = mainColor + } + } + public var textColor : UIColor = UIColor.white { + didSet { + label.textColor = textColor + } + } + + // MARK: - Init & Deinit + + init() { + super.init(frame: CGRect.zero) + styleView() + } + + init(text: String, textColor: UIColor, backgroundColor: UIColor) { + super.init(frame: CGRect.zero) + labelText = text + self.textColor = textColor + mainColor = backgroundColor + styleView() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Prepare and Update View + + private func styleView() { + self.layer.cornerRadius = cornerRadius + self.backgroundColor = mainColor + + label.textAlignment = .center + label.font = font + label.numberOfLines = 1 + label.text = labelText + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = mainColor + self.addSubview(label) + + NSLayoutConstraint.activate([ + label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: horizontalPadding), + label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -horizontalPadding), + label.topAnchor.constraint(equalTo: self.topAnchor), + label.bottomAnchor.constraint(equalTo: self.bottomAnchor), + label.widthAnchor.constraint(greaterThanOrEqualToConstant: 0), + label.heightAnchor.constraint(greaterThanOrEqualToConstant: 0) + ]) + } + + public func update(text: String, textColor: UIColor, backgroundColor: UIColor) { + labelText = text + mainColor = backgroundColor + self.textColor = textColor + } + +} diff --git a/ownCloud/UI Elements/StaticTableViewController.swift b/ownCloud/UI Elements/StaticTableViewController.swift index 17ff3a1e7..c1f41d138 100644 --- a/ownCloud/UI Elements/StaticTableViewController.swift +++ b/ownCloud/UI Elements/StaticTableViewController.swift @@ -46,7 +46,7 @@ class StaticTableViewController: UITableViewController, Themeable { if animated { tableView.performBatchUpdates({ sections.insert(section, at: index) - tableView.insertSections(IndexSet(integer: index), with: UITableView.RowAnimation.fade) + tableView.insertSections(IndexSet(integer: index), with: .fade) }) } else { sections.insert(section, at: index) @@ -58,19 +58,23 @@ class StaticTableViewController: UITableViewController, Themeable { func removeSection(_ section: StaticTableViewSection, animated: Bool = false) { if animated { tableView.performBatchUpdates({ - if let index : Int = sections.index(of: section) { + if let index = sections.index(of: section) { sections.remove(at: index) - tableView.deleteSections(IndexSet(integer: index), with: UITableView.RowAnimation.fade) + tableView.deleteSections(IndexSet(integer: index), with: .fade) } }, completion: { (_) in section.viewController = nil }) } else { - sections.remove(at: sections.index(of: section)!) + if let sectionIndex = sections.index(of: section) { + sections.remove(at: sectionIndex) - section.viewController = nil + section.viewController = nil - tableView.reloadData() + tableView.reloadData() + } else { + section.viewController = nil + } } } @@ -108,7 +112,7 @@ class StaticTableViewController: UITableViewController, Themeable { } } - tableView.deleteSections(removalIndexes, with: UITableView.RowAnimation.fade) + tableView.deleteSections(removalIndexes, with: .fade) }, completion: { (_) in for section in removeSections { section.viewController = nil @@ -208,6 +212,15 @@ class StaticTableViewController: UITableViewController, Themeable { return sections[section].footerTitle } + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let cell = staticRowForIndexPath(indexPath) + if cell.type == .datePicker { + return 216.0 + } + + return UITableView.automaticDimension + } + // MARK: - Theme support func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { self.tableView.backgroundColor = collection.tableGroupBackgroundColor diff --git a/ownCloud/UI Elements/StaticTableViewRow.swift b/ownCloud/UI Elements/StaticTableViewRow.swift index 493e68441..4b1edfabb 100644 --- a/ownCloud/UI Elements/StaticTableViewRow.swift +++ b/ownCloud/UI Elements/StaticTableViewRow.swift @@ -19,6 +19,7 @@ import UIKit typealias StaticTableViewRowAction = (_ staticRow : StaticTableViewRow, _ sender: Any?) -> Void +typealias StaticTableViewRowTextAction = (_ staticRow : StaticTableViewRow, _ sender: Any?, _ type: StaticTableViewRowActionType) -> Void typealias StaticTableViewRowEventHandler = (_ staticRow : StaticTableViewRow, _ event : StaticTableViewEvent) -> Void enum StaticTableViewRowButtonStyle { @@ -29,12 +30,33 @@ enum StaticTableViewRowButtonStyle { case custom(textColor: UIColor?, selectedTextColor: UIColor?, backgroundColor: UIColor?, selectedBackgroundColor: UIColor?) } +enum StaticTableViewRowType { + case row + case subtitleRow + case valueRow + case radio + case toggle + case text + case secureText + case label + case switchButton + case button + case datePicker +} + +enum StaticTableViewRowActionType { + case changed + case didBegin + case didEnd +} + class StaticTableViewRow : NSObject, UITextFieldDelegate { public weak var section : StaticTableViewSection? public var identifier : String? public var groupIdentifier : String? + public var type : StaticTableViewRowType public var value : Any? { didSet { @@ -60,6 +82,7 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { } public var action : StaticTableViewRowAction? + public var textFieldAction : StaticTableViewRowTextAction? public var eventHandler : StaticTableViewRowEventHandler? public var viewController: StaticTableViewController? { @@ -84,30 +107,105 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { return self.index != nil } + var representedObject : Any? + public var additionalAccessoryView : UIView? override init() { + type = .row super.init() } - convenience init(rowWithAction: StaticTableViewRowAction?, title: String, alignment: NSTextAlignment = .left, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, identifier : String? = nil, image : UIImage? = nil) { + convenience init(rowWithAction: StaticTableViewRowAction?, title: String, subtitle: String? = nil, image: UIImage? = nil, imageWidth: CGFloat? = nil, alignment: NSTextAlignment = .left, accessoryType: UITableViewCell.AccessoryType = .none, identifier : String? = nil, accessoryView: UIView? = nil) { self.init() + type = .row + + var image = image + if image != nil, imageWidth != nil { + image = image?.paddedTo(width: imageWidth) + } self.identifier = identifier + var cellStyle = UITableViewCell.CellStyle.default + if subtitle != nil { + cellStyle = UITableViewCell.CellStyle.subtitle + } - self.cell = ThemeTableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) + self.cell = ThemeTableViewCell(style: cellStyle, reuseIdentifier: nil) + if subtitle != nil { + self.cell?.detailTextLabel?.text = subtitle + self.cell?.detailTextLabel?.numberOfLines = 0 + } self.cell?.textLabel?.text = title self.cell?.textLabel?.textAlignment = alignment self.cell?.accessoryType = accessoryType self.cell?.imageView?.image = image + if accessoryView != nil { + self.cell?.accessoryView = accessoryView + } + + themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in + self?.cell?.imageView?.tintColor = themeCollection.tableRowColors.labelColor + self?.cell?.accessoryView?.tintColor = themeCollection.tableRowColors.labelColor + }) self.cell?.accessibilityIdentifier = identifier - self.action = rowWithAction + if rowWithAction != nil { + self.action = rowWithAction + } else { + self.cell?.selectionStyle = .none + } } - convenience init(rowWithAction: StaticTableViewRowAction?, title: String, alignment: NSTextAlignment = .left, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, accessoryView: UIView, identifier: String? = nil) { + convenience init(rowWithAction: StaticTableViewRowAction?, title: String, alignment: NSTextAlignment = .left, accessoryView: UIView? = nil, identifier : String? = nil) { self.init() + type = .row + + self.identifier = identifier + + self.cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) + self.cell?.textLabel?.text = title + self.cell?.textLabel?.textAlignment = alignment + self.cell?.accessoryView = accessoryView + self.cell?.accessibilityIdentifier = identifier + + themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in + var textColor, selectedTextColor, backgroundColor, selectedBackgroundColor : UIColor? + + textColor = themeCollection.tintColor + backgroundColor = themeCollection.tableRowColors.backgroundColor + + self?.cell?.textLabel?.textColor = textColor + + if selectedTextColor != nil { + self?.cell?.textLabel?.highlightedTextColor = selectedTextColor + } + + if backgroundColor != nil { + + self?.cell?.backgroundColor = backgroundColor + } + + if selectedBackgroundColor != nil { + let selectedBackgroundView = UIView() + + selectedBackgroundView.backgroundColor = selectedBackgroundColor + + self?.cell?.selectedBackgroundView? = selectedBackgroundView + } + }, applyImmediately: true) + + if rowWithAction != nil { + self.action = rowWithAction + } else { + self.cell?.selectionStyle = .none + } + } + + convenience init(rowWithAction: StaticTableViewRowAction?, title: String, alignment: NSTextAlignment = .left, image: UIImage? = nil, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, accessoryView: UIView?, identifier: String? = nil) { + self.init() + type = .row self.identifier = identifier @@ -118,24 +216,37 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { cell.textLabel?.text = title cell.textLabel?.textAlignment = alignment cell.accessoryType = accessoryType + self.cell?.imageView?.image = image cell.accessibilityIdentifier = identifier - self.action = rowWithAction - - additionalAccessoryView = accessoryView - guard let additionalAccessoryView = additionalAccessoryView else { return } - cell.contentView.addSubview(additionalAccessoryView) - additionalAccessoryView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - additionalAccessoryView.trailingAnchor.constraint(equalTo: cell.accessoryView?.leadingAnchor ?? cell.contentView.trailingAnchor, constant: -5.0), - additionalAccessoryView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0), - additionalAccessoryView.heightAnchor.constraint(equalToConstant: 24.0), - additionalAccessoryView.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor) + if rowWithAction != nil { + self.action = rowWithAction + } else { + self.cell?.selectionStyle = .none + } + + if let accessoryView = accessoryView { + cell.textLabel?.numberOfLines = 0 + additionalAccessoryView = accessoryView + + cell.contentView.addSubview(accessoryView) + accessoryView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + accessoryView.trailingAnchor.constraint(equalTo: cell.accessoryView?.leadingAnchor ?? cell.contentView.trailingAnchor, constant: -5.0), + accessoryView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0), + accessoryView.heightAnchor.constraint(equalToConstant: 24.0), + accessoryView.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor) ]) + } + + themeApplierToken = Theme.shared.add(applier: { [weak self] (_, themeCollection, _) in + self?.cell?.imageView?.tintColor = themeCollection.tableRowColors.labelColor + }) } convenience init(subtitleRowWithAction: StaticTableViewRowAction?, title: String, subtitle: String? = nil, style : UITableViewCell.CellStyle = .subtitle, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, identifier : String? = nil) { self.init() + type = .subtitleRow self.identifier = identifier @@ -157,16 +268,26 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { convenience init(valueRowWithAction: StaticTableViewRowAction?, title: String, value: String, accessoryType: UITableViewCell.AccessoryType = UITableViewCell.AccessoryType.none, identifier : String? = nil) { self.init(subtitleRowWithAction: valueRowWithAction, title: title, subtitle: value, style: .value1, accessoryType: accessoryType, identifier: identifier) + type = .valueRow } // MARK: - Radio Item - convenience init(radioItemWithAction: StaticTableViewRowAction?, groupIdentifier: String, value: Any, title: String, selected: Bool, identifier : String? = nil) { + convenience init(radioItemWithAction: StaticTableViewRowAction?, groupIdentifier: String, value: Any, title: String, subtitle: String? = nil, selected: Bool, identifier : String? = nil) { self.init() + type = .radio + var tableViewStyle = UITableViewCell.CellStyle.default self.identifier = identifier + if subtitle != nil { + tableViewStyle = UITableViewCell.CellStyle.subtitle + } - self.cell = ThemeTableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) + self.cell = ThemeTableViewCell(style: tableViewStyle, reuseIdentifier: nil) self.cell?.textLabel?.text = title + if subtitle != nil { + self.cell?.detailTextLabel?.text = subtitle + self.cell?.detailTextLabel?.numberOfLines = 0 + } if let accessibilityIdentifier : String = identifier { self.cell?.accessibilityIdentifier = groupIdentifier + "." + accessibilityIdentifier @@ -185,19 +306,63 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { } } + // MARK: - Toggle Item + convenience init(toggleItemWithAction: StaticTableViewRowAction?, title: String, subtitle: String? = nil, selected: Bool, identifier : String? = nil) { + self.init() + type = .toggle + + var tableViewStyle = UITableViewCell.CellStyle.default + self.identifier = identifier + if subtitle != nil { + tableViewStyle = UITableViewCell.CellStyle.subtitle + } + + self.cell = ThemeTableViewCell(style: tableViewStyle, reuseIdentifier: nil) + self.cell?.textLabel?.text = title + if subtitle != nil { + self.cell?.detailTextLabel?.text = subtitle + self.cell?.detailTextLabel?.numberOfLines = 0 + } + + if let accessibilityIdentifier : String = identifier { + self.cell?.accessibilityIdentifier = accessibilityIdentifier + } + + if selected { + self.cell?.accessoryType = UITableViewCell.AccessoryType.checkmark + self.value = true + } else { + self.value = false + } + + self.action = { (row, sender) in + + guard let value = row.value as? Bool else { return } + if value { + row.cell?.accessoryType = UITableViewCell.AccessoryType.none + row.value = false + } else { + row.cell?.accessoryType = UITableViewCell.AccessoryType.checkmark + row.value = true + } + + toggleItemWithAction?(row, sender) + } + } + // MARK: - Text Field public var textField : UITextField? - convenience init(textFieldWithAction action: StaticTableViewRowAction?, placeholder placeholderString: String = "", value textValue: String = "", secureTextEntry : Bool = false, keyboardType: UIKeyboardType = UIKeyboardType.default, autocorrectionType: UITextAutocorrectionType = UITextAutocorrectionType.default, autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.none, enablesReturnKeyAutomatically: Bool = true, returnKeyType : UIReturnKeyType = UIReturnKeyType.default, identifier : String? = nil, accessibilityLabel: String? = nil) { - + convenience init(textFieldWithAction action: StaticTableViewRowTextAction?, placeholder placeholderString: String = "", value textValue: String = "", secureTextEntry : Bool = false, keyboardType: UIKeyboardType = UIKeyboardType.default, autocorrectionType: UITextAutocorrectionType = UITextAutocorrectionType.default, autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.none, enablesReturnKeyAutomatically: Bool = true, returnKeyType : UIReturnKeyType = UIReturnKeyType.default, inputAccessoryView : UIView? = nil, identifier : String? = nil, accessibilityLabel: String? = nil, actionEvent: UIControl.Event = UIControl.Event.editingChanged) { self.init() + type = .text self.identifier = identifier self.cell = ThemeTableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) self.cell?.selectionStyle = UITableViewCell.SelectionStyle.none - self.action = action + self.textFieldAction = action self.value = textValue let cellTextField : UITextField = UITextField() @@ -212,10 +377,11 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { cellTextField.autocapitalizationType = autocapitalizationType cellTextField.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically cellTextField.returnKeyType = returnKeyType + cellTextField.inputAccessoryView = inputAccessoryView cellTextField.text = textValue cellTextField.accessibilityIdentifier = identifier - cellTextField.addTarget(self, action: #selector(textFieldContentChanged(_:)), for: UIControl.Event.editingChanged) + cellTextField.addTarget(self, action: #selector(textFieldContentChanged(_:)), for: actionEvent) if cell != nil { cell?.contentView.addSubview(cellTextField) @@ -244,7 +410,7 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { cellTextField.accessibilityLabel = accessibilityLabel } - convenience init(secureTextFieldWithAction action: StaticTableViewRowAction?, placeholder placeholderString: String = "", value textValue: String = "", keyboardType: UIKeyboardType = UIKeyboardType.default, autocorrectionType: UITextAutocorrectionType = UITextAutocorrectionType.default, autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.none, enablesReturnKeyAutomatically: Bool = true, returnKeyType : UIReturnKeyType = UIReturnKeyType.default, identifier : String? = nil, accessibilityLabel: String? = nil) { + convenience init(secureTextFieldWithAction action: StaticTableViewRowTextAction?, placeholder placeholderString: String = "", value textValue: String = "", keyboardType: UIKeyboardType = UIKeyboardType.default, autocorrectionType: UITextAutocorrectionType = UITextAutocorrectionType.default, autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.none, enablesReturnKeyAutomatically: Bool = true, returnKeyType : UIReturnKeyType = UIReturnKeyType.default, inputAccessoryView : UIView? = nil, identifier : String? = nil, accessibilityLabel: String? = nil, actionEvent: UIControl.Event = UIControl.Event.editingChanged) { self.init( textFieldWithAction: action, placeholder: placeholderString, value: textValue, secureTextEntry: true, @@ -253,14 +419,25 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { autocapitalizationType: autocapitalizationType, enablesReturnKeyAutomatically: enablesReturnKeyAutomatically, returnKeyType: returnKeyType, + inputAccessoryView: inputAccessoryView, identifier : identifier, - accessibilityLabel: accessibilityLabel) + accessibilityLabel: accessibilityLabel, + actionEvent: actionEvent) + type = .secureText } @objc func textFieldContentChanged(_ sender: UITextField) { self.value = sender.text - self.action?(self, sender) + self.textFieldAction?(self, sender, .changed) + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + self.textFieldAction?(self, textField, .didBegin) + } + + func textFieldDidEndEditing(_ textField: UITextField) { + self.textFieldAction?(self, textField, .didEnd) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { @@ -272,6 +449,7 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { // MARK: - Labels convenience init(label: String, identifier: String? = nil) { self.init() + type = .label self.identifier = identifier @@ -292,6 +470,7 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { // MARK: - Switches convenience init(switchWithAction action: StaticTableViewRowAction?, title: String, value switchValue: Bool = false, identifier: String? = nil) { self.init() + type = .switchButton self.identifier = identifier @@ -330,15 +509,24 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { // MARK: - Buttons - convenience init(buttonWithAction action: StaticTableViewRowAction?, title: String, style: StaticTableViewRowButtonStyle = StaticTableViewRowButtonStyle.proceed, identifier : String? = nil) { - + convenience init(buttonWithAction action: StaticTableViewRowAction?, title: String, style: StaticTableViewRowButtonStyle = .proceed, image: UIImage? = nil, imageWidth : CGFloat? = nil, alignment: NSTextAlignment = .center, identifier : String? = nil, accessoryView: UIView? = nil) { self.init() + type = .button self.identifier = identifier + var image = image + if image != nil, imageWidth != nil { + image = image?.paddedTo(width: imageWidth) + } + self.cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) self.cell?.textLabel?.text = title - self.cell?.textLabel?.textAlignment = NSTextAlignment.center + self.cell?.textLabel?.textAlignment = alignment + self.cell?.imageView?.image = image + if accessoryView != nil { + self.cell?.accessoryView = accessoryView + } self.cell?.accessibilityIdentifier = identifier @@ -371,6 +559,8 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { } self?.cell?.textLabel?.textColor = textColor + self?.cell?.imageView?.tintColor = textColor + self?.cell?.accessoryView?.tintColor = textColor if selectedTextColor != nil { @@ -394,7 +584,46 @@ class StaticTableViewRow : NSObject, UITextFieldDelegate { self.action = action } + // MARK: - Date Picker + + convenience init(datePickerWithAction action: StaticTableViewRowAction?, date dateValue: Date, identifier: String? = nil) { + self.init() + type = .datePicker + + self.identifier = identifier + + let datePickerView = UIDatePicker() + datePickerView.date = dateValue + datePickerView.datePickerMode = .date + datePickerView.minimumDate = Date() + datePickerView.accessibilityIdentifier = identifier + datePickerView.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: UIControl.Event.valueChanged) + datePickerView.translatesAutoresizingMaskIntoConstraints = false + datePickerView.setValue(Theme.shared.activeCollection.tableRowColors.labelColor, forKey: "textColor") + + self.cell = ThemeTableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: nil) + self.cell?.selectionStyle = .none + self.cell?.addSubview(datePickerView) + + self.value = dateValue + self.action = action + + if let cell = self.cell { + NSLayoutConstraint.activate([ + datePickerView.leftAnchor.constraint(equalTo: cell.safeAreaLayoutGuide.leftAnchor), + datePickerView.rightAnchor.constraint(equalTo: cell.safeAreaLayoutGuide.rightAnchor), + datePickerView.topAnchor.constraint(equalTo: cell.topAnchor), + datePickerView.heightAnchor.constraint(equalToConstant: 216.0) + ]) + } + } + + @objc func datePickerValueChanged(_ sender: UIDatePicker) { + self.action?(self, sender) + } + // MARK: - Deinit + deinit { if themeApplierToken != nil { Theme.shared.remove(applierForToken: themeApplierToken) diff --git a/ownCloud/UI Elements/StaticTableViewSection.swift b/ownCloud/UI Elements/StaticTableViewSection.swift index 7b5da06f8..168ad885c 100644 --- a/ownCloud/UI Elements/StaticTableViewSection.swift +++ b/ownCloud/UI Elements/StaticTableViewSection.swift @@ -89,7 +89,7 @@ class StaticTableViewSection: NSObject { if let selectedValueObject = selectedValue as? NSObject, let valueObject = value as? NSObject, (selectedValueObject == valueObject) { selected = true } - radioGroupRows.append(StaticTableViewRow(radioItemWithAction: radioAction, groupIdentifier: groupIdentifier, value: value, title: label, selected: selected)) + radioGroupRows.append(StaticTableViewRow(radioItemWithAction: radioAction, groupIdentifier: groupIdentifier, value: value, title: label, subtitle: "", selected: selected)) } } @@ -147,6 +147,12 @@ class StaticTableViewSection: NSObject { } } + func remove(rowWithIdentifier identifier: String, animated : Bool = false) { + if let row = row(withIdentifier: identifier) { + self.remove(rows: [row], animated: animated) + } + } + // MARK: - Update Section Titles func updateHeader(title: String?) { self.headerTitle = title diff --git a/ownCloud/UIKit Extensions/Array+Extension.swift b/ownCloud/UIKit Extensions/Array+Extension.swift new file mode 100644 index 000000000..3bc21fcdf --- /dev/null +++ b/ownCloud/UIKit Extensions/Array+Extension.swift @@ -0,0 +1,34 @@ +// +// Array+Extension.swift +// ownCloud +// +// Created by Matthias Hühne on 15.05.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, 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 Foundation + +extension Array { + func unique(map: ((Element) -> (T))) -> [Element] { + var set = Set() + var arrayOrdered = [Element]() + for value in self { + if !set.contains(map(value)) { + set.insert(map(value)) + arrayOrdered.append(value) + } + } + + return arrayOrdered + } +} diff --git a/ownCloud/UIKit Extensions/UIImage+Extension.swift b/ownCloud/UIKit Extensions/UIImage+Extension.swift index 20a8e2921..b57f11bba 100644 --- a/ownCloud/UIKit Extensions/UIImage+Extension.swift +++ b/ownCloud/UIKit Extensions/UIImage+Extension.swift @@ -44,4 +44,18 @@ extension UIImage { UIRectFillUsingBlendMode(contentRect, operation) }) } + + func paddedTo(width: CGFloat? = nil, height : CGFloat? = nil) -> UIImage? { + let origSize = size + let newSize : CGSize = CGSize(width: width ?? origSize.width, height: height ?? origSize.height) + var image : UIImage? = UIImage.imageWithSize(size: newSize, scale: scale, { (contentRect) in + self.draw(at: CGPoint(x: Int((contentRect.size.width - origSize.width) / 2), y: Int((contentRect.size.height - origSize.height) / 2))) + }) + + if image != nil, image?.renderingMode != self.renderingMode { + image = image?.withRenderingMode(self.renderingMode) + } + + return image + } } diff --git a/ownCloud/UIKit Extensions/UIImagePickerController+Extension.swift b/ownCloud/UIKit Extensions/UIImagePickerController+Extension.swift deleted file mode 100644 index 515a05939..000000000 --- a/ownCloud/UIKit Extensions/UIImagePickerController+Extension.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// UIImagePickerController+Extension.swift -// ownCloud -// -// Created by Pablo Carrascal on 07/11/2018. -// Copyright © 2018 ownCloud GmbH. All rights reserved. -// - -import UIKit -import MobileCoreServices - -extension UIImagePickerController { - - class func regularImagePicker(with sourceType: UIImagePickerController.SourceType) -> UIImagePickerController { - let picker = UIImagePickerController() - picker.sourceType = sourceType - picker.mediaTypes = [kUTTypeMovie as String, kUTTypeImage as String] - picker.navigationBar.isTranslucent = false - picker.navigationBar.barTintColor = Theme.shared.activeCollection.navigationBarColors.backgroundColor - picker.navigationBar.backgroundColor = Theme.shared.activeCollection.navigationBarColors.backgroundColor - picker.navigationBar.tintColor = Theme.shared.activeCollection.navigationBarColors.tintColor - picker.navigationBar.titleTextAttributes = [ .foregroundColor : Theme.shared.activeCollection.navigationBarColors.labelColor ] - - return picker - } -} diff --git a/ownCloud/UIKit Extensions/UISearchBar+Extension.swift b/ownCloud/UIKit Extensions/UISearchBar+Extension.swift new file mode 100644 index 000000000..fce8fb3e1 --- /dev/null +++ b/ownCloud/UIKit Extensions/UISearchBar+Extension.swift @@ -0,0 +1,60 @@ +// +// UISearchBar+Extension.swift +// ownCloud +// +// Created by Matthias Hühne on 30.04.19. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2019, 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 Foundation +import UIKit + +extension UISearchBar { + private var textField: UITextField? { + let subViews = self.subviews.flatMap { $0.subviews } + return (subViews.filter { $0 is UITextField }).first as? UITextField + } + + private var searchIcon: UIImage? { + let subViews = subviews.flatMap { $0.subviews } + return ((subViews.filter { $0 is UIImageView }).first as? UIImageView)?.image + } + + private var activityIndicator: UIActivityIndicatorView? { + return textField?.leftView?.subviews.compactMap { $0 as? UIActivityIndicatorView }.first + } + + var isLoading: Bool { + get { + return activityIndicator != nil + } set { + OnMainThread { + let _searchIcon = self.searchIcon + if newValue { + if self.activityIndicator == nil { + let _activityIndicator = UIActivityIndicatorView(style: Theme.shared.activeCollection.searchBarActivityIndicatorViewStyle) + _activityIndicator.startAnimating() + _activityIndicator.backgroundColor = UIColor.clear + self.setImage(UIImage(), for: .search, state: .normal) + self.textField?.leftView?.addSubview(_activityIndicator) + let leftViewSize = self.textField?.leftView?.frame.size ?? CGSize.zero + _activityIndicator.center = CGPoint(x: leftViewSize.width/2, y: leftViewSize.height/2) + } + } else { + self.setImage(_searchIcon, for: .search, state: .normal) + self.activityIndicator?.removeFromSuperview() + } + } + } + } +} diff --git a/ownCloud/UIKit Extensions/UITableViewController+Extension.swift b/ownCloud/UIKit Extensions/UITableViewController+Extension.swift index 5b47407ab..8240675ce 100644 --- a/ownCloud/UIKit Extensions/UITableViewController+Extension.swift +++ b/ownCloud/UIKit Extensions/UITableViewController+Extension.swift @@ -33,6 +33,7 @@ extension UITableViewController { NSLayoutConstraint.activate([ coloredView.topAnchor.constraint(equalTo: self.tableView.topAnchor, constant: -self.view.frame.size.height), + coloredView.leftAnchor.constraint(equalTo: self.tableView.leftAnchor), coloredView.widthAnchor.constraint(equalTo: self.tableView.widthAnchor), coloredView.heightAnchor.constraint(equalToConstant: self.view.frame.size.height + 1) ]) diff --git a/ownCloudScreenshotsTests/SnapshotHelper.swift b/ownCloudScreenshotsTests/SnapshotHelper.swift index cccec11a9..110a2b74a 100644 --- a/ownCloudScreenshotsTests/SnapshotHelper.swift +++ b/ownCloudScreenshotsTests/SnapshotHelper.swift @@ -172,7 +172,7 @@ open class Snapshot: NSObject { let window = app.windows.firstMatch let screenshot = window.screenshot() guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } - + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") do { try screenshot.pngRepresentation.write(to: path)