From 574ab7e3a2bd54af80e152ea405b44c7859a641b Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 24 Oct 2024 13:55:27 +0200 Subject: [PATCH] Version 6.0.0 (#3122) Signed-off-by: Nextcloud bot Signed-off-by: Marino Faggiana Signed-off-by: Milen Pivchev Signed-off-by: rakekniven <2069590+rakekniven@users.noreply.github.com> --- Brand/Database.swift | 2 +- Brand/File_Provider_Extension.plist | 2 + Brand/Intro/NCIntroViewController.swift | 34 +- Brand/NCBrand.swift | 206 ++-- Brand/iOSClient.plist | 2 + Cartfile | 2 - Cartfile.resolved | 2 - ExternalResources/NCApplicationHandle.swift | 2 +- .../DocumentActionViewController.swift | 6 +- .../FileProviderData.swift | 92 +- .../FileProviderDomain.swift | 14 +- .../FileProviderEnumerator.swift | 35 +- .../FileProviderExtension+Actions.swift | 101 +- ...ProviderExtension+NetworkingDelegate.swift | 25 +- .../FileProviderExtension+Thumbnail.swift | 15 +- .../FileProviderExtension.swift | 129 ++- .../FileProviderItem.swift | 2 +- .../FileProviderUtility.swift | 13 +- Nextcloud.xcodeproj/project.pbxproj | 467 +++++++-- .../File Provider Extension.xcscheme | 10 +- .../xcshareddata/xcschemes/Share.xcscheme | 4 +- .../NotificationService.swift | 3 +- README.md | 10 +- Share/NCShareCell.swift | 16 +- Share/NCShareExtension+DataSource.swift | 58 +- Share/NCShareExtension+Files.swift | 17 +- ...eExtension+NCAccountRequestDelegate.swift} | 82 +- Share/NCShareExtension.swift | 123 +-- Share/Share-Bridging-Header.h | 1 + Tests/Common/BaseXCTestCase.swift | 1 + Tests/Common/TestConstants.swift | 1 + .../FilesIntegrationTests.swift | 12 +- Widget/Dashboard/DashboardData.swift | 61 +- .../Dashboard/DashboardWidgetProvider.swift | 2 +- Widget/Dashboard/DashboardWidgetView.swift | 15 +- Widget/Files/FilesData.swift | 74 +- Widget/Files/FilesWidgetProvider.swift | 4 +- Widget/Files/FilesWidgetView.swift | 22 +- Widget/Lockscreen/LockscreenData.swift | 39 +- Widget/Toolbar/ToolbarData.swift | 17 +- Widget/Toolbar/ToolbarWidgetProvider.swift | 2 +- Widget/Toolbar/ToolbarWidgetView.swift | 22 +- .../IntentHandler.swift | 20 +- .../Account Request/NCAccountRequest.swift | 14 +- .../NCAccountSettingsModel.swift | 80 +- .../NCAccountSettingsView.swift | 30 +- iOSClient/Activity/NCActivity.storyboard | 33 +- iOSClient/Activity/NCActivity.swift | 118 ++- .../Activity/NCActivityCommentView.swift | 5 +- .../Activity/NCActivityTableViewCell.swift | 56 +- iOSClient/AppDelegate.swift | 311 ++---- .../NCAssistantCreateNewTask.swift | 2 +- .../Assistant/Models/NCAssistantTask.swift | 19 +- iOSClient/Assistant/NCAssistant.swift | 4 +- .../Assistant/NCAssistantEmptyView.swift | 3 +- .../Task Detail/NCAssistantTaskDetail.swift | 2 +- .../NCAudioRecorderViewController.swift | 28 +- iOSClient/BrowserWeb/NCBrowserWeb.swift | 2 +- iOSClient/Color/NCColorPicker.swift | 4 +- iOSClient/Data/NCManageDatabase+Account.swift | 73 +- .../Data/NCManageDatabase+Activity.swift | 1 + iOSClient/Data/NCManageDatabase+Avatar.swift | 10 +- .../Data/NCManageDatabase+Capabilities.swift | 113 +- iOSClient/Data/NCManageDatabase+Chunk.swift | 1 + .../Data/NCManageDatabase+Comments.swift | 1 + .../NCManageDatabase+DashboardWidget.swift | 1 + .../Data/NCManageDatabase+DirectEditing.swift | 1 + .../Data/NCManageDatabase+Directory.swift | 16 +- iOSClient/Data/NCManageDatabase+E2EE.swift | 1 + .../Data/NCManageDatabase+ExternalSites.swift | 1 + iOSClient/Data/NCManageDatabase+GPS.swift | 1 + .../Data/NCManageDatabase+Groupfolders.swift | 1 + .../Data/NCManageDatabase+LayoutForView.swift | 1 + .../Data/NCManageDatabase+LocalFile.swift | 7 +- .../NCManageDatabase+Metadata+Session.swift | 79 +- .../Data/NCManageDatabase+Metadata.swift | 719 +++++++------ .../Data/NCManageDatabase+SecurityGuard.swift | 1 + iOSClient/Data/NCManageDatabase+Share.swift | 1 + iOSClient/Data/NCManageDatabase+Tag.swift | 1 + iOSClient/Data/NCManageDatabase+Tip.swift | 1 + iOSClient/Data/NCManageDatabase+Trash.swift | 13 +- .../Data/NCManageDatabase+UserStatus.swift | 1 + iOSClient/Data/NCManageDatabase+Video.swift | 1 + iOSClient/Data/NCManageDatabase.swift | 61 +- iOSClient/DeepLink/NCDeepLinkHandler.swift | 6 +- .../NotificationCenter+MainThread.swift | 1 - .../UIAlertController+Extension.swift | 89 +- iOSClient/Extensions/UIImage+Extension.swift | 14 +- iOSClient/Favorites/NCFavorite.swift | 50 +- iOSClient/Files/NCFiles.swift | 273 ++--- iOSClient/GUI/ComponentView.swift | 7 +- .../GUI/{HUDView.swift => NCHUDView.swift} | 18 +- iOSClient/GUI/NCHud.swift | 131 +++ iOSClient/Groupfolders/NCGroupfolders.swift | 60 +- iOSClient/Login/NCLogin.storyboard | 85 +- iOSClient/Login/NCLogin.swift | 103 +- iOSClient/Login/NCLoginPoll.swift | 65 +- iOSClient/Login/NCLoginProvider.swift | 34 +- .../Cell/NCCellProtocol.swift | 40 +- .../Collection Common/Cell/NCGridCell.swift | 79 +- .../Collection Common/Cell/NCGridCell.xib | 70 +- .../Collection Common/Cell/NCListCell.swift | 104 +- .../Collection Common/Cell/NCListCell.xib | 67 +- .../Collection Common/Cell/NCPhotoCell.swift | 91 +- .../Collection Common/Cell/NCPhotoCell.xib | 58 +- ...nViewCommon+CollectionViewDataSource.swift | 502 +++++---- ...+CollectionViewDataSourcePrefetching.swift | 49 + ...ionViewCommon+CollectionViewDelegate.swift | 79 +- .../NCCollectionViewCommon+DragDrop.swift | 23 +- .../NCCollectionViewCommon+EasyTipView.swift | 12 +- .../NCCollectionViewCommon+MediaLayout.swift | 26 +- .../NCCollectionViewCommon+SelectTabBar.swift | 45 +- ...mmon+SwipeCollectionViewCellDelegate.swift | 94 -- .../NCCollectionViewCommon.swift | 977 ++++++++---------- .../NCCollectionViewCommonPinchGesture.swift | 94 ++ .../NCCollectionViewCommonSelectTabBar.swift | 12 +- .../NCCollectionViewDataSource.swift} | 285 ++--- .../NCCollectionViewDownloadThumbnail.swift | 63 +- .../NCCollectionViewUnifiedSearch.swift | 3 +- .../NCSectionFirstHeader.swift | 52 +- .../NCSectionFirstHeader.xib | 33 +- .../NCSectionFirstHeaderEmptyData.swift | 58 +- .../NCSectionFirstHeaderEmptyData.xib | 35 +- .../Section Header Footer/NCSectionFooter.xib | 22 +- .../Main/Create cloud/NCCreateDocument.swift | 78 +- .../NCCreateFormUploadConflict.swift | 27 +- .../Upload Assets/NCUploadAssetsModel.swift | 80 +- .../Upload Assets/NCUploadAssetsView.swift | 83 +- iOSClient/Main/NCActionCenter.swift | 353 ++++--- iOSClient/Main/NCDragDrop.swift | 68 +- iOSClient/Main/NCMainTabBar.swift | 36 +- iOSClient/Main/NCMainTabBarController.swift | 32 +- iOSClient/Main/NCPasscode.swift | 9 +- iOSClient/Main/NCPickerViewController.swift | 69 +- ...CGridMediaCell.swift => NCMediaCell.swift} | 11 +- .../{NCGridMediaCell.xib => NCMediaCell.xib} | 28 +- .../NCMedia+CollectionViewDataSource.swift | 111 +- ...+CollectionViewDataSourcePrefetching.swift | 45 + .../NCMedia+CollectionViewDelegate.swift | 45 +- iOSClient/Media/NCMedia+Command.swift | 175 ++-- iOSClient/Media/NCMedia+DragDrop.swift | 31 +- iOSClient/Media/NCMedia+MediaLayout.swift | 37 +- iOSClient/Media/NCMedia.swift | 278 ++--- iOSClient/Media/NCMediaDataSource.swift | 339 ++++-- .../Media/NCMediaDownloadThumbnail.swift | 74 ++ .../Media/NCMediaDownloadThumbnaill.swift | 88 -- iOSClient/Media/NCMediaLayout.swift | 16 +- iOSClient/Media/NCMediaPinchGesture.swift | 94 ++ iOSClient/Menu/AppDelegate+Menu.swift | 94 +- .../Menu/NCCollectionViewCommon+Menu.swift | 123 ++- iOSClient/Menu/NCContextMenu.swift | 218 ++-- iOSClient/Menu/NCMenuAction.swift | 17 +- iOSClient/Menu/NCOperationSaveLivePhoto.swift | 76 +- iOSClient/Menu/NCShare+Menu.swift | 6 +- iOSClient/Menu/NCTrash+Menu.swift | 14 +- iOSClient/Menu/NCViewer+Menu.swift | 107 +- iOSClient/Menu/UIViewController+Menu.swift | 23 +- iOSClient/More/Cells/BaseNCMoreCell.swift | 1 + iOSClient/More/Cells/CCCellMore.swift | 1 + .../More/Cells/NCMoreAppSuggestionsCell.swift | 6 +- iOSClient/More/NCMore.swift | 84 +- iOSClient/NCAccount.swift | 154 +++ iOSClient/NCCapabilities.swift | 97 ++ iOSClient/NCGlobal.swift | 206 ++-- iOSClient/NCImageCache.swift | 403 ++++---- iOSClient/NCSession.swift | 92 ++ .../Networking/E2EE/NCEndToEndMetadata.swift | 28 +- .../E2EE/NCEndToEndMetadataV1.swift | 53 +- .../E2EE/NCEndToEndMetadataV20.swift | 100 +- .../Networking/E2EE/NCNetworkingE2EE.swift | 108 +- .../E2EE/NCNetworkingE2EECreateFolder.swift | 63 +- .../E2EE/NCNetworkingE2EEDelete.swift | 54 +- .../E2EE/NCNetworkingE2EEMarkFolder.swift | 20 +- .../E2EE/NCNetworkingE2EERename.swift | 34 +- .../E2EE/NCNetworkingE2EEUpload.swift | 188 ++-- iOSClient/Networking/NCAutoUpload.swift | 151 +-- iOSClient/Networking/NCConfigServer.swift | 6 +- .../Networking/NCNetworking+AsyncAwait.swift | 156 +-- .../Networking/NCNetworking+Download.swift | 284 +++-- .../Networking/NCNetworking+LivePhoto.swift | 97 +- .../NCNetworking+Synchronization.swift | 40 +- iOSClient/Networking/NCNetworking+Task.swift | 455 ++++++++ .../Networking/NCNetworking+Upload.swift | 491 +++++---- .../Networking/NCNetworking+WebDAV.swift | 794 +++++++------- iOSClient/Networking/NCNetworking.swift | 248 ++--- .../NCNetworkingCheckRemoteUser.swift | 51 +- .../Networking/NCNetworkingProcess.swift | 523 ++++++---- iOSClient/Networking/NCService.swift | 181 ++-- .../Networking/NCTransfersProgress.swift | 125 +++ iOSClient/Nextcloud-Bridging-Header.h | 1 + iOSClient/Notification/NCNotification.swift | 51 +- iOSClient/Offline/NCOffline.swift | 27 +- .../PushNotification/NCPushNotification.swift | 12 +- iOSClient/Recent/NCRecent.swift | 47 +- .../RichWorkspace/NCRichWorkspaceCommon.swift | 31 +- .../RichWorkspace/NCViewerRichWorkspace.swift | 13 +- .../NCViewerRichWorkspaceWebView.swift | 2 +- .../Scan document/NCDocumentCamera.swift | 2 +- .../Scan document/NCScan+CollectionView.swift | 4 +- iOSClient/Scan document/NCScan.swift | 30 +- .../Scan document/NCUploadScanDocument.swift | 97 +- iOSClient/SceneDelegate.swift | 191 ++-- iOSClient/Select/NCSelect.storyboard | 8 +- iOSClient/Select/NCSelect.swift | 294 +++--- .../Capabilities/NCCapabilitiesModel.swift | 44 +- .../Capabilities/NCCapabilitiesView.swift | 2 +- .../Advanced/File Name/NCFileNameModel.swift | 10 +- .../Advanced/File Name/NCFileNameView.swift | 8 +- .../Advanced/NCSettingsAdvancedModel.swift | 25 +- .../Advanced/NCSettingsAdvancedView.swift | 16 +- .../AutoUpload/NCAutoUploadModel.swift | 62 +- .../AutoUpload/NCAutoUploadView.swift | 26 +- .../Settings/Display/NCDisplayModel.swift | 15 +- .../Settings/Display/NCDisplayView.swift | 20 +- .../Settings/Helpers/NCWebBrowserView.swift | 2 +- iOSClient/Settings/NCKeychain.swift | 27 +- .../Settings/NCSettingsBundleHelper.swift | 1 + .../Settings/E2EE/NCEndToEndInitialize.swift | 121 +-- .../Settings/E2EE/NCManageE2EEModel.swift | 27 +- .../Settings/E2EE/NCManageE2EEView.swift | 8 +- .../Settings/Settings/NCSettingsModel.swift | 17 +- .../Settings/Settings/NCSettingsView.swift | 47 +- .../Advanced/NCShareAdvancePermission.swift | 9 +- .../NCShareAdvancePermissionFooter.swift | 6 +- iOSClient/Share/Advanced/NCShareCells.swift | 32 +- .../Advanced/NCShareNewUserAddComment.swift | 1 - iOSClient/Share/NCShare+Helper.swift | 4 +- iOSClient/Share/NCShare+NCCellDelegate.swift | 12 +- iOSClient/Share/NCShare.swift | 96 +- iOSClient/Share/NCShareCommentsCell.xib | 35 +- iOSClient/Share/NCShareHeader.swift | 10 +- iOSClient/Share/NCShareLinkCell.swift | 2 +- iOSClient/Share/NCShareLinkCell.xib | 8 +- iOSClient/Share/NCShareNetworking.swift | 32 +- iOSClient/Share/NCSharePaging.swift | 10 +- iOSClient/Share/NCShareUserCell.swift | 19 +- iOSClient/Shares/NCShares.swift | 72 +- .../af.lproj/Localizable.strings | Bin 132268 -> 136422 bytes .../an.lproj/Localizable.strings | Bin 131626 -> 135780 bytes .../ar.lproj/InfoPlist.strings | Bin 1398 -> 1394 bytes .../ar.lproj/Localizable.strings | Bin 130636 -> 134116 bytes .../ast.lproj/Localizable.strings | Bin 133308 -> 137462 bytes .../az.lproj/Localizable.strings | Bin 131950 -> 136104 bytes .../be.lproj/Localizable.strings | Bin 131706 -> 135860 bytes .../bg_BG.lproj/Localizable.strings | Bin 138726 -> 142826 bytes .../bn_BD.lproj/Localizable.strings | Bin 131880 -> 136034 bytes .../br.lproj/Localizable.strings | Bin 135650 -> 139798 bytes .../bs.lproj/Localizable.strings | Bin 131942 -> 136096 bytes .../ca.lproj/Localizable.strings | Bin 138614 -> 142744 bytes .../cs-CZ.lproj/Localizable.strings | Bin 135582 -> 139748 bytes .../cy_GB.lproj/Localizable.strings | Bin 131902 -> 136056 bytes .../da.lproj/Localizable.strings | Bin 132814 -> 137030 bytes .../de.lproj/Localizable.strings | Bin 144034 -> 148398 bytes .../el.lproj/Localizable.strings | Bin 145130 -> 149134 bytes .../en-GB.lproj/Localizable.strings | Bin 131728 -> 135886 bytes .../en.lproj/Localizable.strings | 42 +- .../eo.lproj/Localizable.strings | Bin 132348 -> 136502 bytes .../es-419.lproj/Localizable.strings | Bin 135756 -> 139910 bytes .../es-AR.lproj/Localizable.strings | Bin 134818 -> 138972 bytes .../es-CL.lproj/Localizable.strings | Bin 136474 -> 140628 bytes .../es-CO.lproj/Localizable.strings | Bin 136116 -> 140270 bytes .../es-CR.lproj/Localizable.strings | Bin 136118 -> 140272 bytes .../es-DO.lproj/Localizable.strings | Bin 136114 -> 140268 bytes .../es-EC.lproj/Localizable.strings | Bin 141002 -> 145070 bytes .../es-GT.lproj/Localizable.strings | Bin 136118 -> 140272 bytes .../es-HN.lproj/Localizable.strings | Bin 135740 -> 139894 bytes .../es-MX.lproj/Localizable.strings | Bin 136458 -> 140612 bytes .../es-NI.lproj/Localizable.strings | Bin 135730 -> 139884 bytes .../es-PA.lproj/Localizable.strings | Bin 135730 -> 139884 bytes .../es-PE.lproj/Localizable.strings | Bin 135720 -> 139874 bytes .../es-PR.lproj/Localizable.strings | Bin 135732 -> 139886 bytes .../es-PY.lproj/Localizable.strings | Bin 135756 -> 139910 bytes .../es-SV.lproj/Localizable.strings | Bin 136106 -> 140260 bytes .../es-UY.lproj/Localizable.strings | Bin 135754 -> 139908 bytes .../es.lproj/Localizable.strings | Bin 141202 -> 145308 bytes .../et_EE.lproj/Localizable.strings | Bin 132168 -> 136322 bytes .../eu.lproj/Localizable.strings | Bin 139298 -> 143450 bytes .../fa.lproj/Localizable.strings | Bin 132436 -> 136570 bytes .../fi-FI.lproj/Localizable.strings | Bin 134590 -> 138740 bytes .../fo.lproj/Localizable.strings | Bin 131636 -> 135790 bytes .../fr.lproj/Localizable.strings | Bin 145912 -> 150878 bytes .../ga.lproj/Localizable.strings | Bin 142836 -> 147090 bytes .../gd.lproj/Localizable.strings | Bin 133136 -> 137290 bytes .../gl.lproj/Localizable.strings | Bin 141552 -> 145854 bytes .../he.lproj/Localizable.strings | Bin 129750 -> 133904 bytes .../hi_IN.lproj/Localizable.strings | Bin 131610 -> 135764 bytes .../hr.lproj/Localizable.strings | Bin 136300 -> 140460 bytes .../hsb.lproj/Localizable.strings | Bin 131616 -> 135770 bytes .../hu.lproj/Localizable.strings | Bin 137622 -> 141712 bytes .../hy.lproj/Localizable.strings | Bin 131894 -> 136048 bytes .../ia.lproj/Localizable.strings | Bin 132394 -> 136548 bytes .../id.lproj/Localizable.strings | Bin 132528 -> 136682 bytes .../ig.lproj/Localizable.strings | Bin 131586 -> 135740 bytes .../is.lproj/Localizable.strings | Bin 133836 -> 137990 bytes .../it.lproj/Localizable.strings | Bin 140554 -> 145242 bytes .../ja-JP.lproj/Localizable.strings | Bin 115248 -> 119564 bytes .../ka-GE.lproj/Localizable.strings | Bin 134346 -> 138500 bytes .../ka.lproj/Localizable.strings | Bin 131614 -> 135768 bytes .../kab.lproj/Localizable.strings | Bin 131674 -> 135828 bytes .../km.lproj/Localizable.strings | Bin 131868 -> 136022 bytes .../kn.lproj/Localizable.strings | Bin 132096 -> 136250 bytes .../ko.lproj/Localizable.strings | Bin 110306 -> 114646 bytes .../la.lproj/Localizable.strings | Bin 131600 -> 135754 bytes .../lb.lproj/Localizable.strings | Bin 132078 -> 136232 bytes .../lo.lproj/Localizable.strings | Bin 130526 -> 134684 bytes .../lt_LT.lproj/Localizable.strings | Bin 134254 -> 138882 bytes .../lv.lproj/Localizable.strings | Bin 132756 -> 136884 bytes .../mk.lproj/Localizable.strings | Bin 132904 -> 137058 bytes .../mn.lproj/Localizable.strings | Bin 132412 -> 136566 bytes .../mr.lproj/Localizable.strings | Bin 131578 -> 135732 bytes .../ms_MY.lproj/Localizable.strings | Bin 131724 -> 135878 bytes .../my.lproj/Localizable.strings | Bin 131796 -> 135950 bytes .../nb-NO.lproj/Localizable.strings | Bin 134358 -> 138462 bytes .../ne.lproj/Localizable.strings | Bin 131630 -> 135784 bytes .../nl.lproj/Localizable.strings | Bin 136894 -> 141088 bytes .../nn_NO.lproj/Localizable.strings | Bin 131748 -> 135902 bytes .../oc.lproj/Localizable.strings | Bin 132754 -> 136908 bytes .../pl.lproj/Localizable.strings | Bin 136240 -> 140378 bytes .../ps.lproj/Localizable.strings | Bin 131624 -> 135778 bytes .../pt-BR.lproj/Localizable.strings | Bin 138654 -> 143226 bytes .../pt-PT.lproj/Localizable.strings | Bin 135414 -> 139558 bytes .../ro.lproj/Localizable.strings | Bin 134734 -> 138888 bytes .../ru.lproj/Localizable.strings | Bin 138632 -> 143200 bytes .../sc.lproj/Localizable.strings | Bin 140320 -> 144504 bytes .../si.lproj/Localizable.strings | Bin 132490 -> 136644 bytes .../sk-SK.lproj/Localizable.strings | Bin 136378 -> 140584 bytes .../sl.lproj/Localizable.strings | Bin 137422 -> 141510 bytes .../sq.lproj/Localizable.strings | Bin 133392 -> 137546 bytes .../sr.lproj/Localizable.strings | Bin 135910 -> 140164 bytes .../sr@latin.lproj/Localizable.strings | Bin 131914 -> 136068 bytes .../sv.lproj/Localizable.strings | Bin 133946 -> 138114 bytes .../sw.lproj/Localizable.strings | Bin 131610 -> 135764 bytes .../ta.lproj/Localizable.strings | Bin 131948 -> 136102 bytes .../th_TH.lproj/Localizable.strings | Bin 131532 -> 135686 bytes .../tk.lproj/Localizable.strings | Bin 132312 -> 136466 bytes .../tr.lproj/Localizable.strings | Bin 136766 -> 140830 bytes .../ug.lproj/Localizable.strings | Bin 131782 -> 135936 bytes .../uk.lproj/Localizable.strings | Bin 134116 -> 138270 bytes .../ur_PK.lproj/Localizable.strings | Bin 131658 -> 135812 bytes .../uz.lproj/Localizable.strings | Bin 131610 -> 135764 bytes .../vi.lproj/Localizable.strings | Bin 132988 -> 137142 bytes .../zh-Hans.lproj/Localizable.strings | Bin 98770 -> 99536 bytes .../zh-Hant-TW.lproj/Localizable.strings | Bin 111730 -> 115910 bytes .../zh_HK.lproj/Localizable.strings | Bin 97768 -> 100244 bytes .../zu_ZA.lproj/Localizable.strings | Bin 131610 -> 135764 bytes iOSClient/Transfers/NCTransferCell.swift | 58 +- iOSClient/Transfers/NCTransferCell.xib | 38 +- iOSClient/Transfers/NCTransfers.swift | 237 +++-- .../Trash/Cell/NCTrashCellProtocol.swift | 6 +- iOSClient/Trash/Cell/NCTrashGridCell.swift | 17 +- iOSClient/Trash/Cell/NCTrashListCell.swift | 8 +- iOSClient/Trash/NCTrash+CollectionView.swift | 65 +- iOSClient/Trash/NCTrash+Networking.swift | 83 +- .../Trash/NCTrash+SelectTabBarDelegate.swift | 10 +- iOSClient/Trash/NCTrash.swift | 43 +- iOSClient/Trash/NCTrashSelectTabBar.swift | 2 +- iOSClient/UserStatus/NCUserStatus.swift | 32 +- .../FileNameValidator+Extensions.swift | 28 + iOSClient/Utility/NCAskAuthorization.swift | 4 +- iOSClient/Utility/NCCameraRoll.swift | 58 +- iOSClient/Utility/NCContentPresenter.swift | 5 +- iOSClient/Utility/NCLivePhoto.swift | 18 +- iOSClient/Utility/NCUserBaseUrl.swift | 40 - iOSClient/Utility/NCUtility+Image.swift | 205 ++-- iOSClient/Utility/NCUtility.swift | 19 +- iOSClient/Utility/NCUtilityFileSystem.swift | 53 +- iOSClient/Utility/PKCS12.swift | 1 + iOSClient/Utility/ParallelWorker.swift | 42 +- .../ScreenAwakeManager/AwakeMode.swift | 34 + .../ScreenAwakeManager.swift | 90 ++ .../Models/TOPasscodeCircleImage.h | 57 + .../Models/TOPasscodeCircleImage.m | 75 ++ .../Models/TOPasscodeViewContentLayout.h | 100 ++ .../Models/TOPasscodeViewContentLayout.m | 205 ++++ ...scodeViewControllerAnimatedTransitioning.h | 60 ++ ...scodeViewControllerAnimatedTransitioning.m | 120 +++ .../Models/TOSettingsKeypadImage.h | 54 + .../Models/TOSettingsKeypadImage.m | 186 ++++ .../TOPasscodeViewControllerConstants.h | 71 ++ .../TOPasscodeSettingsViewController.h | 108 ++ .../TOPasscodeSettingsViewController.m | 630 +++++++++++ .../TOPasscodeViewController.h | 163 +++ .../TOPasscodeViewController.m | 710 +++++++++++++ .../Views/Main/TOPasscodeCircleButton.h | 81 ++ .../Views/Main/TOPasscodeCircleButton.m | 236 +++++ .../Views/Main/TOPasscodeKeypadView.h | 101 ++ .../Views/Main/TOPasscodeKeypadView.m | 432 ++++++++ .../Views/Main/TOPasscodeView.h | 136 +++ .../Views/Main/TOPasscodeView.m | 690 +++++++++++++ .../Settings/TOPasscodeSettingsKeypadButton.h | 45 + .../Settings/TOPasscodeSettingsKeypadButton.m | 103 ++ .../Settings/TOPasscodeSettingsKeypadView.h | 74 ++ .../Settings/TOPasscodeSettingsKeypadView.m | 395 +++++++ .../Settings/TOPasscodeSettingsWarningLabel.h | 43 + .../Settings/TOPasscodeSettingsWarningLabel.m | 154 +++ .../Views/Shared/TOPasscodeButtonLabel.h | 61 ++ .../Views/Shared/TOPasscodeButtonLabel.m | 186 ++++ .../Views/Shared/TOPasscodeCircleView.h | 46 + .../Views/Shared/TOPasscodeCircleView.m | 89 ++ .../Views/Shared/TOPasscodeFixedInputView.h | 54 + .../Views/Shared/TOPasscodeFixedInputView.m | 171 +++ .../Views/Shared/TOPasscodeInputField.h | 111 ++ .../Views/Shared/TOPasscodeInputField.m | 423 ++++++++ .../Shared/TOPasscodeVariableInputView.h | 55 + .../Shared/TOPasscodeVariableInputView.m | 254 +++++ iOSClient/Utility/ThreadSafeArray.swift | 1 + iOSClient/Utility/ThreadSafeDictionary.swift | 1 + iOSClient/Viewer/NCViewer.swift | 77 +- .../NCViewerMedia/NCPlayer/NCPlayer.swift | 40 +- .../NCPlayer/NCPlayerToolBar.swift | 69 +- .../NCPlayer/NCPlayerToolBar.xib | 22 +- .../NCViewerMedia+VisionKit.swift | 1 + .../Viewer/NCViewerMedia/NCViewerMedia.swift | 173 ++-- .../NCViewerMediaDetailView.swift | 2 +- .../NCViewerMedia/NCViewerMediaPage.swift | 180 ++-- .../NCViewerNextcloudText.swift | 15 +- .../Viewer/NCViewerPDF/NCViewerPDF.swift | 78 +- .../Viewer/NCViewerProviderContextMenu.swift | 20 +- .../NCViewerQuickLook/NCViewerQuickLook.swift | 20 +- .../NCViewerRichDocument.swift | 63 +- 420 files changed, 17021 insertions(+), 8767 deletions(-) delete mode 100644 Cartfile delete mode 100644 Cartfile.resolved rename Share/{NCShareExtension+NCDelegate.swift => NCShareExtension+NCAccountRequestDelegate.swift} (64%) rename iOSClient/GUI/{HUDView.swift => NCHUDView.swift} (88%) create mode 100644 iOSClient/GUI/NCHud.swift create mode 100644 iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift delete mode 100644 iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift create mode 100644 iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift rename iOSClient/{Data/NCDataSource.swift => Main/Collection Common/NCCollectionViewDataSource.swift} (63%) rename iOSClient/Media/Cell/{NCGridMediaCell.swift => NCMediaCell.swift} (83%) rename iOSClient/Media/Cell/{NCGridMediaCell.xib => NCMediaCell.xib} (75%) create mode 100644 iOSClient/Media/NCMedia+CollectionViewDataSourcePrefetching.swift create mode 100644 iOSClient/Media/NCMediaDownloadThumbnail.swift delete mode 100644 iOSClient/Media/NCMediaDownloadThumbnaill.swift create mode 100644 iOSClient/Media/NCMediaPinchGesture.swift create mode 100644 iOSClient/NCAccount.swift create mode 100644 iOSClient/NCCapabilities.swift create mode 100644 iOSClient/NCSession.swift create mode 100644 iOSClient/Networking/NCNetworking+Task.swift create mode 100644 iOSClient/Networking/NCTransfersProgress.swift create mode 100644 iOSClient/Utility/FileNameValidator+Extensions.swift delete mode 100644 iOSClient/Utility/NCUserBaseUrl.swift create mode 100644 iOSClient/Utility/ScreenAwakeManager/AwakeMode.swift create mode 100644 iOSClient/Utility/ScreenAwakeManager/ScreenAwakeManager.swift create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h create mode 100755 iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h create mode 100644 iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m diff --git a/Brand/Database.swift b/Brand/Database.swift index 81e9028b7f..ae439a5ec7 100644 --- a/Brand/Database.swift +++ b/Brand/Database.swift @@ -26,4 +26,4 @@ import Foundation // Database Realm // let databaseName = "nextcloud.realm" -let databaseSchemaVersion: UInt64 = 354 +let databaseSchemaVersion: UInt64 = 365 diff --git a/Brand/File_Provider_Extension.plist b/Brand/File_Provider_Extension.plist index ee7d70040e..da5bbb7398 100755 --- a/Brand/File_Provider_Extension.plist +++ b/Brand/File_Provider_Extension.plist @@ -31,6 +31,8 @@ group.it.twsweb.Crypto-Cloud NSExtensionFileProviderSupportsEnumeration + NSExtensionFileProviderSupportsPickingFolders + NSExtensionPointIdentifier com.apple.fileprovider-nonui NSExtensionPrincipalClass diff --git a/Brand/Intro/NCIntroViewController.swift b/Brand/Intro/NCIntroViewController.swift index 369eef6255..fb5cc511ed 100644 --- a/Brand/Intro/NCIntroViewController.swift +++ b/Brand/Intro/NCIntroViewController.swift @@ -26,7 +26,6 @@ import UIKit class NCIntroViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - @IBOutlet weak var buttonLogin: UIButton! @IBOutlet weak var buttonSignUp: UIButton! @IBOutlet weak var buttonHost: UIButton! @@ -99,18 +98,23 @@ class NCIntroViewController: UIViewController, UICollectionViewDataSource, UICol view.backgroundColor = NCBrandColor.shared.customer timerAutoScroll = Timer.scheduledTimer(timeInterval: 5, target: self, selector: (#selector(NCIntroViewController.autoScroll)), userInfo: nil, repeats: true) - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { _ in - let window = UIApplication.shared.firstWindow - if window?.rootViewController is NCMainTabBarController { - self.dismiss(animated: true) - } else { - if let mainTabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? NCMainTabBarController { - mainTabBarController.modalPresentationStyle = .fullScreen - mainTabBarController.view.alpha = 0 - window?.rootViewController = mainTabBarController - window?.makeKeyAndVisible() - UIView.animate(withDuration: 0.5) { - mainTabBarController.view.alpha = 1 + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { notification in + if let userInfo = notification.userInfo, + let account = userInfo["account"] as? String { + let window = UIApplication.shared.firstWindow + if let controller = window?.rootViewController as? NCMainTabBarController { + controller.account = account + self.dismiss(animated: true) + } else { + if let controller = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? NCMainTabBarController { + controller.account = account + controller.modalPresentationStyle = .fullScreen + controller.view.alpha = 0 + window?.rootViewController = controller + window?.makeKeyAndVisible() + UIView.animate(withDuration: 0.5) { + controller.view.alpha = 1 + } } } } @@ -179,11 +183,11 @@ class NCIntroViewController: UIViewController, UICollectionViewDataSource, UICol } @IBAction func login(_ sender: Any) { - appDelegate.openLogin(selector: NCGlobal.shared.introLogin, openLoginWeb: false) + appDelegate.openLogin(selector: NCGlobal.shared.introLogin) } @IBAction func signup(_ sender: Any) { - appDelegate.openLogin(selector: NCGlobal.shared.introSignup, openLoginWeb: false) + appDelegate.openLogin(selector: NCGlobal.shared.introSignup) } @IBAction func host(_ sender: Any) { diff --git a/Brand/NCBrand.swift b/Brand/NCBrand.swift index cfeef5ca6d..f02ea986a7 100755 --- a/Brand/NCBrand.swift +++ b/Brand/NCBrand.swift @@ -58,7 +58,7 @@ let userAgent: String = { // BRAND ONLY @objc public var use_AppConfig: Bool = false // Don't touch me !! - // Options + // Use server theming color @objc public var use_themingColor: Bool = true var disable_intro: Bool = false @@ -71,6 +71,10 @@ let userAgent: String = { var disable_mobileconfig: Bool = false var disable_show_more_nextcloud_apps_in_settings: Bool = false var doNotAskPasscodeAtStartup: Bool = false + var disable_source_code_in_settings: Bool = false + + // (name: "Name 1", url: "https://cloud.nextcloud.com"),(name: "Name 2", url: "https://cloud.nextcloud.com") + var enforce_servers: [(name: String, url: String)] = [] // Internal option behaviour var cleanUpDay: Int = 0 // Set default "Delete, in the cache, all files older than" possible days value are: 0, 1, 7, 30, 90, 180, 365 @@ -127,26 +131,22 @@ class NCBrandColor: NSObject { return instance }() - // Color + /// This is rewrited from customet theme, default is Nextcloud color + /// let customer: UIColor = UIColor(red: 0.0 / 255.0, green: 130.0 / 255.0, blue: 201.0 / 255.0, alpha: 1.0) // BLU NC : #0082c9 var customerText: UIColor = .white - var brand: UIColor // don't touch me - var brandElement: UIColor // don't touch me - var brandText: UIColor // don't touch me + // INTERNAL DEFINE COLORS + private var themingColor = ThreadSafeDictionary() + private var themingColorElement = ThreadSafeDictionary() + private var themingColorText = ThreadSafeDictionary() + var userColors: [CGColor] = [] let nextcloud: UIColor = UIColor(red: 0.0 / 255.0, green: 130.0 / 255.0, blue: 201.0 / 255.0, alpha: 1.0) let yellowFavorite: UIColor = UIColor(red: 248.0 / 255.0, green: 205.0 / 255.0, blue: 70.0 / 255.0, alpha: 1.0) - - var userColors: [CGColor] = [] - var themingColor: String = "" - var themingColorElement: String = "" - var themingColorText: String = "" - let iconImageColor: UIColor = .label let iconImageColor2: UIColor = .secondaryLabel let iconImageMultiColors: [UIColor] = [.secondaryLabel, .label] - let textColor: UIColor = .label let textColor2: UIColor = .secondaryLabel @@ -174,126 +174,162 @@ class NCBrandColor: NSObject { } } - override init() { - brand = customer - brandElement = customer - brandText = customerText - } + override init() { } + /** + Generate colors from the official nextcloud color. + You can provide how many colors you want (multiplied by 3). + if `step` = 6, + 3 colors \* 6 will result in 18 generated colors + */ func createUserColors() { + func generateColors(steps: Int = 6) -> [CGColor] { + func stepCalc(steps: Int, color1: CGColor, color2: CGColor) -> [CGFloat] { + var step = [CGFloat](repeating: 0, count: 3) + + step[0] = (color2.components![0] - color1.components![0]) / CGFloat(steps) + step[1] = (color2.components![1] - color1.components![1]) / CGFloat(steps) + step[2] = (color2.components![2] - color1.components![2]) / CGFloat(steps) + return step + } + + func mixPalette(steps: Int, color1: CGColor, color2: CGColor) -> [CGColor] { + var palette = [color1] + let step = stepCalc(steps: steps, color1: color1, color2: color2) + let c1Components = color1.components! + + for i in 1 ..< steps { + let r = c1Components[0] + step[0] * CGFloat(i) + let g = c1Components[1] + step[1] * CGFloat(i) + let b = c1Components[2] + step[2] * CGFloat(i) + + palette.append(UIColor(red: r, green: g, blue: b, alpha: 1).cgColor) + } + return palette + } + + let red = UIColor(red: 182 / 255, green: 70 / 255, blue: 157 / 255, alpha: 1).cgColor + let yellow = UIColor(red: 221 / 255, green: 203 / 255, blue: 85 / 255, alpha: 1).cgColor + let blue = UIColor(red: 0 / 255, green: 130 / 255, blue: 201 / 255, alpha: 1).cgColor + + let palette1 = mixPalette(steps: steps, color1: red, color2: yellow) + let palette2 = mixPalette(steps: steps, color1: yellow, color2: blue) + let palette3 = mixPalette(steps: steps, color1: blue, color2: red) + + return palette1 + palette2 + palette3 + } + userColors = generateColors() } - func settingThemingColor(account: String) { + @discardableResult + func settingThemingColor(account: String) -> Bool { let darker: CGFloat = 30 // % let lighter: CGFloat = 30 // % + var colorThemingColor: UIColor? + var colorThemingColorElement: UIColor? + var colorThemingColorText: UIColor? if NCBrandOptions.shared.use_themingColor { - self.themingColor = NCGlobal.shared.capabilityThemingColor - self.themingColorElement = NCGlobal.shared.capabilityThemingColorElement - self.themingColorText = NCGlobal.shared.capabilityThemingColorText + let themingColor = NCCapabilities.shared.getCapabilities(account: account).capabilityThemingColor + let themingColorElement = NCCapabilities.shared.getCapabilities(account: account).capabilityThemingColorElement + let themingColorText = NCCapabilities.shared.getCapabilities(account: account).capabilityThemingColorText - // COLOR + // THEMING COLOR if themingColor.first == "#" { if let color = UIColor(hex: themingColor) { - brand = color - } else { - brand = customer - } - } else { - brand = customer - } - - // COLOR TEXT - if themingColorText.first == "#" { - if let color = UIColor(hex: themingColorText) { - brandText = color + colorThemingColor = color } else { - brandText = customerText + colorThemingColor = customer } } else { - brandText = customerText + colorThemingColor = customer } - // COLOR ELEMENT + // THEMING COLOR ELEMENT (control isTooLight / isTooDark) if themingColorElement.first == "#" { if let color = UIColor(hex: themingColorElement) { - brandElement = color + if color.isTooLight() { + if let color = color.darker(by: darker) { + colorThemingColorElement = color + } + } else if color.isTooDark() { + if let color = color.lighter(by: lighter) { + colorThemingColorElement = color + } + } else { + colorThemingColorElement = color + } } else { - brandElement = brand + colorThemingColorElement = customer } } else { - brandElement = brand + colorThemingColorElement = customer } - if brandElement.isTooLight() { - if let color = brandElement.darker(by: darker) { - brandElement = color - } - } else if brandElement.isTooDark() { - if let color = brandElement.lighter(by: lighter) { - brandElement = color + // THEMING COLOR TEXT + if themingColorText.first == "#" { + if let color = UIColor(hex: themingColorText) { + colorThemingColorText = color + } else { + colorThemingColorText = .white } + } else { + colorThemingColorText = .white } } else { + // THEMING COLOR + colorThemingColor = customer + + // THEMING COLOR ELEMENT (control isTooLight / isTooDark) if self.customer.isTooLight() { if let color = customer.darker(by: darker) { - brandElement = color + colorThemingColorElement = color } } else if customer.isTooDark() { if let color = customer.lighter(by: lighter) { - brandElement = color + colorThemingColorElement = color } } else { - brandElement = customer + colorThemingColorElement = customer } - brand = customer - brandText = customerText + // THEMING COLOR TEXT + colorThemingColorText = customerText } - } - private func stepCalc(steps: Int, color1: CGColor, color2: CGColor) -> [CGFloat] { - var step = [CGFloat](repeating: 0, count: 3) + if self.themingColor[account] != colorThemingColor || self.themingColorElement[account] != colorThemingColorElement || self.themingColorText[account] != colorThemingColorText { - step[0] = (color2.components![0] - color1.components![0]) / CGFloat(steps) - step[1] = (color2.components![1] - color1.components![1]) / CGFloat(steps) - step[2] = (color2.components![2] - color1.components![2]) / CGFloat(steps) - return step - } + self.themingColor[account] = colorThemingColor + self.themingColorElement[account] = colorThemingColorElement + self.themingColorText[account] = colorThemingColorText - private func mixPalette(steps: Int, color1: CGColor, color2: CGColor) -> [CGColor] { - var palette = [color1] - let step = stepCalc(steps: steps, color1: color1, color2: color2) - let c1Components = color1.components! + return true + } - for i in 1 ..< steps { - let r = c1Components[0] + step[0] * CGFloat(i) - let g = c1Components[1] + step[1] * CGFloat(i) - let b = c1Components[2] + step[2] * CGFloat(i) + return false + } - palette.append(UIColor(red: r, green: g, blue: b, alpha: 1).cgColor) + public func getTheming(account: String?) -> UIColor { + if let account, let color = self.themingColor[account] { + return color } - return palette + return customer } - /** - Generate colors from the official nextcloud color. - You can provide how many colors you want (multiplied by 3). - if `step` = 6, - 3 colors \* 6 will result in 18 generated colors - */ - func generateColors(steps: Int = 6) -> [CGColor] { - let red = UIColor(red: 182 / 255, green: 70 / 255, blue: 157 / 255, alpha: 1).cgColor - let yellow = UIColor(red: 221 / 255, green: 203 / 255, blue: 85 / 255, alpha: 1).cgColor - let blue = UIColor(red: 0 / 255, green: 130 / 255, blue: 201 / 255, alpha: 1).cgColor - - let palette1 = mixPalette(steps: steps, color1: red, color2: yellow) - let palette2 = mixPalette(steps: steps, color1: yellow, color2: blue) - let palette3 = mixPalette(steps: steps, color1: blue, color2: red) + public func getElement(account: String?) -> UIColor { + if let account, let color = self.themingColorElement[account] { + return color + } + return customer + } - return palette1 + palette2 + palette3 + public func getText(account: String?) -> UIColor { + if let account, let color = self.themingColorText[account] { + return color + } + return .white } } diff --git a/Brand/iOSClient.plist b/Brand/iOSClient.plist index aaffe64cf9..dae6de5543 100755 --- a/Brand/iOSClient.plist +++ b/Brand/iOSClient.plist @@ -9,6 +9,8 @@ CFBundleAllowMixedLocalizations + LSMinimumSystemVersion + 12.3 CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/Cartfile b/Cartfile deleted file mode 100644 index 3946734806..0000000000 --- a/Cartfile +++ /dev/null @@ -1,2 +0,0 @@ -github "https://github.com/marinofaggiana/TOPasscodeViewController" "master" -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.5.1 \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved deleted file mode 100644 index a19f7cdae1..0000000000 --- a/Cartfile.resolved +++ /dev/null @@ -1,2 +0,0 @@ -binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" "3.6.0" -github "marinofaggiana/TOPasscodeViewController" "ed795637acd2b1ef154e011a04ebab4686d0523c" diff --git a/ExternalResources/NCApplicationHandle.swift b/ExternalResources/NCApplicationHandle.swift index a2a85fcde7..39585bcd17 100644 --- a/ExternalResources/NCApplicationHandle.swift +++ b/ExternalResources/NCApplicationHandle.swift @@ -53,7 +53,7 @@ class NCApplicationHandle: NSObject { // class: NCCollectionViewCommon (+Menu) // func: toggleMenu(metadata: tableMetadata, imageIcon: UIImage?) - func addCollectionViewCommonMenu(metadata: tableMetadata, imageIcon: UIImage?, actions: inout [NCMenuAction]) { + func addCollectionViewCommonMenu(metadata: tableMetadata, image: UIImage?, actions: inout [NCMenuAction]) { } // class: NCMore diff --git a/File Provider Extension UI/DocumentActionViewController.swift b/File Provider Extension UI/DocumentActionViewController.swift index 0b27289230..859b234652 100644 --- a/File Provider Extension UI/DocumentActionViewController.swift +++ b/File Provider Extension UI/DocumentActionViewController.swift @@ -32,9 +32,9 @@ class DocumentActionViewController: FPUIActionExtensionViewController { override func loadView() { super.loadView() - view.backgroundColor = NCBrandColor.shared.brandElement - titleError.textColor = NCBrandColor.shared.brandText - cancelButton.setTitleColor(NCBrandColor.shared.brandText, for: .normal) + view.backgroundColor = NCBrandColor.shared.customer + titleError.textColor = NCBrandColor.shared.customerText + cancelButton.setTitleColor(NCBrandColor.shared.customerText, for: .normal) titleError.text = "" } diff --git a/File Provider Extension/FileProviderData.swift b/File Provider Extension/FileProviderData.swift index d0568579e4..0add4d429b 100644 --- a/File Provider Extension/FileProviderData.swift +++ b/File Provider Extension/FileProviderData.swift @@ -33,19 +33,24 @@ class fileProviderData: NSObject { var domain: NSFileProviderDomain? var fileProviderManager: NSFileProviderManager = NSFileProviderManager.default let utilityFileSystem = NCUtilityFileSystem() - - var account = "" - var user = "" - var userId = "" - var accountUrlBase = "" - var homeServerUrl = "" - + let database = NCManageDatabase.shared var listFavoriteIdentifierRank: [String: NSNumber] = [:] - var fileProviderSignalDeleteContainerItemIdentifier: [NSFileProviderItemIdentifier: NSFileProviderItemIdentifier] = [:] var fileProviderSignalUpdateContainerItem: [NSFileProviderItemIdentifier: FileProviderItem] = [:] var fileProviderSignalDeleteWorkingSetItemIdentifier: [NSFileProviderItemIdentifier: NSFileProviderItemIdentifier] = [:] var fileProviderSignalUpdateWorkingSetItem: [NSFileProviderItemIdentifier: FileProviderItem] = [:] + private var account: String = "" + var session: NCSession.Session { + if !account.isEmpty, + let tableAccount = self.database.getTableAccount(account: account) { + return NCSession.Session(account: tableAccount.account, urlBase: tableAccount.urlBase, user: tableAccount.user, userId: tableAccount.userId) + } else if let activeTableAccount = self.database.getActiveTableAccount() { + self.account = activeTableAccount.account + return NCSession.Session(account: activeTableAccount.account, urlBase: activeTableAccount.urlBase, user: activeTableAccount.user, userId: activeTableAccount.userId) + } else { + return NCSession.Session(account: "", urlBase: "", user: "", userId: "") + } + } enum FileProviderError: Error { case downloadError @@ -81,54 +86,45 @@ class fileProviderData: NSObject { let version = NSString(format: NCBrandOptions.shared.textCopyrightNextcloudiOS as NSString, NCUtility().getVersionApp()) as String NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Start File Provider session with level \(levelLog) " + version + " (File Provider Extension)") - // NO DOMAIN -> Set default account - if domain == nil { - guard let activeAccount = NCManageDatabase.shared.getActiveAccount() else { return nil } - account = activeAccount.account - user = activeAccount.user - userId = activeAccount.userId - accountUrlBase = activeAccount.urlBase - homeServerUrl = utilityFileSystem.getHomeServer(urlBase: activeAccount.urlBase, userId: activeAccount.userId) - - NCManageDatabase.shared.setCapabilities(account: account) - NextcloudKit.shared.setup(account: activeAccount.account, user: activeAccount.user, userId: activeAccount.userId, password: NCKeychain().getPassword(account: activeAccount.account), urlBase: activeAccount.urlBase, userAgent: userAgent, nextcloudVersion: NCGlobal.shared.capabilityServerVersionMajor, delegate: NCNetworking.shared) - NCNetworking.shared.delegate = providerExtension as? NCNetworkingDelegate - - return tableAccount.init(value: activeAccount) - } - - // DOMAIN - let accounts = NCManageDatabase.shared.getAllAccount() - if accounts.isEmpty { return nil } - - for activeAccount in accounts { - guard let url = NSURL(string: activeAccount.urlBase) else { continue } - guard let host = url.host else { continue } - let accountDomain = activeAccount.userId + " (" + host + ")" - if accountDomain == domain!.identifier.rawValue { - account = activeAccount.account - user = activeAccount.user - userId = activeAccount.userId - accountUrlBase = activeAccount.urlBase - homeServerUrl = utilityFileSystem.getHomeServer(urlBase: activeAccount.urlBase, userId: activeAccount.userId) - - NCManageDatabase.shared.setCapabilities(account: account) - - NextcloudKit.shared.setup(account: activeAccount.account, user: activeAccount.user, userId: activeAccount.userId, password: NCKeychain().getPassword(account: activeAccount.account), urlBase: activeAccount.urlBase, userAgent: userAgent, nextcloudVersion: NCGlobal.shared.capabilityServerVersionMajor, delegate: NCNetworking.shared) - NCNetworking.shared.delegate = providerExtension as? NCNetworkingDelegate - - return tableAccount.init(value: activeAccount) + var tblAccount = self.database.getActiveTableAccount() + if let domain { + for tableAccount in self.database.getAllTableAccount() { + guard let urlBase = NSURL(string: tableAccount.urlBase) else { continue } + guard let host = urlBase.host else { continue } + let accountDomain = tableAccount.userId + " (" + host + ")" + if accountDomain == domain.identifier.rawValue { + let account = "\(tableAccount.user) \(tableAccount.urlBase)" + tblAccount = self.database.getTableAccount(account: account) + break + } } } - return nil + guard let tblAccount else { return nil } + + self.account = tblAccount.account + /// NextcloudKit Session + NextcloudKit.shared.setup(delegate: NCNetworking.shared) + NextcloudKit.shared.appendSession(account: tblAccount.account, + urlBase: tblAccount.urlBase, + user: tblAccount.user, + userId: tblAccount.userId, + password: NCKeychain().getPassword(account: tblAccount.account), + userAgent: userAgent, + nextcloudVersion: NCCapabilities.shared.getCapabilities(account: tblAccount.account).capabilityServerVersionMajor, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) + NCNetworking.shared.delegate = providerExtension as? NCNetworkingDelegate + + return tableAccount(value: tblAccount) } // MARK: - @discardableResult func signalEnumerator(ocId: String, type: TypeSignal) -> FileProviderItem? { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId), - let parentItemIdentifier = fileProviderUtility().getParentItemIdentifier(metadata: metadata) else { return nil } + guard let metadata = self.database.getMetadataFromOcId(ocId), + let parentItemIdentifier = fileProviderUtility().getParentItemIdentifier(metadata: metadata) else { + return nil + } let item = FileProviderItem(metadata: metadata, parentItemIdentifier: parentItemIdentifier) if type == .delete { diff --git a/File Provider Extension/FileProviderDomain.swift b/File Provider Extension/FileProviderDomain.swift index 17c03bccb0..aac0fb6fdd 100644 --- a/File Provider Extension/FileProviderDomain.swift +++ b/File Provider Extension/FileProviderDomain.swift @@ -28,7 +28,7 @@ class FileProviderDomain: NSObject { NSFileProviderManager.getDomainsWithCompletionHandler { fileProviderDomain, error in var domains: [String] = [] let pathRelativeToDocumentStorage = NSFileProviderManager.default.documentStorageURL.absoluteString - let accounts = NCManageDatabase.shared.getAllAccount() + let tableAccounts = NCManageDatabase.shared.getAllTableAccount() for domain in fileProviderDomain { domains.append(domain.identifier.rawValue) @@ -37,10 +37,10 @@ class FileProviderDomain: NSObject { // Delete for domain in domains { var domainFound = false - for account in accounts { - guard let urlBase = NSURL(string: account.urlBase) else { continue } + for tableAccount in tableAccounts { + guard let urlBase = NSURL(string: tableAccount.urlBase) else { continue } guard let host = urlBase.host else { continue } - let accountDomain = account.userId + " (" + host + ")" + let accountDomain = tableAccount.userId + " (" + host + ")" if domain == accountDomain { domainFound = true break @@ -57,11 +57,11 @@ class FileProviderDomain: NSObject { } // Add - for account in accounts { + for tableAccount in tableAccounts { var domainFound = false - guard let urlBase = NSURL(string: account.urlBase) else { continue } + guard let urlBase = NSURL(string: tableAccount.urlBase) else { continue } guard let host = urlBase.host else { continue } - let accountDomain = account.userId + " (" + host + ")" + let accountDomain = tableAccount.userId + " (" + host + ")" for domain in domains { if domain == accountDomain { domainFound = true diff --git a/File Provider Extension/FileProviderEnumerator.swift b/File Provider Extension/FileProviderEnumerator.swift index 244b69948d..671091d1ab 100644 --- a/File Provider Extension/FileProviderEnumerator.swift +++ b/File Provider Extension/FileProviderEnumerator.swift @@ -30,16 +30,17 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { var enumeratedItemIdentifier: NSFileProviderItemIdentifier var serverUrl: String? let providerUtility = fileProviderUtility() + let database = NCManageDatabase.shared var recordsPerPage: Int = 20 var anchor: UInt64 = 0 init(enumeratedItemIdentifier: NSFileProviderItemIdentifier) { self.enumeratedItemIdentifier = enumeratedItemIdentifier if enumeratedItemIdentifier == .rootContainer { - serverUrl = fileProviderData.shared.homeServerUrl + serverUrl = NCUtilityFileSystem().getHomeServer(session: fileProviderData.shared.session) } else { if let metadata = providerUtility.getTableMetadataFromItemIdentifier(enumeratedItemIdentifier), - let directorySource = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) { + let directorySource = self.database.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) { serverUrl = directorySource.serverUrl + "/" + metadata.fileName } @@ -55,15 +56,15 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { if enumeratedItemIdentifier == .workingSet { var itemIdentifierMetadata: [NSFileProviderItemIdentifier: tableMetadata] = [:] /// Tags - let tags = NCManageDatabase.shared.getTags(predicate: NSPredicate(format: "account == %@", fileProviderData.shared.account)) + let tags = self.database.getTags(predicate: NSPredicate(format: "account == %@", fileProviderData.shared.session.account)) for tag in tags { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(tag.ocId) else { continue } + guard let metadata = self.database.getMetadataFromOcId(tag.ocId) else { continue } itemIdentifierMetadata[providerUtility.getItemIdentifier(metadata: metadata)] = metadata } /// Favorite - fileProviderData.shared.listFavoriteIdentifierRank = NCManageDatabase.shared.getTableMetadatasDirectoryFavoriteIdentifierRank(account: fileProviderData.shared.account) + fileProviderData.shared.listFavoriteIdentifierRank = self.database.getTableMetadatasDirectoryFavoriteIdentifierRank(account: fileProviderData.shared.session.account) for (identifier, _) in fileProviderData.shared.listFavoriteIdentifierRank { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(identifier) else { continue } + guard let metadata = self.database.getMetadataFromOcId(identifier) else { continue } itemIdentifierMetadata[providerUtility.getItemIdentifier(metadata: metadata)] = metadata } /// Create items @@ -90,7 +91,7 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { self.fetchItemsForPage(serverUrl: serverUrl, pageNumber: pageNumber) { metadatas in if let metadatas { for metadata in metadatas { - if metadata.e2eEncrypted || (!metadata.session.isEmpty && metadata.session != NCNetworking.shared.sessionUploadBackgroundExtension) { + if metadata.e2eEncrypted || (!metadata.session.isEmpty && metadata.session != NCNetworking.shared.sessionUploadBackgroundExt) { continue } if let parentItemIdentifier = self.providerUtility.getParentItemIdentifier(metadata: metadata) { @@ -157,25 +158,25 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { } func fetchItemsForPage(serverUrl: String, pageNumber: Int, completionHandler: @escaping (_ metadatas: Results?) -> Void) { - let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@", fileProviderData.shared.account, serverUrl) + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@", fileProviderData.shared.session.account, serverUrl) if pageNumber == 1 { - NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrl, depth: "1", showHiddenFiles: NCKeychain().showHiddenFiles, account: fileProviderData.shared.account) { _, files, _, error in - if error == .success { - NCManageDatabase.shared.convertFilesToMetadatas(files, useFirstAsMetadataFolder: true) { metadataFolder, metadatas in + NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrl, depth: "1", showHiddenFiles: NCKeychain().showHiddenFiles, account: fileProviderData.shared.session.account) { _, files, _, error in + if error == .success, let files { + self.database.convertFilesToMetadatas(files, useFirstAsMetadataFolder: true) { metadataFolder, metadatas in /// FOLDER - NCManageDatabase.shared.addMetadata(metadataFolder) - NCManageDatabase.shared.addDirectory(e2eEncrypted: metadataFolder.e2eEncrypted, favorite: metadataFolder.favorite, ocId: metadataFolder.ocId, fileId: metadataFolder.fileId, etag: metadataFolder.etag, permissions: metadataFolder.permissions, richWorkspace: metadataFolder.richWorkspace, serverUrl: serverUrl, account: metadataFolder.account) + self.database.addMetadata(metadataFolder) + self.database.addDirectory(e2eEncrypted: metadataFolder.e2eEncrypted, favorite: metadataFolder.favorite, ocId: metadataFolder.ocId, fileId: metadataFolder.fileId, etag: metadataFolder.etag, permissions: metadataFolder.permissions, richWorkspace: metadataFolder.richWorkspace, serverUrl: serverUrl, account: metadataFolder.account) /// FILES - NCManageDatabase.shared.deleteMetadata(predicate: predicate) - NCManageDatabase.shared.addMetadatas(metadatas) + self.database.deleteMetadata(predicate: predicate) + self.database.addMetadatas(metadatas) } } - let resultsMetadata = NCManageDatabase.shared.fetchPagedResults(ofType: tableMetadata.self, primaryKey: "ocId", recordsPerPage: self.recordsPerPage, pageNumber: pageNumber, filter: predicate, sortedByKeyPath: "fileName") + let resultsMetadata = self.database.fetchPagedResults(ofType: tableMetadata.self, primaryKey: "ocId", recordsPerPage: self.recordsPerPage, pageNumber: pageNumber, filter: predicate, sortedByKeyPath: "fileName") completionHandler(resultsMetadata) } } else { - let resultsMetadata = NCManageDatabase.shared.fetchPagedResults(ofType: tableMetadata.self, primaryKey: "ocId", recordsPerPage: recordsPerPage, pageNumber: pageNumber, filter: predicate, sortedByKeyPath: "fileName") + let resultsMetadata = self.database.fetchPagedResults(ofType: tableMetadata.self, primaryKey: "ocId", recordsPerPage: recordsPerPage, pageNumber: pageNumber, filter: predicate, sortedByKeyPath: "fileName") completionHandler(resultsMetadata) } } diff --git a/File Provider Extension/FileProviderExtension+Actions.swift b/File Provider Extension/FileProviderExtension+Actions.swift index 1f5f1811a3..65d4b9f464 100644 --- a/File Provider Extension/FileProviderExtension+Actions.swift +++ b/File Provider Extension/FileProviderExtension+Actions.swift @@ -27,23 +27,23 @@ import NextcloudKit extension FileProviderExtension { override func createDirectory(withName directoryName: String, inParentItemIdentifier parentItemIdentifier: NSFileProviderItemIdentifier, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) { - guard let tableDirectory = providerUtility.getTableDirectoryFromParentItemIdentifier(parentItemIdentifier, account: fileProviderData.shared.account, homeServerUrl: fileProviderData.shared.homeServerUrl) else { + guard let tableDirectory = providerUtility.getTableDirectoryFromParentItemIdentifier(parentItemIdentifier, account: fileProviderData.shared.session.account, homeServerUrl: utilityFileSystem.getHomeServer(session: fileProviderData.shared.session)) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } - let directoryName = utilityFileSystem.createFileName(directoryName, serverUrl: tableDirectory.serverUrl, account: fileProviderData.shared.account) + let directoryName = utilityFileSystem.createFileName(directoryName, serverUrl: tableDirectory.serverUrl, account: fileProviderData.shared.session.account) let serverUrlFileName = tableDirectory.serverUrl + "/" + directoryName - NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: fileProviderData.shared.account) { _, ocId, _, error in + NextcloudKit.shared.createFolder(serverUrlFileName: serverUrlFileName, account: fileProviderData.shared.session.account) { _, ocId, _, _, error in if error == .success { - NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", showHiddenFiles: NCKeychain().showHiddenFiles, account: fileProviderData.shared.account) { _, files, _, error in - if error == .success, let file = files.first { + NextcloudKit.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", showHiddenFiles: NCKeychain().showHiddenFiles, account: fileProviderData.shared.session.account) { _, files, _, error in + if error == .success, let file = files?.first { let isDirectoryEncrypted = self.utilityFileSystem.isDirectoryE2EE(file: file) - let metadata = NCManageDatabase.shared.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryEncrypted) + let metadata = self.database.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryEncrypted) - NCManageDatabase.shared.addDirectory(e2eEncrypted: false, favorite: false, ocId: ocId!, fileId: metadata.fileId, etag: metadata.etag, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account) - NCManageDatabase.shared.addMetadata(metadata) + self.database.addDirectory(e2eEncrypted: false, favorite: false, ocId: ocId!, fileId: metadata.fileId, etag: metadata.etag, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account) + self.database.addMetadata(metadata) - guard let metadataInsert = NCManageDatabase.shared.getMetadataFromOcId(ocId!), + guard let metadataInsert = self.database.getMetadataFromOcId(ocId!), let parentItemIdentifier = self.providerUtility.getParentItemIdentifier(metadata: metadataInsert) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } @@ -69,7 +69,7 @@ extension FileProviderExtension { let serverUrl = metadata.serverUrl let fileName = metadata.fileName - NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: metadata.account) { account, error in + NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: metadata.account) { account, _, error in if error == .success { let fileNamePath = self.utilityFileSystem.getDirectoryProviderStorageOcId(itemIdentifier.rawValue) @@ -81,11 +81,11 @@ extension FileProviderExtension { if isDirectory { let dirForDelete = self.utilityFileSystem.stringAppendServerUrl(serverUrl, addFileName: fileName) - NCManageDatabase.shared.deleteDirectoryAndSubDirectory(serverUrl: dirForDelete, account: account) + self.database.deleteDirectoryAndSubDirectory(serverUrl: dirForDelete, account: account) } - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", ocId)) - NCManageDatabase.shared.deleteLocalFile(predicate: NSPredicate(format: "ocId == %@", ocId)) + self.database.deleteMetadataOcId(ocId) + self.database.deleteLocalFileOcId(ocId) completionHandler(nil) } else { completionHandler(NSFileProviderError(.serverUnreachable)) @@ -101,38 +101,41 @@ extension FileProviderExtension { let ocIdFrom = metadataFrom.ocId let serverUrlFrom = metadataFrom.serverUrl let fileNameFrom = serverUrlFrom + "/" + itemFrom.filename - guard let tableDirectoryTo = providerUtility.getTableDirectoryFromParentItemIdentifier(parentItemIdentifier, account: fileProviderData.shared.account, homeServerUrl: fileProviderData.shared.homeServerUrl) else { + + guard let tableDirectoryTo = providerUtility.getTableDirectoryFromParentItemIdentifier(parentItemIdentifier, account: fileProviderData.shared.session.account, homeServerUrl: utilityFileSystem.getHomeServer(session: fileProviderData.shared.session)) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } let serverUrlTo = tableDirectoryTo.serverUrl - let fileNameTo = serverUrlTo + "/" + itemFrom.filename + var fileNameTo = serverUrlTo + "/" + itemFrom.filename + if let newName { + fileNameTo = serverUrlTo + "/" + newName + } - NextcloudKit.shared.moveFileOrFolder(serverUrlFileNameSource: fileNameFrom, serverUrlFileNameDestination: fileNameTo, overwrite: false, account: metadataFrom.account) { account, error in - if error == .success { - if metadataFrom.directory { - NCManageDatabase.shared.deleteDirectoryAndSubDirectory(serverUrl: serverUrlFrom, account: account) - NCManageDatabase.shared.renameDirectory(ocId: ocIdFrom, serverUrl: serverUrlTo) - } - NCManageDatabase.shared.moveMetadata(ocId: ocIdFrom, serverUrlTo: serverUrlTo) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + NextcloudKit.shared.moveFileOrFolder(serverUrlFileNameSource: fileNameFrom, serverUrlFileNameDestination: fileNameTo, overwrite: true, account: metadataFrom.account) { account, _, error in + if error == .success { + if metadataFrom.directory { + self.database.deleteDirectoryAndSubDirectory(serverUrl: serverUrlFrom, account: account) + self.database.renameDirectory(ocId: ocIdFrom, serverUrl: serverUrlTo) + } + self.database.moveMetadata(ocId: ocIdFrom, serverUrlTo: serverUrlTo) - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocIdFrom) else { - return completionHandler(nil, NSFileProviderError(.noSuchItem)) + guard let metadata = self.database.getMetadataFromOcId(ocIdFrom) else { + return completionHandler(nil, NSFileProviderError(.noSuchItem)) - } - let item = FileProviderItem(metadata: metadata, parentItemIdentifier: parentItemIdentifier) + } + let item = FileProviderItem(metadata: metadata, parentItemIdentifier: parentItemIdentifier) - completionHandler(item, nil) - } else if error.errorCode == NCGlobal.shared.errorBadRequest { - completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: error.errorDescription, NSLocalizedFailureReasonErrorKey: ""])) - } else { - completionHandler(nil, NSFileProviderError(.serverUnreachable)) + completionHandler(item, nil) + } else { + completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: error.errorDescription, NSLocalizedFailureReasonErrorKey: ""])) + } } } } override func renameItem(withIdentifier itemIdentifier: NSFileProviderItemIdentifier, toName itemName: String, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) { - guard let metadata = providerUtility.getTableMetadataFromItemIdentifier(itemIdentifier), - let directoryTable = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) else { + guard let metadata = providerUtility.getTableMetadataFromItemIdentifier(itemIdentifier) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } let fileNameFrom = metadata.fileNameView @@ -140,30 +143,22 @@ extension FileProviderExtension { let fileNamePathTo = metadata.serverUrl + "/" + itemName let ocId = metadata.ocId - NextcloudKit.shared.moveFileOrFolder(serverUrlFileNameSource: fileNamePathFrom, serverUrlFileNameDestination: fileNamePathTo, overwrite: false, account: metadata.account) { account, error in + NextcloudKit.shared.moveFileOrFolder(serverUrlFileNameSource: fileNamePathFrom, serverUrlFileNameDestination: fileNamePathTo, overwrite: false, account: metadata.account) { _, _, error in if error == .success { - // Rename metadata - NCManageDatabase.shared.renameMetadata(fileNameTo: itemName, ocId: ocId) - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { + self.database.renameMetadata(fileNameNew: itemName, ocId: ocId) + self.database.setMetadataServeUrlFileNameStatusNormal(ocId: ocId) + + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } - if metadata.directory { - NCManageDatabase.shared.setDirectory(serverUrl: fileNamePathFrom, serverUrlTo: fileNamePathTo, encrypted: directoryTable.e2eEncrypted, account: account) - } else { - let itemIdentifier = self.providerUtility.getItemIdentifier(metadata: metadata) - self.providerUtility.moveFile(self.utilityFileSystem.getDirectoryProviderStorageOcId(itemIdentifier.rawValue, fileNameView: fileNameFrom), toPath: self.utilityFileSystem.getDirectoryProviderStorageOcId(itemIdentifier.rawValue, fileNameView: itemName)) - self.providerUtility.moveFile(self.utilityFileSystem.getDirectoryProviderStoragePreviewOcId(itemIdentifier.rawValue, etag: metadata.etag), toPath: self.utilityFileSystem.getDirectoryProviderStoragePreviewOcId(itemIdentifier.rawValue, etag: metadata.etag)) - self.providerUtility.moveFile(self.utilityFileSystem.getDirectoryProviderStorageIconOcId(itemIdentifier.rawValue, etag: metadata.etag), toPath: self.utilityFileSystem.getDirectoryProviderStorageIconOcId(itemIdentifier.rawValue, etag: metadata.etag)) - NCManageDatabase.shared.setLocalFile(ocId: ocId, fileName: itemName) - } guard let parentItemIdentifier = self.providerUtility.getParentItemIdentifier(metadata: metadata) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } let item = FileProviderItem(metadata: tableMetadata.init(value: metadata), parentItemIdentifier: parentItemIdentifier) completionHandler(item, nil) - } else if error.errorCode == NCGlobal.shared.errorBadRequest { + } else if error.errorCode == NCGlobal.shared.errorBadRequest { completionHandler(nil, NSFileProviderError(.noSuchItem, userInfo: [NSLocalizedDescriptionKey: error.errorDescription, NSLocalizedFailureReasonErrorKey: ""])) } else { completionHandler(nil, NSFileProviderError(.serverUnreachable)) @@ -188,20 +183,20 @@ extension FileProviderExtension { } if (favorite == true && metadata.favorite == false) || (favorite == false && metadata.favorite == true) { - let fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId) - NextcloudKit.shared.setFavorite(fileName: fileNamePath, favorite: favorite, account: metadata.account) { _, error in + let fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: fileProviderData.shared.session) + NextcloudKit.shared.setFavorite(fileName: fileNamePath, favorite: favorite, account: metadata.account) { _, _, error in if error == .success { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } // Change DB metadata.favorite = favorite - NCManageDatabase.shared.addMetadata(metadata) + self.database.addMetadata(metadata) /// SIGNAL let item = fileProviderData.shared.signalEnumerator(ocId: metadata.ocId, type: .workingSet) completionHandler(item, nil) } else { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } // Errore, remove from listFavoriteIdentifierRank @@ -221,7 +216,7 @@ extension FileProviderExtension { let ocId = metadataForTag.ocId let account = metadataForTag.account - NCManageDatabase.shared.addTag(ocId, tagIOS: tagData, account: account) + self.database.addTag(ocId, tagIOS: tagData, account: account) /// SIGNAL WORKINGSET let item = fileProviderData.shared.signalEnumerator(ocId: ocId, type: .workingSet) completionHandler(item, nil) diff --git a/File Provider Extension/FileProviderExtension+NetworkingDelegate.swift b/File Provider Extension/FileProviderExtension+NetworkingDelegate.swift index a5d7ec5f5d..109068b0ed 100644 --- a/File Provider Extension/FileProviderExtension+NetworkingDelegate.swift +++ b/File Provider Extension/FileProviderExtension+NetworkingDelegate.swift @@ -37,10 +37,19 @@ extension FileProviderExtension: NCNetworkingDelegate { guard let url = task.currentRequest?.url, let metadata = NCManageDatabase.shared.getMetadata(from: url, sessionTaskIdentifier: task.taskIdentifier) else { return } - DispatchQueue.global(qos: .userInteractive).async { + if let ocId, !metadata.ocIdTransfer.isEmpty { + let atPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocIdTransfer) + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocId) + self.utilityFileSystem.copyFile(atPath: atPath, toPath: toPath) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { if error == .success, let ocId { /// SIGNAL - fileProviderData.shared.signalEnumerator(ocId: metadata.ocIdTemp, type: .delete) + fileProviderData.shared.signalEnumerator(ocId: metadata.ocIdTransfer, type: .delete) + if !metadata.ocIdTransfer.isEmpty, ocId != metadata.ocIdTransfer { + NCManageDatabase.shared.deleteMetadataOcId(metadata.ocIdTransfer) + } metadata.fileName = fileName metadata.serverUrl = serverUrl metadata.uploadDate = (date as? NSDate) ?? NSDate() @@ -61,19 +70,13 @@ extension FileProviderExtension: NCNetworkingDelegate { NCManageDatabase.shared.addMetadata(metadata) NCManageDatabase.shared.addLocalFile(metadata: metadata) - /// NEW File - if !metadata.ocIdTemp.isEmpty, ocId != metadata.ocIdTemp { - let atPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocIdTemp) - let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocId) - self.utilityFileSystem.copyFile(atPath: atPath, toPath: toPath) - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", metadata.ocIdTemp)) - } + /// SIGNAL fileProviderData.shared.signalEnumerator(ocId: metadata.ocId, type: .update) } else { - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", metadata.ocIdTemp)) + NCManageDatabase.shared.deleteMetadataOcId(metadata.ocIdTransfer) /// SIGNAL - fileProviderData.shared.signalEnumerator(ocId: metadata.ocIdTemp, type: .delete) + fileProviderData.shared.signalEnumerator(ocId: metadata.ocIdTransfer, type: .delete) } } } diff --git a/File Provider Extension/FileProviderExtension+Thumbnail.swift b/File Provider Extension/FileProviderExtension+Thumbnail.swift index 70bfcb550a..0cffbb8515 100644 --- a/File Provider Extension/FileProviderExtension+Thumbnail.swift +++ b/File Provider Extension/FileProviderExtension+Thumbnail.swift @@ -24,6 +24,7 @@ import UIKit import FileProvider import NextcloudKit +import Alamofire extension FileProviderExtension { override func fetchThumbnails(for itemIdentifiers: [NSFileProviderItemIdentifier], requestedSize size: CGSize, perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier, Data?, Error?) -> Void, completionHandler: @escaping (Error?) -> Void) -> Progress { @@ -36,14 +37,14 @@ extension FileProviderExtension { if counterProgress == progress.totalUnitCount { completionHandler(nil) } continue } - let fileNameIconLocalPath = utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag) - NextcloudKit.shared.downloadPreview(fileId: metadata.fileId, widthPreview: Int(size.width), heightPreview: Int(size.height), etag: metadata.etag, account: metadata.account) { _ in - } completion: { _, data, error in - if error == .success, let data { - do { - try data.write(to: URL(fileURLWithPath: fileNameIconLocalPath), options: .atomic) - } catch { } + NextcloudKit.shared.downloadPreview(fileId: metadata.fileId, + width: Int(size.width), + height: Int(size.height), + etag: metadata.etag, + account: metadata.account) { _ in + } completion: { _, _, _, _, responseData, error in + if error == .success, let data = responseData?.data { perThumbnailCompletionHandler(itemIdentifier, data, nil) } else { perThumbnailCompletionHandler(itemIdentifier, nil, NSFileProviderError(.serverUnreachable)) diff --git a/File Provider Extension/FileProviderExtension.swift b/File Provider Extension/FileProviderExtension.swift index 1666cd625a..b094be1174 100644 --- a/File Provider Extension/FileProviderExtension.swift +++ b/File Provider Extension/FileProviderExtension.swift @@ -55,16 +55,13 @@ import Alamofire class FileProviderExtension: NSFileProviderExtension { let providerUtility = fileProviderUtility() let utilityFileSystem = NCUtilityFileSystem() + let database = NCManageDatabase.shared override init() { super.init() - // Create directory File Provider Storage _ = utilityFileSystem.directoryProviderStorage - // Configure URLSession - _ = NCNetworking.shared.sessionManagerUploadBackgroundExtension - // Domains - // FileProviderDomain().registerDomains() + _ = fileProviderData.shared.setupAccount(domain: domain, providerExtension: self) } deinit { @@ -114,12 +111,12 @@ class FileProviderExtension: NSFileProviderExtension { override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem { if identifier == .rootContainer { let metadata = tableMetadata() - metadata.account = fileProviderData.shared.account + metadata.account = fileProviderData.shared.session.account metadata.directory = true metadata.ocId = NSFileProviderItemIdentifier.rootContainer.rawValue metadata.fileName = "root" metadata.fileNameView = "root" - metadata.serverUrl = fileProviderData.shared.homeServerUrl + metadata.serverUrl = utilityFileSystem.getHomeServer(session: fileProviderData.shared.session) metadata.classFile = NKCommon.TypeClassFile.directory.rawValue return FileProviderItem(metadata: metadata, parentItemIdentifier: NSFileProviderItemIdentifier(NSFileProviderItemIdentifier.rootContainer.rawValue)) } else { @@ -170,35 +167,36 @@ class FileProviderExtension: NSFileProviderExtension { if let result = fileProviderData.shared.getUploadMetadata(id: itemIdentifier.rawValue) { metadata = result.metadata } else { - metadata = NCManageDatabase.shared.getMetadataFromOcIdAndOcIdTemp(itemIdentifier.rawValue) + metadata = self.database.getMetadataFromOcIdAndocIdTransfer(itemIdentifier.rawValue) } guard let metadata else { return completionHandler(NSFileProviderError(.noSuchItem)) } - if metadata.session == NCNetworking.shared.sessionUploadBackgroundExtension { + if metadata.session == NCNetworking.shared.sessionUploadBackgroundExt { return completionHandler(nil) } let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileName) // Exists ? return - if let tableLocalFile = NCManageDatabase.shared.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)), + if let tableLocalFile = self.database.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)), utilityFileSystem.fileProviderStorageExists(metadata), tableLocalFile.etag == metadata.etag { return completionHandler(nil) } else { - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - session: NextcloudKit.shared.nkCommonInstance.sessionIdentifierDownload, - sessionError: "", - selector: "", - status: NCGlobal.shared.metadataStatusDownloading) + self.database.setMetadataSession(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + sessionTaskIdentifier: 0, + sessionError: "", + selector: "", + status: NCGlobal.shared.metadataStatusDownloading) } /// SIGNAL fileProviderData.shared.signalEnumerator(ocId: metadata.ocId, type: .update) NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: metadata.account, requestHandler: { _ in }, taskHandler: { task in - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - taskIdentifier: task.taskIdentifier) + self.database.setMetadataSession(ocId: metadata.ocId, + sessionTaskIdentifier: task.taskIdentifier) fileProviderData.shared.fileProviderManager.register(task, forItemWithIdentifier: NSFileProviderItemIdentifier(itemIdentifier.rawValue)) { _ in } }, progressHandler: { _ in }) { _, etag, date, _, _, _, error in @@ -215,16 +213,16 @@ class FileProviderExtension: NSFileProviderExtension { metadata.status = NCGlobal.shared.metadataStatusNormal metadata.date = (date as? NSDate) ?? NSDate() metadata.etag = etag ?? "" - NCManageDatabase.shared.addLocalFile(metadata: metadata) - NCManageDatabase.shared.addMetadata(metadata) + self.database.addLocalFile(metadata: metadata) + self.database.addMetadata(metadata) completionHandler(nil) } else if error.errorCode == 200 { - NCManageDatabase.shared.setMetadataStatus(ocId: metadata.ocId, status: NCGlobal.shared.metadataStatusNormal) + self.database.setMetadataStatus(ocId: metadata.ocId, status: NCGlobal.shared.metadataStatusNormal) completionHandler(nil) } else { metadata.status = NCGlobal.shared.metadataStatusDownloadError metadata.sessionError = error.errorDescription - NCManageDatabase.shared.addMetadata(metadata) + self.database.addMetadata(metadata) completionHandler(NSFileProviderError(.noSuchItem)) } /// SIGNAL @@ -242,23 +240,33 @@ class FileProviderExtension: NSFileProviderExtension { if let result = fileProviderData.shared.getUploadMetadata(id: itemIdentifier.rawValue) { metadata = result.metadata } else { - metadata = NCManageDatabase.shared.getMetadataFromOcIdAndOcIdTemp(itemIdentifier.rawValue) + metadata = self.database.getMetadataFromOcIdAndocIdTransfer(itemIdentifier.rawValue) } guard let metadata else { return } let serverUrlFileName = metadata.serverUrl + "/" + fileName - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - session: NCNetworking.shared.sessionUploadBackgroundExtension, - sessionError: "", - selector: "", - status: NCGlobal.shared.metadataStatusUploading) - if let task = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance).upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: url.path, dateCreationFile: nil, dateModificationFile: nil, account: metadata.account, session: NCNetworking.shared.sessionManagerUploadBackgroundExtension) { - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - status: NCGlobal.shared.metadataStatusUploading, - taskIdentifier: task.taskIdentifier) - fileProviderData.shared.fileProviderManager.register(task, forItemWithIdentifier: NSFileProviderItemIdentifier(metadata.fileId)) { _ in } + self.database.setMetadataSession(ocId: metadata.ocId, + session: NCNetworking.shared.sessionUploadBackgroundExt, + sessionTaskIdentifier: 0, + sessionError: "", + selector: "", + status: NCGlobal.shared.metadataStatusUploading) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if let task = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance).upload(serverUrlFileName: serverUrlFileName, + fileNameLocalPath: url.path, + dateCreationFile: nil, + dateModificationFile: nil, + overwrite: true, + account: metadata.account, + sessionIdentifier: metadata.session) { + self.database.setMetadataSession(ocId: metadata.ocId, + sessionTaskIdentifier: task.taskIdentifier, + status: NCGlobal.shared.metadataStatusUploading) + fileProviderData.shared.fileProviderManager.register(task, forItemWithIdentifier: NSFileProviderItemIdentifier(metadata.fileId)) { _ in } + } } } @@ -266,9 +274,10 @@ class FileProviderExtension: NSFileProviderExtension { let pathComponents = url.pathComponents assert(pathComponents.count > 2) let itemIdentifier = NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2]) - guard let metadata = NCManageDatabase.shared.getMetadataFromOcIdAndOcIdTemp(itemIdentifier.rawValue) else { return } - if metadata.session == NextcloudKit.shared.nkCommonInstance.sessionIdentifierDownload { - NextcloudKit.shared.sessionManager.session.getTasksWithCompletionHandler { _, _, downloadTasks in + guard let metadata = self.database.getMetadataFromOcIdAndocIdTransfer(itemIdentifier.rawValue) else { return } + if metadata.session == NCNetworking.shared.sessionDownload { + let session = NextcloudKit.shared.nkCommonInstance.getSession(account: metadata.session)?.sessionData.session + session?.getTasksWithCompletionHandler { _, _, downloadTasks in downloadTasks.forEach { task in if metadata.sessionTaskIdentifier == task.taskIdentifier { task.cancel() @@ -281,7 +290,7 @@ class FileProviderExtension: NSFileProviderExtension { override func importDocument(at fileURL: URL, toParentItemIdentifier parentItemIdentifier: NSFileProviderItemIdentifier, completionHandler: @escaping (NSFileProviderItem?, Error?) -> Void) { DispatchQueue.main.async { autoreleasepool { - guard let tableDirectory = self.providerUtility.getTableDirectoryFromParentItemIdentifier(parentItemIdentifier, account: fileProviderData.shared.account, homeServerUrl: fileProviderData.shared.homeServerUrl) else { + guard let tableDirectory = self.providerUtility.getTableDirectoryFromParentItemIdentifier(parentItemIdentifier, account: fileProviderData.shared.session.account, homeServerUrl: self.utilityFileSystem.getHomeServer(session: fileProviderData.shared.session)) else { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } var size = 0 as Int64 @@ -299,34 +308,48 @@ class FileProviderExtension: NSFileProviderExtension { return completionHandler(nil, NSFileProviderError(.noSuchItem)) } - let fileName = self.utilityFileSystem.createFileName(fileURL.lastPathComponent, serverUrl: tableDirectory.serverUrl, account: fileProviderData.shared.account) - let ocIdTemp = NSUUID().uuidString.lowercased() + let fileName = self.utilityFileSystem.createFileName(fileURL.lastPathComponent, serverUrl: tableDirectory.serverUrl, account: fileProviderData.shared.session.account) + let ocIdTransfer = NSUUID().uuidString.lowercased() NSFileCoordinator().coordinate(readingItemAt: fileURL, options: .withoutChanges, error: &error) { url in - self.providerUtility.copyFile(url.path, toPath: self.utilityFileSystem.getDirectoryProviderStorageOcId(ocIdTemp, fileNameView: fileName)) + self.providerUtility.copyFile(url.path, toPath: self.utilityFileSystem.getDirectoryProviderStorageOcId(ocIdTransfer, fileNameView: fileName)) } fileURL.stopAccessingSecurityScopedResource() - let metadata = NCManageDatabase.shared.createMetadata(account: fileProviderData.shared.account, user: fileProviderData.shared.user, userId: fileProviderData.shared.userId, fileName: fileName, fileNameView: fileName, ocId: ocIdTemp, serverUrl: tableDirectory.serverUrl, urlBase: fileProviderData.shared.accountUrlBase, url: "", contentType: "") - metadata.session = NCNetworking.shared.sessionUploadBackgroundExtension - metadata.size = size - metadata.status = NCGlobal.shared.metadataStatusUploading + let metadataForUpload = self.database.createMetadata(fileName: fileName, + fileNameView: fileName, + ocId: ocIdTransfer, + serverUrl: tableDirectory.serverUrl, + url: "", + contentType: "", + session: fileProviderData.shared.session, + sceneIdentifier: nil) + + metadataForUpload.session = NCNetworking.shared.sessionUploadBackgroundExt + metadataForUpload.size = size + metadataForUpload.status = NCGlobal.shared.metadataStatusUploading - NCManageDatabase.shared.addMetadata(metadata) + self.database.addMetadata(metadataForUpload) let serverUrlFileName = tableDirectory.serverUrl + "/" + fileName - let fileNameLocalPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocIdTemp, fileNameView: fileName) - - if let task = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance).upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, dateCreationFile: nil, dateModificationFile: nil, account: metadata.account, session: NCNetworking.shared.sessionManagerUploadBackgroundExtension) { - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - status: NCGlobal.shared.metadataStatusUploading, - taskIdentifier: task.taskIdentifier) - fileProviderData.shared.fileProviderManager.register(task, forItemWithIdentifier: NSFileProviderItemIdentifier(ocIdTemp)) { _ in } - fileProviderData.shared.appendUploadMetadata(id: ocIdTemp, metadata: metadata, task: task) + let fileNameLocalPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocIdTransfer, fileNameView: fileName) + + if let task = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance).upload(serverUrlFileName: serverUrlFileName, + fileNameLocalPath: fileNameLocalPath, + dateCreationFile: nil, + dateModificationFile: nil, + overwrite: true, + account: metadataForUpload.account, + sessionIdentifier: metadataForUpload.session) { + self.database.setMetadataSession(ocId: metadataForUpload.ocId, + sessionTaskIdentifier: task.taskIdentifier, + status: NCGlobal.shared.metadataStatusUploading) + fileProviderData.shared.fileProviderManager.register(task, forItemWithIdentifier: NSFileProviderItemIdentifier(ocIdTransfer)) { _ in } + fileProviderData.shared.appendUploadMetadata(id: ocIdTransfer, metadata: metadataForUpload, task: task) } - let item = FileProviderItem(metadata: tableMetadata.init(value: metadata), parentItemIdentifier: parentItemIdentifier) + let item = FileProviderItem(metadata: tableMetadata.init(value: metadataForUpload), parentItemIdentifier: parentItemIdentifier) completionHandler(item, nil) } } diff --git a/File Provider Extension/FileProviderItem.swift b/File Provider Extension/FileProviderItem.swift index e5f2ba0cc9..52ba12150c 100644 --- a/File Provider Extension/FileProviderItem.swift +++ b/File Provider Extension/FileProviderItem.swift @@ -36,7 +36,7 @@ class FileProviderItem: NSObject, NSFileProviderItem { return metadata.fileNameView } var typeIdentifier: String { - let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, mimeType: "", directory: metadata.directory) + let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, mimeType: "", directory: metadata.directory, account: metadata.account) return results.typeIdentifier } var capabilities: NSFileProviderItemCapabilities { diff --git a/File Provider Extension/FileProviderUtility.swift b/File Provider Extension/FileProviderUtility.swift index 037dc21a08..f2c3fefde5 100644 --- a/File Provider Extension/FileProviderUtility.swift +++ b/File Provider Extension/FileProviderUtility.swift @@ -26,15 +26,16 @@ import UIKit class fileProviderUtility: NSObject { let fileManager = FileManager() let utilityFileSystem = NCUtilityFileSystem() + let database = NCManageDatabase.shared func getAccountFromItemIdentifier(_ itemIdentifier: NSFileProviderItemIdentifier) -> String? { let ocId = itemIdentifier.rawValue - return NCManageDatabase.shared.getMetadataFromOcId(ocId)?.account + return self.database.getMetadataFromOcId(ocId)?.account } func getTableMetadataFromItemIdentifier(_ itemIdentifier: NSFileProviderItemIdentifier) -> tableMetadata? { let ocId = itemIdentifier.rawValue - return NCManageDatabase.shared.getMetadataFromOcId(ocId) + return self.database.getMetadataFromOcId(ocId) } func getItemIdentifier(metadata: tableMetadata) -> NSFileProviderItemIdentifier { @@ -42,13 +43,13 @@ class fileProviderUtility: NSObject { } func getParentItemIdentifier(metadata: tableMetadata) -> NSFileProviderItemIdentifier? { - let homeServerUrl = utilityFileSystem.getHomeServer(urlBase: metadata.urlBase, userId: metadata.userId) - if let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) { + let homeServerUrl = utilityFileSystem.getHomeServer(session: fileProviderData.shared.session) + if let directory = self.database.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) { if directory.serverUrl == homeServerUrl { return NSFileProviderItemIdentifier(NSFileProviderItemIdentifier.rootContainer.rawValue) } else { // get the metadata.ocId of parent Directory - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(directory.ocId) { + if let metadata = self.database.getMetadataFromOcId(directory.ocId) { let identifier = getItemIdentifier(metadata: metadata) return identifier } @@ -65,7 +66,7 @@ class fileProviderUtility: NSObject { guard let metadata = getTableMetadataFromItemIdentifier(parentItemIdentifier) else { return nil } predicate = NSPredicate(format: "ocId == %@", metadata.ocId) } - guard let directory = NCManageDatabase.shared.getTableDirectory(predicate: predicate) else { return nil } + guard let directory = self.database.getTableDirectory(predicate: predicate) else { return nil } return directory } diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index a9fe1a92ed..489259c8f7 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -44,12 +44,8 @@ AF56C1DC2784856200D8BAE2 /* NCActivityCommentView.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF56C1DB2784856200D8BAE2 /* NCActivityCommentView.xib */; }; AF68326A27BE65A90010BF0B /* NCMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF68326927BE65A90010BF0B /* NCMenuAction.swift */; }; AF730AF827834B1400B7520E /* NCShare+NCCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF730AF727834B1400B7520E /* NCShare+NCCellDelegate.swift */; }; - AF730AFA27843E4C00B7520E /* NCShareExtension+NCDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF730AF927843E4C00B7520E /* NCShareExtension+NCDelegate.swift */; }; + AF730AFA27843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */; }; AF7E504E27A2D8FF00B5E4AF /* UIBarButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7E504D27A2D8FF00B5E4AF /* UIBarButton+Extension.swift */; }; - AF817EF1274BC781009ED85B /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; - AF817EF2274BC781009ED85B /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; - AF817EF3274BC781009ED85B /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; - AF817EF4274BC781009ED85B /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; AF8ED1FC2757821000B8DBC4 /* NextcloudUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8ED1FB2757821000B8DBC4 /* NextcloudUnitTests.swift */; }; AF93471227E2341B002537EE /* NCShare+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471127E2341B002537EE /* NCShare+Menu.swift */; }; AF93471927E2361E002537EE /* NCShareAdvancePermissionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */; }; @@ -71,6 +67,13 @@ F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */; }; F314F1142A30E2DE00BC7FAB /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A390295DC5E0006CB2D0 /* View+Extension.swift */; }; F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */; }; + F33918C42C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; + F33918C52C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; + F33918C62C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; + F33918C72C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; + F33918C82C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; + F33918C92C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; + F33918CA2C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */; }; F3391B082B4C52C5001C0C4B /* FirebaseDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = F3391B072B4C52C5001C0C4B /* FirebaseDatabase */; }; F3391B0A2B4C52CB001C0C4B /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = F3391B092B4C52CB001C0C4B /* JGProgressHUD */; }; F3391B0C2B4C52D5001C0C4B /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = F3391B0B2B4C52D5001C0C4B /* SVGKit */; }; @@ -157,6 +160,14 @@ F3BB464D2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F3BB464C2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib */; }; F3BB46522A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */; }; F3BB46542A3A1E9D00461F6E /* CCCellMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */; }; + F3E173B02C9AF637006D177A /* ScreenAwakeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */; }; + F3E173C02C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F3E173C12C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F3E173C22C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F3E173C32C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F3E173C42C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F3E173C52C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; + F3E173C62C9B1067006D177A /* AwakeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3E173BF2C9B1067006D177A /* AwakeMode.swift */; }; F3EF2E0C2BFCF3810025EF46 /* NCLoginPoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EF2E0B2BFCF3810025EF46 /* NCLoginPoll.swift */; }; F3F0419B2B9F7E6700D5155F /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F3F0419A2B9F7E6700D5155F /* RealmSwift */; }; F3F0419D2B9F7E6E00D5155F /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F3F0419C2B9F7E6E00D5155F /* RealmSwift */; }; @@ -221,6 +232,9 @@ F713FBE72C31646500F10760 /* NCNetworking+AsyncAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = F713FBE42C31645200F10760 /* NCNetworking+AsyncAwait.swift */; }; F713FBE82C31646600F10760 /* NCNetworking+AsyncAwait.swift in Sources */ = {isa = PBXBuildFile; fileRef = F713FBE42C31645200F10760 /* NCNetworking+AsyncAwait.swift */; }; F713FF002472764100214AF6 /* UIImage+animatedGIF.m in Sources */ = {isa = PBXBuildFile; fileRef = F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */; }; + F71433E52C778FFA00E20B5A /* NotificationCenter+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70460512499061800BB98A7 /* NotificationCenter+MainThread.swift */; }; + F71433E62C778FFB00E20B5A /* NotificationCenter+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70460512499061800BB98A7 /* NotificationCenter+MainThread.swift */; }; + F71433E72C778FFB00E20B5A /* NotificationCenter+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70460512499061800BB98A7 /* NotificationCenter+MainThread.swift */; }; F7145610296433C80038D028 /* NCDocumentCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = F714560F296433C80038D028 /* NCDocumentCamera.swift */; }; F7145A231D12E3B700CAFEEC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F7E70DE91A24DE4100E1B66A /* Localizable.strings */; }; F714803B262EBE3900693E51 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F714803A262EBE3900693E51 /* MainInterface.storyboard */; }; @@ -318,7 +332,6 @@ F7346E2328B0FEBA006CE2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7346E2228B0FEBA006CE2D2 /* Assets.xcassets */; }; F734B06628E75C0100E180D5 /* TLPhotoPicker in Frameworks */ = {isa = PBXBuildFile; productRef = F734B06528E75C0100E180D5 /* TLPhotoPicker */; }; F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */; }; - F737DA992B7B864E0063BAFC /* TOPasscodeViewController.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86822642CF5500ED5349 /* TOPasscodeViewController.xcframework */; }; F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; F737DA9F2B7B8AB90063BAFC /* NCLoginNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F738D48F2756740100CD1D38 /* NCLoginNavigationController.swift */; }; @@ -400,8 +413,16 @@ F73EF7EC2B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7E62B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift */; }; F73EF7ED2B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7E62B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift */; }; F73F537F1E929C8500F8678D /* NCMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73F537E1E929C8500F8678D /* NCMore.swift */; }; + F7401C152C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; + F7401C162C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; + F7401C172C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; + F7401C182C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; + F7401C192C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; + F7401C1A2C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; + F7401C1B2C75E6F300649E87 /* NCCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7401C142C75E6F300649E87 /* NCCapabilities.swift */; }; F740BEF02A35C2AD00E9B6D5 /* UILabel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */; }; F741C2242B6B9FD600E849BB /* NCMediaSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F741C2232B6B9FD600E849BB /* NCMediaSelectTabBar.swift */; }; + F74230F32C79B57200CA1ACA /* NCNetworking+Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74230F22C79B57200CA1ACA /* NCNetworking+Task.swift */; }; F7434B3620E23FE000417916 /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; F7434B3820E2400600417916 /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; F745B253222D88AE00346520 /* NCLoginQRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F745B252222D88AE00346520 /* NCLoginQRCode.swift */; }; @@ -417,7 +438,6 @@ F7490E7229882BB4009DCE94 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F7490E7129882BB4009DCE94 /* RealmSwift */; }; F7490E7429882BCC009DCE94 /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; F7490E7529882BE2009DCE94 /* NCManageDatabase+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78A10BE29322E8A008499B8 /* NCManageDatabase+Directory.swift */; }; - F7490E7629882BF3009DCE94 /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; F7490E7729882C10009DCE94 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; F7490E7829882C28009DCE94 /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; F7490E7E29882C6E009DCE94 /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; }; @@ -457,10 +477,13 @@ F74B6D992A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B6D942A7E239A00F03C5F /* NCManageDatabase+Chunk.swift */; }; F74B6D9A2A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B6D942A7E239A00F03C5F /* NCManageDatabase+Chunk.swift */; }; F74B6D9B2A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B6D942A7E239A00F03C5F /* NCManageDatabase+Chunk.swift */; }; + F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76673EC22C901F5007ED366 /* FileProviderDomain.swift */; }; F74C0436253F1CDC009762AB /* NCShares.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74C0434253F1CDC009762AB /* NCShares.swift */; }; F74C0437253F1CDC009762AB /* NCShares.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F74C0435253F1CDC009762AB /* NCShares.storyboard */; }; F74C86382AEFBE64009A1D4A /* NCImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B649B2ADFFAED00014640 /* NCImageCache.swift */; }; F74C863D2AEFBFD9009A1D4A /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = F74C863C2AEFBFD9009A1D4A /* LRUCache */; }; + F74D50352C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74D50342C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */; }; + F74D50362C9856D300BBBF4C /* NCCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */; }; F74DE14325135B6800917068 /* NCTransfers.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74DE14125135B6800917068 /* NCTransfers.swift */; }; F74DE14425135B6800917068 /* NCTransfers.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F74DE14225135B6800917068 /* NCTransfers.storyboard */; }; F7501C322212E57500FB1415 /* NCMedia.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7501C302212E57400FB1415 /* NCMedia.storyboard */; }; @@ -489,7 +512,7 @@ F75A9EE723796C6F0044CFCE /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F75B0ABD244C4DBB00E58DCA /* NCActionCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75B0ABC244C4DBB00E58DCA /* NCActionCenter.swift */; }; F75C0C4823D1FAE300163CC8 /* NCRichWorkspaceCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75C0C4723D1FAE300163CC8 /* NCRichWorkspaceCommon.swift */; }; - F75CA1472962F13700B01130 /* HUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75CA1462962F13700B01130 /* HUDView.swift */; }; + F75CA1472962F13700B01130 /* NCHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75CA1462962F13700B01130 /* NCHUDView.swift */; }; F75D19E325EFE09000D74598 /* NCTrash+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75D19E225EFE09000D74598 /* NCTrash+Menu.swift */; }; F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = F75DD769290ABB25002EB562 /* Intent.intentdefinition */; }; F75DD766290ABB25002EB562 /* Intent.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = F75DD769290ABB25002EB562 /* Intent.intentdefinition */; }; @@ -569,8 +592,6 @@ F76D3CF12428B40E005DFA87 /* NCViewerPDFSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76D3CF02428B40E005DFA87 /* NCViewerPDFSearch.swift */; }; F76D3CF32428B94E005DFA87 /* NCViewerPDFSearchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F76D3CF22428B94E005DFA87 /* NCViewerPDFSearchCell.xib */; }; F76D3CF52428D0C1005DFA87 /* NCViewerPDF.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F76D3CF42428D0C0005DFA87 /* NCViewerPDF.storyboard */; }; - F76DA95B277B75A90082465B /* TOPasscodeViewController.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86822642CF5500ED5349 /* TOPasscodeViewController.xcframework */; }; - F76DA95C277B75A90082465B /* TOPasscodeViewController.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F70B86822642CF5500ED5349 /* TOPasscodeViewController.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F76DA963277B760E0082465B /* Queuer in Frameworks */ = {isa = PBXBuildFile; productRef = F76DA962277B760E0082465B /* Queuer */; }; F76DA969277B77EA0082465B /* DropDown in Frameworks */ = {isa = PBXBuildFile; productRef = F76DA968277B77EA0082465B /* DropDown */; }; F76DEE9728F808AF0041B1C9 /* LockscreenData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76DEE9428F808AF0041B1C9 /* LockscreenData.swift */; }; @@ -593,8 +614,8 @@ F774264A22EB4D0000B23912 /* NCSearchUserDropDownCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F774264822EB4D0000B23912 /* NCSearchUserDropDownCell.xib */; }; F7743A122C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */; }; F7743A142C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */; }; - F77444F522281649000D5EB0 /* NCGridMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F322281649000D5EB0 /* NCGridMediaCell.swift */; }; - F77444F622281649000D5EB0 /* NCGridMediaCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F77444F422281649000D5EB0 /* NCGridMediaCell.xib */; }; + F77444F522281649000D5EB0 /* NCMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F322281649000D5EB0 /* NCMediaCell.swift */; }; + F77444F622281649000D5EB0 /* NCMediaCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F77444F422281649000D5EB0 /* NCMediaCell.xib */; }; F77444F8222816D5000D5EB0 /* NCPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */; }; F778231E2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */; }; F77A697D250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77A697C250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift */; }; @@ -608,6 +629,13 @@ F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C97382953131000FDDD09 /* NCCameraRoll.swift */; }; F77C973A2953143A00FDDD09 /* NCCameraRoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77C97382953131000FDDD09 /* NCCameraRoll.swift */; }; F77CB6A92AA08053000C3CA4 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F77CB6A82AA08053000C3CA4 /* OpenSSL */; }; + F77DD6A82C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F77DD6A92C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F77DD6AA2C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F77DD6AB2C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F77DD6AC2C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F77DD6AD2C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; + F77DD6AE2C5CC093009448FB /* NCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77DD6A72C5CC093009448FB /* NCSession.swift */; }; F77ED59128C9CE9D00E24ED0 /* ToolbarData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77ED59028C9CE9D00E24ED0 /* ToolbarData.swift */; }; F77ED59328C9CEA000E24ED0 /* ToolbarWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77ED59228C9CEA000E24ED0 /* ToolbarWidgetProvider.swift */; }; F77ED59528C9CEA400E24ED0 /* ToolbarWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77ED59428C9CEA300E24ED0 /* ToolbarWidgetView.swift */; }; @@ -631,7 +659,6 @@ F78302F928B4C3E600B84583 /* NCManageDatabase+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */; }; F78302FA28B4C3EA00B84583 /* NCManageDatabase+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4BF61827562A4B0081CEEF /* NCManageDatabase+Metadata.swift */; }; F78302FB28B4C3EE00B84583 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; }; - F78302FD28B4C42B00B84583 /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; F78302FE28B4C44700B84583 /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; F78302FF28B4C45000B84583 /* NCUtilityFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74AF3A3247FB6AE00AC767B /* NCUtilityFileSystem.swift */; }; F783030028B4C45800B84583 /* NCGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */; }; @@ -672,10 +699,8 @@ F78ACD4B21903F850088454D /* NCTrashListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD4921903F850088454D /* NCTrashListCell.xib */; }; F78ACD52219046DC0088454D /* NCSectionFirstHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD51219046DC0088454D /* NCSectionFirstHeader.swift */; }; F78ACD54219047D40088454D /* NCSectionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD53219047D40088454D /* NCSectionFooter.xib */; }; - F78AF1E72BE938C100F3F060 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7792DE429EEE02D005930CE /* MobileVLCKit.xcframework */; }; - F78AF1E82BE938C100F3F060 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F7792DE429EEE02D005930CE /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; F78B87E72B62527100C65ADC /* NCMediaDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */; }; - F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnaill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnaill.swift */; }; + F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnail.swift */; }; F78C6FDE296D677300C952C3 /* NCContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78C6FDD296D677300C952C3 /* NCContextMenu.swift */; }; F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; }; F78E2D6629AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; }; @@ -722,6 +747,10 @@ F7A48415297028FC00BD1B49 /* Nextcloud Hub.png in Resources */ = {isa = PBXBuildFile; fileRef = F7A48414297028FC00BD1B49 /* Nextcloud Hub.png */; }; F7A509252C26BD5D00326106 /* NCCreateDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A509242C26BD5D00326106 /* NCCreateDocument.swift */; }; F7A509262C26D95D00326106 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = F7160A812BE933390034DCB3 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + F7A51E722C721FC00037BCC0 /* NCTransfersProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A51E712C721FC00037BCC0 /* NCTransfersProgress.swift */; }; + F7A51E732C7230070037BCC0 /* NCTransfersProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A51E712C721FC00037BCC0 /* NCTransfersProgress.swift */; }; + F7A51E742C7230090037BCC0 /* NCTransfersProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A51E712C721FC00037BCC0 /* NCTransfersProgress.swift */; }; + F7A51E752C72300A0037BCC0 /* NCTransfersProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A51E712C721FC00037BCC0 /* NCTransfersProgress.swift */; }; F7A560422AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A560412AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift */; }; F7A560442AE15D2900BE8FD6 /* Queuer in Frameworks */ = {isa = PBXBuildFile; productRef = F7A560432AE15D2900BE8FD6 /* Queuer */; }; F7A560462AE15D3D00BE8FD6 /* Queuer in Frameworks */ = {isa = PBXBuildFile; productRef = F7A560452AE15D3D00BE8FD6 /* Queuer */; }; @@ -743,13 +772,13 @@ F7A8D73A28F17E28008BBE1C /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; }; F7A8D73C28F181BC008BBE1C /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; F7A8D73D28F181D3008BBE1C /* NCUtilityFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74AF3A3247FB6AE00AC767B /* NCUtilityFileSystem.swift */; }; - F7A8D73E28F181E2008BBE1C /* NCUserBaseUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */; }; F7A8D73F28F181EF008BBE1C /* NCGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */; }; F7A8D74028F18212008BBE1C /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B7504A2397D38E004E13EC /* UIImage+Extension.swift */; }; F7A8D74128F18254008BBE1C /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; F7A8D74228F18261008BBE1C /* NCUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70BFC7320E0FA7C00C67599 /* NCUtility.swift */; }; F7A8D74328F1826F008BBE1C /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; F7A8D74428F1827B008BBE1C /* ThreadSafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */; }; + F7A8FD522C5E2557006C9CF8 /* NCAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A8FD512C5E2557006C9CF8 /* NCAccount.swift */; }; F7AC1CB028AB94490032D99F /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AC1CAF28AB94490032D99F /* Array+Extension.swift */; }; F7AC934A296193050002BC0F /* Reasons to use Nextcloud.pdf in Resources */ = {isa = PBXBuildFile; fileRef = F7AC9349296193050002BC0F /* Reasons to use Nextcloud.pdf */; }; F7AE00F5230D5F9E007ACF8A /* NCLoginProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AE00F4230D5F9E007ACF8A /* NCLoginProvider.swift */; }; @@ -785,8 +814,12 @@ F7BF9D852934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; F7BF9D862934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; F7BF9D872934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; }; + F7BFFD282C8846020029A201 /* NCHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BFFD272C8846020029A201 /* NCHud.swift */; }; + F7BFFD2A2C8854200029A201 /* NCHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BFFD272C8846020029A201 /* NCHud.swift */; }; + F7BFFD2B2C8854430029A201 /* NCHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BFFD272C8846020029A201 /* NCHud.swift */; }; + F7BFFD2C2C8854690029A201 /* NCHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BFFD272C8846020029A201 /* NCHud.swift */; }; F7C1DAEF2C3D1DF4000BDC69 /* FileProviderDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76673EC22C901F5007ED366 /* FileProviderDomain.swift */; }; - F7C1EEA525053A9C00866ACC /* NCDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C1EEA425053A9C00866ACC /* NCDataSource.swift */; }; + F7C1EEA525053A9C00866ACC /* NCCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */; }; F7C30DF6291BC0CA0017149B /* NCNetworkingE2EEUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DF5291BC0CA0017149B /* NCNetworkingE2EEUpload.swift */; }; F7C30DF7291BC0D30017149B /* NCNetworkingE2EEUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DF5291BC0CA0017149B /* NCNetworkingE2EEUpload.swift */; }; F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DF9291BCF790017149B /* NCNetworkingE2EECreateFolder.swift */; }; @@ -820,9 +853,47 @@ F7CEE6002BA9A5C9003EFD89 /* NCTrashGridCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7CEE5FE2BA9A5C9003EFD89 /* NCTrashGridCell.xib */; }; F7CEE6012BA9A5C9003EFD89 /* NCTrashGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CEE5FF2BA9A5C9003EFD89 /* NCTrashGridCell.swift */; }; F7D1612023CF19E30039EBBF /* NCViewerRichWorkspace.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */; }; + F7D1C4AC2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */; }; + F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; }; + F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; + F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; + F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; + F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; + F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; + F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; + F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; + F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; + F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; + F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; + F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; + F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; + F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; + F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; + F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; + F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; + F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; + F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; + F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; + F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; + F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; + F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; + F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; + F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; + F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; + F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; + F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; + F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; + F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; + F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; + F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; + F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; + F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; + F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; + F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */ = {isa = PBXBuildFile; productRef = F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D57C8626317BDA00DE301D /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7CA212C25F1333200826ABB /* NCAccountRequest.storyboard */; }; F7D57C8B26317BDE00DE301D /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CA212B25F1333200826ABB /* NCAccountRequest.swift */; }; + F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D60CAE2C941ACB008FBFDD /* NCMediaPinchGesture.swift */; }; F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D68FCB28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift */; }; F7D68FCD28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D68FCB28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift */; }; F7D68FCE28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D68FCB28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift */; }; @@ -852,7 +923,6 @@ F7E98C1927E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E98C1527E0D0FC001F9F19 /* NCManageDatabase+Video.swift */; }; F7EB9B132BBC12F300EDF036 /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EB9B122BBC12F300EDF036 /* UIApplication+Extension.swift */; }; F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = F7ED547B25EEA65400956C55 /* QRCodeReader */; }; - F7EDE4D1262D7B8400414FE6 /* NCDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C1EEA425053A9C00866ACC /* NCDataSource.swift */; }; F7EDE4D6262D7B9600414FE6 /* NCListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD4121903CE00088454D /* NCListCell.swift */; }; F7EDE4DB262D7BA200414FE6 /* NCCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellProtocol.swift */; }; F7EDE4E5262D7BBE00414FE6 /* NCSectionFirstHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78ACD51219046DC0088454D /* NCSectionFirstHeader.swift */; }; @@ -1047,8 +1117,6 @@ dstSubfolderSpec = 10; files = ( F7A509262C26D95D00326106 /* RealmSwift in Embed Frameworks */, - F78AF1E82BE938C100F3F060 /* MobileVLCKit.xcframework in Embed Frameworks */, - F76DA95C277B75A90082465B /* TOPasscodeViewController.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1100,9 +1168,8 @@ AF56C1DB2784856200D8BAE2 /* NCActivityCommentView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCActivityCommentView.xib; sourceTree = ""; }; AF68326927BE65A90010BF0B /* NCMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMenuAction.swift; sourceTree = ""; }; AF730AF727834B1400B7520E /* NCShare+NCCellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+NCCellDelegate.swift"; sourceTree = ""; }; - AF730AF927843E4C00B7520E /* NCShareExtension+NCDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+NCDelegate.swift"; sourceTree = ""; }; + AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+NCAccountRequestDelegate.swift"; sourceTree = ""; }; AF7E504D27A2D8FF00B5E4AF /* UIBarButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButton+Extension.swift"; sourceTree = ""; }; - AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserBaseUrl.swift; sourceTree = ""; }; AF8ED1F92757821000B8DBC4 /* NextcloudUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AF8ED1FB2757821000B8DBC4 /* NextcloudUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudUnitTests.swift; sourceTree = ""; }; AF93471127E2341B002537EE /* NCShare+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+Menu.swift"; sourceTree = ""; }; @@ -1128,6 +1195,7 @@ F3131ED82B038DB20018DB28 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/System/iOSSupport/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; }; F3131EDA2B038DB90018DB28 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/System/iOSSupport/System/Library/Frameworks/WidgetKit.framework; sourceTree = DEVELOPER_DIR; }; F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashSelectTabBar.swift; sourceTree = ""; }; + F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileNameValidator+Extensions.swift"; sourceTree = ""; }; F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCS12.swift; sourceTree = ""; }; F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAsset+Extension.swift"; sourceTree = ""; }; F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; @@ -1150,6 +1218,8 @@ F3BB464C2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCMoreAppSuggestionsCell.xib; sourceTree = ""; }; F3BB46512A39EC4900461F6E /* NCMoreAppSuggestionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreAppSuggestionsCell.swift; sourceTree = ""; }; F3BB46532A3A1E9D00461F6E /* CCCellMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CCCellMore.swift; sourceTree = ""; }; + F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenAwakeManager.swift; sourceTree = ""; }; + F3E173BF2C9B1067006D177A /* AwakeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AwakeMode.swift; sourceTree = ""; }; F3EF2E0B2BFCF3810025EF46 /* NCLoginPoll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCLoginPoll.swift; sourceTree = ""; }; F700222B1EC479840080073F /* Custom.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Custom.xcassets; sourceTree = ""; }; F700510022DF63AC003A3356 /* NCShare.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCShare.storyboard; sourceTree = ""; }; @@ -1308,8 +1378,10 @@ F73EF7DE2B02266C0087E6E9 /* NCManageDatabase+Trash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Trash.swift"; sourceTree = ""; }; F73EF7E62B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+UserStatus.swift"; sourceTree = ""; }; F73F537E1E929C8500F8678D /* NCMore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMore.swift; sourceTree = ""; }; + F7401C142C75E6F300649E87 /* NCCapabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCapabilities.swift; sourceTree = ""; }; F741C2232B6B9FD600E849BB /* NCMediaSelectTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMediaSelectTabBar.swift; sourceTree = ""; }; F7421EAE2294044B00C4B7C1 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; + F74230F22C79B57200CA1ACA /* NCNetworking+Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCNetworking+Task.swift"; sourceTree = ""; }; F745B250222D871800346520 /* QRCodeReader.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QRCodeReader.framework; path = Carthage/Build/iOS/QRCodeReader.framework; sourceTree = ""; }; F745B252222D88AE00346520 /* NCLoginQRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCLoginQRCode.swift; sourceTree = ""; }; F747EB0C2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift"; sourceTree = ""; }; @@ -1321,6 +1393,7 @@ F74C0434253F1CDC009762AB /* NCShares.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShares.swift; sourceTree = ""; }; F74C0435253F1CDC009762AB /* NCShares.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCShares.storyboard; sourceTree = ""; }; F74C4FBA2328C3C100A23E25 /* OpenSSL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenSSL.framework; path = Carthage/Build/iOS/OpenSSL.framework; sourceTree = ""; }; + F74D50342C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift"; sourceTree = ""; }; F74DE14125135B6800917068 /* NCTransfers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCTransfers.swift; sourceTree = ""; }; F74DE14225135B6800917068 /* NCTransfers.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCTransfers.storyboard; sourceTree = ""; }; F7501C302212E57400FB1415 /* NCMedia.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCMedia.storyboard; sourceTree = ""; }; @@ -1345,7 +1418,7 @@ F75B91F71ECAE26300199C96 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; F75B923D1ECAE55E00199C96 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; F75C0C4723D1FAE300163CC8 /* NCRichWorkspaceCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCRichWorkspaceCommon.swift; sourceTree = ""; }; - F75CA1462962F13700B01130 /* HUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDView.swift; sourceTree = ""; }; + F75CA1462962F13700B01130 /* NCHUDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCHUDView.swift; sourceTree = ""; }; F75D19E225EFE09000D74598 /* NCTrash+Menu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCTrash+Menu.swift"; sourceTree = ""; }; F75DD768290ABB25002EB562 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/Intent.intentdefinition; sourceTree = ""; }; F75EDFBC1E8C112F00E6F369 /* libsqlite3.0.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.0.tbd; path = usr/lib/libsqlite3.0.tbd; sourceTree = SDKROOT; }; @@ -1438,8 +1511,8 @@ F77439621FCD6D9C00662C46 /* es-UY */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-UY"; path = "es-UY.lproj/Localizable.strings"; sourceTree = ""; }; F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDelegate.swift"; sourceTree = ""; }; F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CollectionViewDataSource.swift"; sourceTree = ""; }; - F77444F322281649000D5EB0 /* NCGridMediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCGridMediaCell.swift; sourceTree = ""; }; - F77444F422281649000D5EB0 /* NCGridMediaCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCGridMediaCell.xib; sourceTree = ""; }; + F77444F322281649000D5EB0 /* NCMediaCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMediaCell.swift; sourceTree = ""; }; + F77444F422281649000D5EB0 /* NCMediaCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCMediaCell.xib; sourceTree = ""; }; F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPickerViewController.swift; sourceTree = ""; }; F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+MediaLayout.swift"; sourceTree = ""; }; F7792DE429EEE02D005930CE /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; @@ -1449,6 +1522,7 @@ F77BB7492899857B0090FC19 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; F77BC3EC293E528A005F2B08 /* NCConfigServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCConfigServer.swift; sourceTree = ""; }; F77C97382953131000FDDD09 /* NCCameraRoll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCameraRoll.swift; sourceTree = ""; }; + F77DD6A72C5CC093009448FB /* NCSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCSession.swift; sourceTree = ""; }; F77ED59028C9CE9D00E24ED0 /* ToolbarData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarData.swift; sourceTree = ""; }; F77ED59228C9CEA000E24ED0 /* ToolbarWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarWidgetProvider.swift; sourceTree = ""; }; F77ED59428C9CEA300E24ED0 /* ToolbarWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarWidgetView.swift; sourceTree = ""; }; @@ -1474,7 +1548,7 @@ F78ACD51219046DC0088454D /* NCSectionFirstHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCSectionFirstHeader.swift; sourceTree = ""; }; F78ACD53219047D40088454D /* NCSectionFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCSectionFooter.xib; sourceTree = ""; }; F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaDataSource.swift; sourceTree = ""; }; - F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnaill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaDownloadThumbnaill.swift; sourceTree = ""; }; + F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaDownloadThumbnail.swift; sourceTree = ""; }; F78C6FDD296D677300C952C3 /* NCContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenu.swift; sourceTree = ""; }; F78D6F461F0B7CB9002F9619 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/Localizable.strings"; sourceTree = ""; }; F78D6F4D1F0B7CE4002F9619 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; @@ -1505,12 +1579,14 @@ F7A0D1342591FBC5008F8A13 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; F7A48414297028FC00BD1B49 /* Nextcloud Hub.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Nextcloud Hub.png"; sourceTree = SOURCE_ROOT; }; F7A509242C26BD5D00326106 /* NCCreateDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCreateDocument.swift; sourceTree = ""; }; + F7A51E712C721FC00037BCC0 /* NCTransfersProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTransfersProgress.swift; sourceTree = ""; }; F7A560412AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCOperationSaveLivePhoto.swift; sourceTree = ""; }; F7A5DF042C3FD11800753FC4 /* FileProviderExtension+NetworkingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+NetworkingDelegate.swift"; sourceTree = ""; }; F7A60F84292D215000FCE1F2 /* NCShareAccounts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareAccounts.swift; sourceTree = ""; }; F7A60F85292D215000FCE1F2 /* NCShareAccounts.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCShareAccounts.storyboard; sourceTree = ""; }; F7A7FDDB2C2DBD6200E9A93A /* NCDeepLinkHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCDeepLinkHandler.swift; sourceTree = ""; }; F7A846DD2BB01ACB0024816F /* NCTrashCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashCellProtocol.swift; sourceTree = ""; }; + F7A8FD512C5E2557006C9CF8 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7AA41B827C7CF4600494705 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = ""; }; F7AA41B927C7CF4B00494705 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; F7AA41BA27C7CF5000494705 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -1621,7 +1697,8 @@ F7BE7C79290ADF16002ABB61 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Intent.strings"; sourceTree = ""; }; F7BE7C7B290ADF16002ABB61 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Intent.strings"; sourceTree = ""; }; F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+LayoutForView.swift"; sourceTree = ""; }; - F7C1EEA425053A9C00866ACC /* NCDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDataSource.swift; sourceTree = ""; }; + F7BFFD272C8846020029A201 /* NCHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCHud.swift; sourceTree = ""; }; + F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewDataSource.swift; sourceTree = ""; }; F7C30DF5291BC0CA0017149B /* NCNetworkingE2EEUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEUpload.swift; sourceTree = ""; }; F7C30DF9291BCF790017149B /* NCNetworkingE2EECreateFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EECreateFolder.swift; sourceTree = ""; }; F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEDelete.swift; sourceTree = ""; }; @@ -1654,14 +1731,51 @@ F7CE8AFB1DC1F8D8009CAE48 /* Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Share.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F7CEE5FE2BA9A5C9003EFD89 /* NCTrashGridCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCTrashGridCell.xib; sourceTree = ""; }; F7CEE5FF2BA9A5C9003EFD89 /* NCTrashGridCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCTrashGridCell.swift; sourceTree = ""; }; - F7CF16A22A4D7C7A000FF107 /* NCMoreUserCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCMoreUserCell.xib; sourceTree = ""; }; F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerRichWorkspace.storyboard; sourceTree = ""; }; + F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+CollectionViewDataSourcePrefetching.swift"; sourceTree = ""; }; F7D2C772246470CA008513AE /* XLForm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XLForm.framework; path = Carthage/Build/iOS/XLForm.framework; sourceTree = ""; }; + F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonPinchGesture.swift; sourceTree = ""; }; + F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleImage.h; sourceTree = ""; }; + F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleImage.m; sourceTree = ""; }; + F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewContentLayout.h; sourceTree = ""; }; + F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewContentLayout.m; sourceTree = ""; }; + F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerAnimatedTransitioning.h; sourceTree = ""; }; + F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewControllerAnimatedTransitioning.m; sourceTree = ""; }; + F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOSettingsKeypadImage.h; sourceTree = ""; }; + F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOSettingsKeypadImage.m; sourceTree = ""; }; + F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerConstants.h; sourceTree = ""; }; + F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleButton.h; sourceTree = ""; }; + F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleButton.m; sourceTree = ""; }; + F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeKeypadView.h; sourceTree = ""; }; + F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeKeypadView.m; sourceTree = ""; }; + F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeView.h; sourceTree = ""; }; + F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeView.m; sourceTree = ""; }; + F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadButton.h; sourceTree = ""; }; + F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadButton.m; sourceTree = ""; }; + F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadView.h; sourceTree = ""; }; + F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadView.m; sourceTree = ""; }; + F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsWarningLabel.h; sourceTree = ""; }; + F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsWarningLabel.m; sourceTree = ""; }; + F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeButtonLabel.h; sourceTree = ""; }; + F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeButtonLabel.m; sourceTree = ""; }; + F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleView.h; sourceTree = ""; }; + F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleView.m; sourceTree = ""; }; + F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeFixedInputView.h; sourceTree = ""; }; + F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeFixedInputView.m; sourceTree = ""; }; + F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeInputField.h; sourceTree = ""; }; + F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeInputField.m; sourceTree = ""; }; + F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeVariableInputView.h; sourceTree = ""; }; + F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeVariableInputView.m; sourceTree = ""; }; + F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsViewController.h; sourceTree = ""; }; + F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsViewController.m; sourceTree = ""; }; + F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewController.h; sourceTree = ""; }; + F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewController.m; sourceTree = ""; }; F7D532461F5D4123006568B1 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; F7D5324D1F5D4137006568B1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F7D532541F5D4155006568B1 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; F7D5328F1F5D443B006568B1 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; F7D532A41F5D4461006568B1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + F7D60CAE2C941ACB008FBFDD /* NCMediaPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaPinchGesture.swift; sourceTree = ""; }; F7D68FCB28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+DashboardWidget.swift"; sourceTree = ""; }; F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+DragDrop.swift"; sourceTree = ""; }; F7D96FCB246ED7E100536D73 /* NCNetworkingCheckRemoteUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCNetworkingCheckRemoteUser.swift; sourceTree = ""; }; @@ -1790,7 +1904,6 @@ F74C863D2AEFBFD9009A1D4A /* LRUCache in Frameworks */, F72AD70F28C24BA1006CB92D /* NextcloudKit in Frameworks */, F33EE6E72BF4C02600CA1A51 /* NIOSSL in Frameworks */, - F737DA992B7B864E0063BAFC /* TOPasscodeViewController.xcframework in Frameworks */, F72CD01227A7E92400E59476 /* JGProgressHUD in Frameworks */, F77CB6A92AA08053000C3CA4 /* OpenSSL in Frameworks */, F760DE092AE66ED00027D78A /* KeychainAccess in Frameworks */, @@ -1837,6 +1950,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */, F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */, F7ED547C25EEA65400956C55 /* QRCodeReader in Frameworks */, F788ECC7263AAAFA00ADC67F /* MarkdownKit in Frameworks */, @@ -1852,7 +1966,6 @@ F758A01227A7F03E0069468B /* JGProgressHUD in Frameworks */, F77333882927A72100466E35 /* OpenSSL in Frameworks */, F753BA93281FD8020015BFB6 /* EasyTipView in Frameworks */, - F76DA95B277B75A90082465B /* TOPasscodeViewController.xcframework in Frameworks */, F7160A822BE933390034DCB3 /* RealmSwift in Frameworks */, F76DA963277B760E0082465B /* Queuer in Frameworks */, F72AD70D28C24B93006CB92D /* NextcloudKit in Frameworks */, @@ -1862,7 +1975,6 @@ F7F623B52A5EF4D30022D3D4 /* Gzip in Frameworks */, F75EAED826D2552E00F4320E /* MarqueeLabel in Frameworks */, F72DA9B425F53E4E00B87DB1 /* SwiftRichString in Frameworks */, - F78AF1E72BE938C100F3F060 /* MobileVLCKit.xcframework in Frameworks */, F73ADD1C265546890069EA0D /* SwiftEntryKit in Frameworks */, F76B649E2ADFFDEC00014640 /* LRUCache in Frameworks */, ); @@ -1960,14 +2072,6 @@ path = Tests; sourceTree = ""; }; - F33AAF982A601465006ECCBD /* Recovered References */ = { - isa = PBXGroup; - children = ( - F7CF16A22A4D7C7A000FF107 /* NCMoreUserCell.xib */, - ); - name = "Recovered References"; - sourceTree = ""; - }; F37208762BAB4B4B006B5430 /* Common */ = { isa = PBXGroup; children = ( @@ -2024,6 +2128,15 @@ path = Cells; sourceTree = ""; }; + F3E173BE2C9B1057006D177A /* ScreenAwakeManager */ = { + isa = PBXGroup; + children = ( + F3E173AF2C9AF637006D177A /* ScreenAwakeManager.swift */, + F3E173BF2C9B1067006D177A /* AwakeMode.swift */, + ); + path = ScreenAwakeManager; + sourceTree = ""; + }; F70211F31BAC56E9003FC03E /* Main */ = { isa = PBXGroup; children = ( @@ -2098,8 +2211,8 @@ F720B5B72507B9A5008C94E5 /* Cell */ = { isa = PBXGroup; children = ( - F77444F322281649000D5EB0 /* NCGridMediaCell.swift */, - F77444F422281649000D5EB0 /* NCGridMediaCell.xib */, + F77444F322281649000D5EB0 /* NCMediaCell.swift */, + F77444F422281649000D5EB0 /* NCMediaCell.xib */, ); path = Cell; sourceTree = ""; @@ -2219,13 +2332,15 @@ F75A9EE523796C6F0044CFCE /* NCNetworking.swift */, F713FBE42C31645200F10760 /* NCNetworking+AsyncAwait.swift */, F7327E1F2B73A42F00A462C7 /* NCNetworking+Download.swift */, - F7327E272B73A53400A462C7 /* NCNetworking+Upload.swift */, - F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */, F7327E342B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift */, F7327E3C2B73B92800A462C7 /* NCNetworking+Synchronization.swift */, + F74230F22C79B57200CA1ACA /* NCNetworking+Task.swift */, + F7327E272B73A53400A462C7 /* NCNetworking+Upload.swift */, + F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */, F7D96FCB246ED7E100536D73 /* NCNetworkingCheckRemoteUser.swift */, F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */, F755BD9A20594AC7008C5FBB /* NCService.swift */, + F7A51E712C721FC00037BCC0 /* NCTransfersProgress.swift */, ); path = Networking; sourceTree = ""; @@ -2265,10 +2380,10 @@ children = ( 370D26AE248A3D7A00121797 /* NCCellProtocol.swift */, F751247A2C42919C00E63DB8 /* NCPhotoCell.swift */, - F751247B2C42919C00E63DB8 /* NCPhotoCell.xib */, F78ACD3F21903CC20088454D /* NCGridCell.swift */, - F78ACD4521903D010088454D /* NCGridCell.xib */, F78ACD4121903CE00088454D /* NCListCell.swift */, + F751247B2C42919C00E63DB8 /* NCPhotoCell.xib */, + F78ACD4521903D010088454D /* NCGridCell.xib */, F78ACD4321903CF20088454D /* NCListCell.xib */, ); path = Cell; @@ -2280,15 +2395,18 @@ F75FE06B2BB01D0D00A0EFEF /* Cell */, F78ACD50219046AC0088454D /* Section Header Footer */, F70D7C3525FFBF81002B9E34 /* NCCollectionViewCommon.swift */, - F799DF8A2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift */, - F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */, - F747EB0C2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift */, F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */, + F74D50342C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */, F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */, - F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */, + F747EB0C2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift */, F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */, + F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */, + F799DF8A2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift */, + F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */, F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBar.swift */, + F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */, F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */, + F7C1EEA425053A9C00866ACC /* NCCollectionViewDataSource.swift */, F7E7AEA42BA32C6500512E52 /* NCCollectionViewDownloadThumbnail.swift */, F7E7AEA62BA32D0000512E52 /* NCCollectionViewUnifiedSearch.swift */, ); @@ -2422,7 +2540,8 @@ F769CA182966EA3C00039397 /* ComponentView.swift */, F768823D2C0DD304001CF441 /* LazyView.swift */, F768823F2C0DD30B001CF441 /* ViewOnAppear.swift */, - F75CA1462962F13700B01130 /* HUDView.swift */, + F7BFFD272C8846020029A201 /* NCHud.swift */, + F75CA1462962F13700B01130 /* NCHUDView.swift */, ); path = GUI; sourceTree = ""; @@ -2586,9 +2705,9 @@ children = ( F7AC1CAF28AB94490032D99F /* Array+Extension.swift */, F7817CF729801A3500FFBC65 /* Data+Extension.swift */, + AFCE353427E4ED5900FEA6C2 /* DateFormatter+Extension.swift */, F70460512499061800BB98A7 /* NotificationCenter+MainThread.swift */, F79B869A265E19D40085C0E0 /* NSMutableAttributedString+Extension.swift */, - AFCE353427E4ED5900FEA6C2 /* DateFormatter+Extension.swift */, F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */, F343A4B22A1E01FF00DDA874 /* PHAsset+Extension.swift */, F7A0D1342591FBC5008F8A13 /* String+Extension.swift */, @@ -2601,13 +2720,13 @@ F713FEFE2472764000214AF6 /* UIImage+animatedGIF.h */, F713FEFF2472764100214AF6 /* UIImage+animatedGIF.m */, F7B7504A2397D38E004E13EC /* UIImage+Extension.swift */, + F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */, F77BB7492899857B0090FC19 /* UINavigationController+Extension.swift */, F77BB747289985270090FC19 /* UITabBarController+Extension.swift */, AFCE353227E4ED1900FEA6C2 /* UIToolbar+Extension.swift */, F7E41315294A19B300839300 /* UIView+Extension.swift */, F77BB745289984CA0090FC19 /* UIViewController+Extension.swift */, F7E8A390295DC5E0006CB2D0 /* View+Extension.swift */, - F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -2662,7 +2781,6 @@ F7BAAD951ED5A63D00B7EAD4 /* Data */ = { isa = PBXGroup; children = ( - F7C1EEA425053A9C00866ACC /* NCDataSource.swift */, F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */, AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */, AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */, @@ -2711,7 +2829,9 @@ F7BFFA991A24D7BB0044ED85 /* Utility */ = { isa = PBXGroup; children = ( + F3E173BE2C9B1057006D177A /* ScreenAwakeManager */, F702F2FC25EE5D2C008F8E80 /* NYMnemonic */, + F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */, F76D364528A4F8BF00214537 /* NCActivityIndicator.swift */, F733598025C1C188002ABA72 /* NCAskAuthorization.swift */, F77C97382953131000FDDD09 /* NCCameraRoll.swift */, @@ -2719,7 +2839,6 @@ F70968A324212C4E00ED60E5 /* NCLivePhoto.swift */, F702F30725EE5D47008F8E80 /* NCPopupViewController.swift */, F707C26421A2DC5200F6181E /* NCStoreReview.swift */, - AF817EF0274BC781009ED85B /* NCUserBaseUrl.swift */, F70BFC7320E0FA7C00C67599 /* NCUtility.swift */, F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */, F359D8662A7D03420023F405 /* NCUtility+Exif.swift */, @@ -2729,6 +2848,7 @@ F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */, F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */, F33EE6F12BF4C9B200CA1A51 /* PKCS12.swift */, + F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */, ); path = Utility; sourceTree = ""; @@ -2738,7 +2858,7 @@ children = ( F714803A262EBE3900693E51 /* MainInterface.storyboard */, F7148040262EBE4000693E51 /* NCShareExtension.swift */, - AF730AF927843E4C00B7520E /* NCShareExtension+NCDelegate.swift */, + AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */, AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */, AF22B216277D196700DAB0CC /* NCShareExtension+Files.swift */, AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */, @@ -2826,6 +2946,96 @@ path = More; sourceTree = ""; }; + F7D4BF0A2CA2E8D800A5E746 /* Models */ = { + isa = PBXGroup; + children = ( + F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */, + F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */, + F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */, + F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */, + F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */, + F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */, + F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */, + F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */, + ); + path = Models; + sourceTree = ""; + }; + F7D4BF0C2CA2E8D800A5E746 /* Supporting */ = { + isa = PBXGroup; + children = ( + F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */, + ); + path = Supporting; + sourceTree = ""; + }; + F7D4BF132CA2E8D800A5E746 /* Main */ = { + isa = PBXGroup; + children = ( + F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */, + F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */, + F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */, + F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */, + F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */, + F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */, + ); + path = Main; + sourceTree = ""; + }; + F7D4BF1A2CA2E8D800A5E746 /* Settings */ = { + isa = PBXGroup; + children = ( + F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */, + F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */, + F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */, + F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */, + F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */, + F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */, + ); + path = Settings; + sourceTree = ""; + }; + F7D4BF252CA2E8D800A5E746 /* Shared */ = { + isa = PBXGroup; + children = ( + F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */, + F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */, + F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */, + F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */, + F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */, + F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */, + F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */, + F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */, + F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */, + F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */, + ); + path = Shared; + sourceTree = ""; + }; + F7D4BF262CA2E8D800A5E746 /* Views */ = { + isa = PBXGroup; + children = ( + F7D4BF132CA2E8D800A5E746 /* Main */, + F7D4BF1A2CA2E8D800A5E746 /* Settings */, + F7D4BF252CA2E8D800A5E746 /* Shared */, + ); + path = Views; + sourceTree = ""; + }; + F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */ = { + isa = PBXGroup; + children = ( + F7D4BF0A2CA2E8D800A5E746 /* Models */, + F7D4BF0C2CA2E8D800A5E746 /* Supporting */, + F7D4BF262CA2E8D800A5E746 /* Views */, + F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */, + F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */, + F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */, + F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */, + ); + path = TOPasscodeViewController; + sourceTree = ""; + }; F7DFB7E9219C5A0500680748 /* Create cloud */ = { isa = PBXGroup; children = ( @@ -2857,12 +3067,14 @@ F7501C302212E57400FB1415 /* NCMedia.storyboard */, F7501C312212E57400FB1415 /* NCMedia.swift */, F7BD09FF2C468925003A4A6D /* NCMedia+CollectionViewDataSource.swift */, + F7D1C4AB2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift */, F7BD0A012C4689A4003A4A6D /* NCMedia+CollectionViewDelegate.swift */, F72408322B8A27C900F128E2 /* NCMedia+Command.swift */, F7802B312BD5584F00D74270 /* NCMedia+DragDrop.swift */, F7BD0A032C4689E9003A4A6D /* NCMedia+MediaLayout.swift */, F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */, - F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnaill.swift */, + F7D60CAE2C941ACB008FBFDD /* NCMediaPinchGesture.swift */, + F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnail.swift */, F755CB3F2B8CB13C00CE27E9 /* NCMediaLayout.swift */, F741C2232B6B9FD600E849BB /* NCMediaSelectTabBar.swift */, ); @@ -2926,7 +3138,6 @@ F70716E32987F81500E72C1D /* File Provider Extension UI.appex */, C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */, C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */, - F33AAF982A601465006ECCBD /* Recovered References */, ); sourceTree = ""; }; @@ -2935,6 +3146,9 @@ children = ( F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, + F77DD6A72C5CC093009448FB /* NCSession.swift */, + F7A8FD512C5E2557006C9CF8 /* NCAccount.swift */, + F7401C142C75E6F300649E87 /* NCCapabilities.swift */, F76B649B2ADFFAED00014640 /* NCImageCache.swift */, F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */, F7E402282BA85D1D007E5609 /* PrivacyInfo.xcprivacy */, @@ -3402,6 +3616,7 @@ F7160A812BE933390034DCB3 /* RealmSwift */, F33EE6E02BF4BDA500CA1A51 /* NIOSSL */, F33EE6EF2BF4C0FF00CA1A51 /* NIO */, + F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */, ); productName = "Crypto Cloud"; productReference = F7CE8AFA1DC1F8D8009CAE48 /* Nextcloud.app */; @@ -3576,6 +3791,7 @@ F3A0479C2BD268B500658E7B /* XCRemoteSwiftPackageReference "PopupView" */, F33EE6DF2BF4BDA500CA1A51 /* XCRemoteSwiftPackageReference "swift-nio-ssl" */, F33EE6EE2BF4C0FF00CA1A51 /* XCRemoteSwiftPackageReference "swift-nio" */, + F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */, ); productRefGroup = F7F67B9F1A24D27800EE80DA; projectDirPath = ""; @@ -3682,7 +3898,7 @@ buildActionMask = 2147483647; files = ( F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */, - F77444F622281649000D5EB0 /* NCGridMediaCell.xib in Resources */, + F77444F622281649000D5EB0 /* NCMediaCell.xib in Resources */, F78ACD4421903CF20088454D /* NCListCell.xib in Resources */, F3BB464D2A39ADCC00461F6E /* NCMoreAppSuggestionsCell.xib in Resources */, F7F4F10727ECDBDB008676F9 /* Inconsolata-Black.ttf in Resources */, @@ -3800,6 +4016,7 @@ buildActionMask = 2147483647; files = ( F73EF7BD2B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */, + F71433E72C778FFB00E20B5A /* NotificationCenter+MainThread.swift in Sources */, F73EF7C52B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */, 2C1D5D7923E2DE9100334ABB /* NCBrand.swift in Sources */, F770768A263A8A2500A1BA94 /* NCUtilityFileSystem.swift in Sources */, @@ -3809,6 +4026,7 @@ F702F2D225EE5B5C008F8E80 /* NCGlobal.swift in Sources */, F7707689263A896A00A1BA94 /* UIImage+Extension.swift in Sources */, F72FD3BA297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, + F3E173C62C9B1067006D177A /* AwakeMode.swift in Sources */, F73EF7DD2B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F73EF7ED2B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift in Sources */, F7E98C1927E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */, @@ -3829,15 +4047,17 @@ F73EF7AD2B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, F7C9B9232B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, F73EF7CD2B0225610087E6E9 /* NCManageDatabase+PhotoLibrary.swift in Sources */, + F33918CA2C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, D575039F27146F93008DC9DC /* String+Extension.swift in Sources */, F757CC8829E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */, F79B646326CA661600838ACA /* UIControl+Extension.swift in Sources */, F73EF7B52B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, + F77DD6AE2C5CC093009448FB /* NCSession.swift in Sources */, F78A10C429322E8A008499B8 /* NCManageDatabase+Directory.swift in Sources */, F7B769AE2B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift in Sources */, F711A4E22AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, + F7401C1B2C75E6F300649E87 /* NCCapabilities.swift in Sources */, AF4BF61C27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */, - AF817EF4274BC781009ED85B /* NCUserBaseUrl.swift in Sources */, F78E2D6B29AF02DB0024D4F3 /* Database.swift in Sources */, F7817CFF29802D1A00FFBC65 /* NCPushNotificationEncryption.m in Sources */, F798F0EC2588060A000DAFFD /* UIColor+Extension.swift in Sources */, @@ -3892,7 +4112,6 @@ files = ( F74B6D9A2A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, F7490E8329882C84009DCE94 /* NCManageDatabase+LayoutForView.swift in Sources */, - F7490E7629882BF3009DCE94 /* NCUserBaseUrl.swift in Sources */, F73EF7DC2B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F7490E8029882C76009DCE94 /* NCManageDatabase+Avatar.swift in Sources */, F343A4B82A1E084300DDA874 /* PHAsset+Extension.swift in Sources */, @@ -3905,6 +4124,7 @@ F71F6D0C2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F7490E8629882C99009DCE94 /* NCUtilityFileSystem.swift in Sources */, F763D2A22A249C4500A3C901 /* NCManageDatabase+Capabilities.swift in Sources */, + F7401C1A2C75E6F300649E87 /* NCCapabilities.swift in Sources */, F73EF7E42B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, F343A4C02A1E734600DDA874 /* Optional+Extension.swift in Sources */, F711A4E12AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, @@ -3920,6 +4140,7 @@ F7490E7029882B9B009DCE94 /* NCManageDatabase+Metadata.swift in Sources */, F73EF7AC2B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, F73EF7C42B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */, + F77DD6AD2C5CC093009448FB /* NCSession.swift in Sources */, F7490E6C29882AEA009DCE94 /* String+Extension.swift in Sources */, F7490E6B29882A92009DCE94 /* NCGlobal.swift in Sources */, F7490E7529882BE2009DCE94 /* NCManageDatabase+Directory.swift in Sources */, @@ -3935,6 +4156,9 @@ F73EF7B42B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, F7490E8229882C80009DCE94 /* NCManageDatabase+E2EE.swift in Sources */, F7490E7829882C28009DCE94 /* NCUtility.swift in Sources */, + F3E173C52C9B1067006D177A /* AwakeMode.swift in Sources */, + F71433E62C778FFB00E20B5A /* NotificationCenter+MainThread.swift in Sources */, + F33918C92C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, F7490E7F29882C73009DCE94 /* NCManageDatabase+Activity.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3979,14 +4203,32 @@ F798F0E225880608000DAFFD /* UIColor+Extension.swift in Sources */, F7327E3F2B73B92800A462C7 /* NCNetworking+Synchronization.swift in Sources */, F7C9B9202B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, - AF817EF2274BC781009ED85B /* NCUserBaseUrl.swift in Sources */, F78E2D6829AF02DB0024D4F3 /* Database.swift in Sources */, + F7401C182C75E6F300649E87 /* NCCapabilities.swift in Sources */, F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */, F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */, + F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, + F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, + F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, + F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, + F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, + F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, + F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, + F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, + F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, + F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, + F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, + F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, + F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, + F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, + F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, + F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, + F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, F74AF3A5247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */, AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */, + F74D50362C9856D300BBBF4C /* NCCollectionViewDataSource.swift in Sources */, F7BD71E62636EAFC00643C34 /* NCNetworkingE2EE.swift in Sources */, F7F878AF1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, F7327E3B2B73B8D600A462C7 /* Array+Extension.swift in Sources */, @@ -3994,20 +4236,21 @@ AF22B218277D196700DAB0CC /* NCShareExtension+Files.swift in Sources */, F799DF862C4B7E56003410B5 /* NCSectionHeader.swift in Sources */, F702F2D025EE5B5C008F8E80 /* NCGlobal.swift in Sources */, + F7A51E742C7230090037BCC0 /* NCTransfersProgress.swift in Sources */, F343A4BE2A1E734600DDA874 /* Optional+Extension.swift in Sources */, F72437802C10B92400C7C68D /* NCPermissions.swift in Sources */, F7EDE4DB262D7BA200414FE6 /* NCCellProtocol.swift in Sources */, F72944F62A8424F800246839 /* NCEndToEndMetadataV1.swift in Sources */, - F7EDE4D1262D7B8400414FE6 /* NCDataSource.swift in Sources */, F73EF7BA2B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */, F711A4EB2AF9327D00095DD8 /* UIImage+animatedGIF.m in Sources */, F36E64FA2B96236C0085ABB5 /* UIView+Extension.swift in Sources */, F75A9EE723796C6F0044CFCE /* NCNetworking.swift in Sources */, - AF730AFA27843E4C00B7520E /* NCShareExtension+NCDelegate.swift in Sources */, + AF730AFA27843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift in Sources */, F7327E232B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */, F749B64D297B0CBB00087535 /* NCManageDatabase+Share.swift in Sources */, F72FD3B8297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, F7A76DC8256A71CD00119AB3 /* UIImage+Extension.swift in Sources */, + F3E173C32C9B1067006D177A /* AwakeMode.swift in Sources */, F711A4E52AF9310500095DD8 /* NCUtility+Image.swift in Sources */, F73EF7AA2B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, F763D2A02A249C4500A3C901 /* NCManageDatabase+Capabilities.swift in Sources */, @@ -4017,6 +4260,7 @@ F7817D0129802D5F00FFBC65 /* NCViewCertificateDetails.swift in Sources */, F7D57C8B26317BDE00DE301D /* NCAccountRequest.swift in Sources */, F7C30DF7291BC0D30017149B /* NCNetworkingE2EEUpload.swift in Sources */, + F33918C72C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, F78A10C229322E8A008499B8 /* NCManageDatabase+Directory.swift in Sources */, F713FBE72C31646500F10760 /* NCNetworking+AsyncAwait.swift in Sources */, F79FFB272A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */, @@ -4025,8 +4269,10 @@ AF22B217277D196700DAB0CC /* NCShareExtension+DataSource.swift in Sources */, F73EF7E22B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, F33EE6F52BF4C9B200CA1A51 /* PKCS12.swift in Sources */, + F77DD6AB2C5CC093009448FB /* NCSession.swift in Sources */, F76D364728A4F8BF00214537 /* NCActivityIndicator.swift in Sources */, F73EF7CA2B0225610087E6E9 /* NCManageDatabase+PhotoLibrary.swift in Sources */, + F7BFFD2A2C8854200029A201 /* NCHud.swift in Sources */, F749B654297B0F2400087535 /* NCManageDatabase+Avatar.swift in Sources */, AF22B208277B4E4C00DAB0CC /* NCCreateFormUploadConflictCell.swift in Sources */, F74C86382AEFBE64009A1D4A /* NCImageCache.swift in Sources */, @@ -4044,7 +4290,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F78302FD28B4C42B00B84583 /* NCUserBaseUrl.swift in Sources */, F793E5A128B76541005E4B02 /* NotificationCenter+MainThread.swift in Sources */, F76DEE9928F808AF0041B1C9 /* LockscreenWidgetView.swift in Sources */, F7817D0029802D3D00FFBC65 /* NCViewCertificateDetails.swift in Sources */, @@ -4078,16 +4323,20 @@ F7B769A92B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift in Sources */, F78E2D6629AF02DB0024D4F3 /* Database.swift in Sources */, F7327E3E2B73B92800A462C7 /* NCNetworking+Synchronization.swift in Sources */, + F77DD6A92C5CC093009448FB /* NCSession.swift in Sources */, F73EF7C82B0225610087E6E9 /* NCManageDatabase+PhotoLibrary.swift in Sources */, F783030728B4C52800B84583 /* UIColor+Extension.swift in Sources */, F783030028B4C45800B84583 /* NCGlobal.swift in Sources */, F793E59D28B761E7005E4B02 /* NCNetworking.swift in Sources */, F7BF9D832934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */, + F7401C162C75E6F300649E87 /* NCCapabilities.swift in Sources */, F757CC8329E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */, F73EF7A82B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, F7327E212B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */, + F7BFFD2B2C8854430029A201 /* NCHud.swift in Sources */, F7327E3A2B73B8D500A462C7 /* Array+Extension.swift in Sources */, F74B6D962A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, + F7A51E732C7230070037BCC0 /* NCTransfersProgress.swift in Sources */, F711A4E32AF9310400095DD8 /* NCUtility+Image.swift in Sources */, F749B652297B0F2400087535 /* NCManageDatabase+Avatar.swift in Sources */, F783030628B4C51E00B84583 /* String+Extension.swift in Sources */, @@ -4101,6 +4350,7 @@ F77ED59528C9CEA400E24ED0 /* ToolbarWidgetView.swift in Sources */, F78302FB28B4C3EE00B84583 /* NCManageDatabase+Video.swift in Sources */, F72EA95228B7BA2A00C88F0C /* DashboardWidgetProvider.swift in Sources */, + F33918C52C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, F7327E292B73A53400A462C7 /* NCNetworking+Upload.swift in Sources */, F343A4BC2A1E734600DDA874 /* Optional+Extension.swift in Sources */, F7327E362B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift in Sources */, @@ -4110,6 +4360,7 @@ F78302FF28B4C45000B84583 /* NCUtilityFileSystem.swift in Sources */, F73EF7B82B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */, F73EF7C02B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */, + F3E173C12C9B1067006D177A /* AwakeMode.swift in Sources */, F73EF7B02B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, F75DD766290ABB25002EB562 /* Intent.intentdefinition in Sources */, F7D68FCD28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, @@ -4149,6 +4400,7 @@ F7327E382B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift in Sources */, F771E3D320E2392D00AFB62D /* FileProviderExtension.swift in Sources */, F771E3D520E2392D00AFB62D /* FileProviderItem.swift in Sources */, + F3E173C42C9B1067006D177A /* AwakeMode.swift in Sources */, AF4BF616275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */, F343A4B72A1E084300DDA874 /* PHAsset+Extension.swift in Sources */, F7434B3620E23FE000417916 /* NCManageDatabase.swift in Sources */, @@ -4178,8 +4430,10 @@ F78E2D6929AF02DB0024D4F3 /* Database.swift in Sources */, F749B64E297B0CBB00087535 /* NCManageDatabase+Share.swift in Sources */, F73EF7B32B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, - AF817EF3274BC781009ED85B /* NCUserBaseUrl.swift in Sources */, + F7401C192C75E6F300649E87 /* NCCapabilities.swift in Sources */, F771E3F320E239A600AFB62D /* FileProviderData.swift in Sources */, + F7A51E752C72300A0037BCC0 /* NCTransfersProgress.swift in Sources */, + F33918C82C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, F7A0D1372591FBC5008F8A13 /* String+Extension.swift in Sources */, F771E3D720E2392D00AFB62D /* FileProviderEnumerator.swift in Sources */, F74AF3A6247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, @@ -4192,11 +4446,13 @@ F73EF7DB2B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F71F6D0B2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F73EF7EB2B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift in Sources */, + F7BFFD2C2C8854690029A201 /* NCHud.swift in Sources */, F771E3F820E239B500AFB62D /* FileProviderExtension+Thumbnail.swift in Sources */, F76882392C0DD230001CF441 /* NCKeychain.swift in Sources */, F343A4BF2A1E734600DDA874 /* Optional+Extension.swift in Sources */, AF4BF62027562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, F73ADD2226554FD10069EA0D /* NCContentPresenter.swift in Sources */, + F77DD6AC2C5CC093009448FB /* NCSession.swift in Sources */, F72FD3B9297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4205,19 +4461,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F77444F522281649000D5EB0 /* NCGridMediaCell.swift in Sources */, + F77444F522281649000D5EB0 /* NCMediaCell.swift in Sources */, F78C6FDE296D677300C952C3 /* NCContextMenu.swift in Sources */, F7E402332BA89551007E5609 /* NCTrash+Networking.swift in Sources */, F7E7AEA72BA32D0000512E52 /* NCCollectionViewUnifiedSearch.swift in Sources */, F73EF7A72B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, + F33918C42C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, F799DF882C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift in Sources */, F7AE00F8230E81CB007ACF8A /* NCBrowserWeb.swift in Sources */, + F77DD6A82C5CC093009448FB /* NCSession.swift in Sources */, F702F30825EE5D47008F8E80 /* NCPopupViewController.swift in Sources */, F733598125C1C188002ABA72 /* NCAskAuthorization.swift in Sources */, 370D26AF248A3D7A00121797 /* NCCellProtocol.swift in Sources */, F768822C2C0DD1E7001CF441 /* NCKeychain.swift in Sources */, + F7BFFD282C8846020029A201 /* NCHud.swift in Sources */, F71CD6CA2930D7B1006C95C1 /* NCApplicationHandle.swift in Sources */, F73EF7D72B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, + F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */, F76882402C0DD30B001CF441 /* ViewOnAppear.swift in Sources */, F790110E21415BF600D7B136 /* NCViewerRichDocument.swift in Sources */, F78ACD4021903CC20088454D /* NCGridCell.swift in Sources */, @@ -4231,6 +4491,7 @@ F76882282C0DD1E7001CF441 /* NCEndToEndInitialize.swift in Sources */, F702F2CD25EE5B4F008F8E80 /* AppDelegate.swift in Sources */, F769454022E9F077000A798A /* NCSharePaging.swift in Sources */, + F74D50352C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */, F7802B322BD5584F00D74270 /* NCMedia+DragDrop.swift in Sources */, F7EE66AD2A20B226009AE765 /* UILabel+Extension.swift in Sources */, F78ACD4221903CE00088454D /* NCListCell.swift in Sources */, @@ -4248,6 +4509,7 @@ F7FA80012C0F4F3B0072FC60 /* NCUploadAssetsView.swift in Sources */, 371B5A2E23D0B04500FAFAE9 /* NCMenu.swift in Sources */, F3EF2E0C2BFCF3810025EF46 /* NCLoginPoll.swift in Sources */, + F74230F32C79B57200CA1ACA /* NCNetworking+Task.swift in Sources */, F757CC8229E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */, F7B769A82B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift in Sources */, F79EDAA326B004980007D134 /* NCPlayerToolBar.swift in Sources */, @@ -4287,6 +4549,7 @@ F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */, F78A10BF29322E8A008499B8 /* NCManageDatabase+Directory.swift in Sources */, F7743A122C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift in Sources */, + F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */, F704B5E92430C0B800632F5F /* NCCreateFormUploadConflictCell.swift in Sources */, F7327E3D2B73B92800A462C7 /* NCNetworking+Synchronization.swift in Sources */, F72D404923D2082500A97FD0 /* NCViewerNextcloudText.swift in Sources */, @@ -4299,8 +4562,26 @@ F724377B2C10B83E00C7C68D /* NCPermissions.swift in Sources */, F794E13D2BBBFF2E003693D7 /* NCMainTabBarController.swift in Sources */, F7CBC1252BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, + F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, + F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, + F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, + F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, + F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, + F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, + F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, + F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, + F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, + F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, + F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, + F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, + F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, + F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, + F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, + F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, + F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, F75C0C4823D1FAE300163CC8 /* NCRichWorkspaceCommon.swift in Sources */, F78ACD4A21903F850088454D /* NCTrashListCell.swift in Sources */, + F7A51E722C721FC00037BCC0 /* NCTransfersProgress.swift in Sources */, F76882352C0DD1E7001CF441 /* NCWebBrowserView.swift in Sources */, F3A047972BD2668800658E7B /* NCAssistantEmptyView.swift in Sources */, F757CC8D29E82D0500F31428 /* NCGroupfolders.swift in Sources */, @@ -4315,7 +4596,6 @@ AF730AF827834B1400B7520E /* NCShare+NCCellDelegate.swift in Sources */, F70460522499061800BB98A7 /* NotificationCenter+MainThread.swift in Sources */, F78F74362163781100C2ADAD /* NCTrash.swift in Sources */, - AF817EF1274BC781009ED85B /* NCUserBaseUrl.swift in Sources */, F39298972A3B12CB00509762 /* BaseNCMoreCell.swift in Sources */, AF2D7C7C2742556F00ADF566 /* NCShareLinkCell.swift in Sources */, F73EF7C72B0225610087E6E9 /* NCManageDatabase+PhotoLibrary.swift in Sources */, @@ -4328,7 +4608,9 @@ F71F6D072B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F761856C29E98543006EB3B0 /* NCIntroCollectionViewCell.swift in Sources */, F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */, + F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */, F74B6D952A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, + F7A8FD522C5E2557006C9CF8 /* NCAccount.swift in Sources */, F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */, F702F2F725EE5CED008F8E80 /* NCLogin.swift in Sources */, F7EB9B132BBC12F300EDF036 /* UIApplication+Extension.swift in Sources */, @@ -4349,7 +4631,7 @@ F72944F22A84246400246839 /* NCEndToEndMetadataV20.swift in Sources */, F70BFC7420E0FA7D00C67599 /* NCUtility.swift in Sources */, F79EDAA526B004980007D134 /* NCPlayer.swift in Sources */, - F7C1EEA525053A9C00866ACC /* NCDataSource.swift in Sources */, + F7C1EEA525053A9C00866ACC /* NCCollectionViewDataSource.swift in Sources */, F713FF002472764100214AF6 /* UIImage+animatedGIF.m in Sources */, AFCE353527E4ED5900FEA6C2 /* DateFormatter+Extension.swift in Sources */, F718C24E254D507B00C5C256 /* NCViewerMediaDetailView.swift in Sources */, @@ -4364,6 +4646,7 @@ F73EF7DF2B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, F79B646026CA661600838ACA /* UIControl+Extension.swift in Sources */, F768823E2C0DD305001CF441 /* LazyView.swift in Sources */, + F3E173B02C9AF637006D177A /* ScreenAwakeManager.swift in Sources */, F7CA212D25F1333300826ABB /* NCAccountRequest.swift in Sources */, F747EB0D2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift in Sources */, F765F73125237E3F00391DBE /* NCRecent.swift in Sources */, @@ -4403,6 +4686,8 @@ AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */, F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */, F7A560422AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift in Sources */, + F7D1C4AC2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift in Sources */, + F7401C152C75E6F300649E87 /* NCCapabilities.swift in Sources */, F7E402312BA891EB007E5609 /* NCTrash+SelectTabBarDelegate.swift in Sources */, F70753EB2542A99800972D44 /* NCViewerMediaPage.swift in Sources */, F7817CF829801A3500FFBC65 /* Data+Extension.swift in Sources */, @@ -4420,7 +4705,7 @@ F70968A424212C4E00ED60E5 /* NCLivePhoto.swift in Sources */, F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */, F7BC288026663F85004D46C5 /* NCViewCertificateDetails.swift in Sources */, - F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnaill.swift in Sources */, + F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnail.swift in Sources */, F73EF7AF2B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */, F745B253222D88AE00346520 /* NCLoginQRCode.swift in Sources */, @@ -4442,7 +4727,7 @@ F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */, F70CEF5623E9C7E50007035B /* UIColor+Extension.swift in Sources */, F76882242C0DD1E7001CF441 /* NCSettingsAdvancedView.swift in Sources */, - F75CA1472962F13700B01130 /* HUDView.swift in Sources */, + F75CA1472962F13700B01130 /* NCHUDView.swift in Sources */, F77BB748289985270090FC19 /* UITabBarController+Extension.swift in Sources */, F763D29D2A249C4500A3C901 /* NCManageDatabase+Capabilities.swift in Sources */, F76882252C0DD1E7001CF441 /* NCSettingsAdvancedModel.swift in Sources */, @@ -4464,6 +4749,7 @@ AF2D7C7E2742559100ADF566 /* NCShareUserCell.swift in Sources */, F74DE14325135B6800917068 /* NCTransfers.swift in Sources */, AF4BF614275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */, + F3E173C02C9B1067006D177A /* AwakeMode.swift in Sources */, F711A4DC2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, AF4BF61E27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, ); @@ -4509,18 +4795,22 @@ F72FD3B7297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, F7A8D74128F18254008BBE1C /* UIColor+Extension.swift in Sources */, F73EF7D92B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, + F3E173C22C9B1067006D177A /* AwakeMode.swift in Sources */, F7864ACE2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */, F7B769AA2B7A0B2000C1AAEB /* NCManageDatabase+Metadata+Session.swift in Sources */, F7A8D74028F18212008BBE1C /* UIImage+Extension.swift in Sources */, F73EF7D12B0225BA0087E6E9 /* NCManageDatabase+Tag.swift in Sources */, + F71433E52C778FFA00E20B5A /* NotificationCenter+MainThread.swift in Sources */, F73EF7C92B0225610087E6E9 /* NCManageDatabase+PhotoLibrary.swift in Sources */, F73EF7B92B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */, F71F6D092B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, + F7401C172C75E6F300649E87 /* NCCapabilities.swift in Sources */, F33EE6F42BF4C9B200CA1A51 /* PKCS12.swift in Sources */, + F33918C62C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, F757CC8429E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */, F78E2D6729AF02DB0024D4F3 /* Database.swift in Sources */, + F77DD6AA2C5CC093009448FB /* NCSession.swift in Sources */, F7A8D73628F17E1A008BBE1C /* NCManageDatabase+Activity.swift in Sources */, - F7A8D73E28F181E2008BBE1C /* NCUserBaseUrl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4881,7 +5171,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(TEST_HOST)"; TEST_TARGET_NAME = Nextcloud; @@ -4926,7 +5215,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(TEST_HOST)"; TEST_TARGET_NAME = Nextcloud; @@ -4977,7 +5265,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nextcloud.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Nextcloud"; }; @@ -5021,7 +5308,6 @@ SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; - SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nextcloud.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Nextcloud"; VALIDATE_PRODUCT = YES; @@ -5262,6 +5548,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/iOSClient/Nextcloud-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; }; name = Debug; }; @@ -5284,6 +5571,7 @@ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/iOSClient/Nextcloud-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; + _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES; }; name = Release; }; @@ -5394,7 +5682,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 36; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5421,7 +5709,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.5; + MARKETING_VERSION = 6.0.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; @@ -5460,7 +5748,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -5484,7 +5772,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.5; + MARKETING_VERSION = 6.0.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; @@ -5639,7 +5927,7 @@ repositoryURL = "https://github.com/realm/realm-swift"; requirement = { kind = exactVersion; - version = 10.52.0; + version = 10.54.0; }; }; F72CD01027A7E92400E59476 /* XCRemoteSwiftPackageReference "JGProgressHUD" */ = { @@ -5743,7 +6031,7 @@ repositoryURL = "https://github.com/krzyzanowskim/OpenSSL"; requirement = { kind = exactVersion; - version = 3.1.5004; + version = 3.3.2000; }; }; F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */ = { @@ -5759,7 +6047,7 @@ repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { kind = exactVersion; - version = 4.0.8; + version = 5.0.0; }; }; F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { @@ -5786,6 +6074,14 @@ version = 3.3.0; }; }; + F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/tylerjonesio/vlckit-spm"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.5.1; + }; + }; F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marinofaggiana/Mantis"; @@ -6268,6 +6564,11 @@ package = F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */; productName = Parchment; }; + F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { + isa = XCSwiftPackageProductDependency; + package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; + productName = VLCKitSPM; + }; F7D56B192972405500FA46C4 /* Mantis */ = { isa = XCSwiftPackageProductDependency; package = F7D56B182972405400FA46C4 /* XCRemoteSwiftPackageReference "Mantis" */; diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme index 63a60a11ef..37b52330d0 100755 --- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/File Provider Extension.xcscheme @@ -120,8 +120,12 @@ debugServiceExtension = "internal" allowLocationSimulation = "YES" launchAutomaticallySubstyle = "2"> - + + + - + + RemotePath = "/var/containers/Bundle/Application/92F13FE8-7056-4509-8468-5856675AB1CA/Nextcloud.app"> CGSize { var height: CGFloat = 0 - if dataSource.getMetadataSourceForAllSections().isEmpty { - height = NCGlobal.shared.getHeightHeaderEmptyData(view: view, portraitOffset: 0, landscapeOffset: -50) + if self.dataSource.isEmpty() { + height = NCUtility().getHeightHeaderEmptyData(view: view, portraitOffset: 0, landscapeOffset: -50) } return CGSize(width: collectionView.frame.width, height: height) } } extension NCShareExtension: UICollectionViewDataSource { - func numberOfSections(in collectionView: UICollectionView) -> Int { return dataSource.numberOfSections() } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return dataSource.numberOfItemsInSection(section) + return self.dataSource.numberOfItemsInSection(section) } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let metadata = dataSource.cellForItemAt(indexPath: indexPath), let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell else { - return UICollectionViewCell() + let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)! + guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { + return cell } - cell.fileObjectId = metadata.ocId - cell.indexPath = indexPath + cell.fileOcId = metadata.ocId + cell.fileOcIdTransfer = metadata.ocIdTransfer cell.fileUser = metadata.ownerId cell.labelTitle.text = metadata.fileNameView cell.labelTitle.textColor = NCBrandColor.shared.textColor @@ -102,14 +101,13 @@ extension NCShareExtension: UICollectionViewDataSource { cell.imageMore.image = nil cell.imageItem.image = nil cell.imageItem.backgroundColor = nil - cell.progressView.progress = 0.0 if metadata.directory { setupDirectoryCell(cell, indexPath: indexPath, with: metadata) } if metadata.favorite { - cell.imageFavorite.image = NCImageCache.images.favorite + cell.imageFavorite.image = NCImageCache.shared.getImageFavorite() } cell.imageSelect.isHidden = true @@ -119,7 +117,7 @@ extension NCShareExtension: UICollectionViewDataSource { cell.selected(false, isEditMode: false) if metadata.isLivePhoto { - cell.imageStatus.image = NCImageCache.images.livePhoto + cell.imageStatus.image = utility.loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor2]) } cell.setTags(tags: Array(metadata.tags)) @@ -139,31 +137,31 @@ extension NCShareExtension: UICollectionViewDataSource { } if metadata.e2eEncrypted { - cell.imageItem.image = NCImageCache.images.folderEncrypted + cell.imageItem.image = NCImageCache.shared.getFolderEncrypted(account: metadata.account) } else if isShare { - cell.imageItem.image = NCImageCache.images.folderSharedWithMe + cell.imageItem.image = NCImageCache.shared.getFolderSharedWithMe(account: metadata.account) } else if !metadata.shareType.isEmpty { metadata.shareType.contains(3) ? - (cell.imageItem.image = NCImageCache.images.folderPublic) : - (cell.imageItem.image = NCImageCache.images.folderSharedWithMe) + (cell.imageItem.image = NCImageCache.shared.getFolderPublic(account: metadata.account)) : + (cell.imageItem.image = NCImageCache.shared.getFolderSharedWithMe(account: metadata.account)) } else if metadata.mountType == "group" { - cell.imageItem.image = NCImageCache.images.folderGroup + cell.imageItem.image = NCImageCache.shared.getFolderGroup(account: metadata.account) } else if isMounted { - cell.imageItem.image = NCImageCache.images.folderExternal + cell.imageItem.image = NCImageCache.shared.getFolderExternal(account: metadata.account) } else if metadata.fileName == autoUploadFileName && metadata.serverUrl == autoUploadDirectory { - cell.imageItem.image = NCImageCache.images.folderAutomaticUpload + cell.imageItem.image = NCImageCache.shared.getFolderAutomaticUpload(account: metadata.account) } else { - cell.imageItem.image = NCImageCache.images.folder + cell.imageItem.image = NCImageCache.shared.getFolder(account: metadata.account) } cell.labelInfo.text = utility.dateDiff(metadata.date as Date) let lockServerUrl = utilityFileSystem.stringAppendServerUrl(metadata.serverUrl, addFileName: metadata.fileName) - let tableDirectory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", activeAccount.account, lockServerUrl)) + let tableDirectory = self.database.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, lockServerUrl)) // Local image: offline if tableDirectory != nil && tableDirectory!.offline { - cell.imageLocal.image = NCImageCache.images.offlineFlag + cell.imageLocal.image = NCImageCache.shared.getImageOfflineFlag() } } } @@ -171,7 +169,6 @@ extension NCShareExtension: UICollectionViewDataSource { // MARK: - Table View (uploading files) extension NCShareExtension: UITableViewDelegate { - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return heightRowTableView } @@ -179,12 +176,11 @@ extension NCShareExtension: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard !uploadStarted else { return } let fileName = filesName[indexPath.row] - renameFile(named: fileName) + renameFile(named: fileName, account: session.account) } } extension NCShareExtension: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { filesName.count } @@ -192,7 +188,7 @@ extension NCShareExtension: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? NCShareCell else { return UITableViewCell() } let fileName = filesName[indexPath.row] - cell.setup(fileName: fileName) + cell.setup(fileName: fileName, account: session.account) cell.delegate = self return cell } diff --git a/Share/NCShareExtension+Files.swift b/Share/NCShareExtension+Files.swift index 1da8b52ef6..6c64a1a06e 100644 --- a/Share/NCShareExtension+Files.swift +++ b/Share/NCShareExtension+Files.swift @@ -22,14 +22,16 @@ // import Foundation +import UIKit import UniformTypeIdentifiers import NextcloudKit extension NCShareExtension { @objc func reloadDatasource(withLoadFolder: Bool) { - layoutForView = NCManageDatabase.shared.setLayoutForView(account: activeAccount.account, key: keyLayout, serverUrl: serverUrl) - let metadatas = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND directory == true", activeAccount.account, serverUrl)) - self.dataSource = NCDataSource(metadatas: metadatas, account: activeAccount.account, layoutForView: layoutForView) + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND directory == true", session.account, serverUrl) + let results = self.database.getResultsMetadatasPredicate(predicate, layoutForView: NCDBLayoutForView()) + + self.dataSource = NCCollectionViewDataSource(results: results) if withLoadFolder { loadFolder() @@ -42,7 +44,7 @@ extension NCShareExtension { @objc func didCreateFolder(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let ocId = userInfo["ocId"] as? String, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) + let metadata = self.database.getMetadataFromOcId(ocId) else { return } self.serverUrl += "/" + metadata.fileName @@ -51,10 +53,13 @@ extension NCShareExtension { } func loadFolder() { - NCNetworking.shared.readFolder(serverUrl: serverUrl, account: activeAccount.account) { task in + NCNetworking.shared.readFolder(serverUrl: serverUrl, + account: session.account, + checkResponseDataChanged: false, + queue: .main) { task in self.dataSourceTask = task self.collectionView.reloadData() - } completion: { _, metadataFolder, _, _, _, error in + } completion: { _, metadataFolder, _, _, error in DispatchQueue.main.async { if error != .success { self.showAlert(description: error.errorDescription) diff --git a/Share/NCShareExtension+NCDelegate.swift b/Share/NCShareExtension+NCAccountRequestDelegate.swift similarity index 64% rename from Share/NCShareExtension+NCDelegate.swift rename to Share/NCShareExtension+NCAccountRequestDelegate.swift index a3a1e1f76e..7d4a084112 100644 --- a/Share/NCShareExtension+NCDelegate.swift +++ b/Share/NCShareExtension+NCAccountRequestDelegate.swift @@ -29,16 +29,16 @@ extension NCShareExtension: NCAccountRequestDelegate { // MARK: - Account func showAccountPicker() { - let accounts = NCManageDatabase.shared.getAllAccountOrderAlias() + let accounts = self.database.getAllAccountOrderAlias() guard accounts.count > 1, let vcAccountRequest = UIStoryboard(name: "NCAccountRequest", bundle: nil).instantiateInitialViewController() as? NCAccountRequest else { return } // Only here change the active account for account in accounts { - account.active = account.account == self.activeAccount.account + account.active = account.account == session.account } - vcAccountRequest.activeAccount = self.activeAccount + vcAccountRequest.activeAccount = self.session.account vcAccountRequest.accounts = accounts.sorted { sorg, dest -> Bool in return sorg.active && !dest.active } @@ -57,49 +57,37 @@ extension NCShareExtension: NCAccountRequestDelegate { func accountRequestAddAccount() { } - func accountRequestChangeAccount(account: String) { - guard let activeAccount = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", account)) else { + func accountRequestChangeAccount(account: String, controller: UIViewController?) { + guard let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", account)), + let capabilities = self.database.setCapabilities(account: account) else { cancel(with: NCShareExtensionError.noAccount) return } - self.activeAccount = activeAccount - - // CAPABILITIES - NCManageDatabase.shared.setCapabilities(account: account) + self.account = account // COLORS - NCBrandColor.shared.settingThemingColor(account: activeAccount.account) - NCBrandColor.shared.createUserColors() - NCImageCache.shared.createImagesBrandCache() + NCBrandColor.shared.settingThemingColor(account: account) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeTheming, userInfo: ["account": account]) // NETWORKING - NextcloudKit.shared.setup( - account: activeAccount.account, - user: activeAccount.user, - userId: activeAccount.userId, - password: NCKeychain().getPassword(account: activeAccount.account), - urlBase: activeAccount.urlBase, - userAgent: userAgent, - nextcloudVersion: 0, - delegate: NCNetworking.shared) + NextcloudKit.shared.setup(delegate: NCNetworking.shared) + NextcloudKit.shared.appendSession(account: tableAccount.account, + urlBase: tableAccount.urlBase, + user: tableAccount.user, + userId: tableAccount.userId, + password: NCKeychain().getPassword(account: tableAccount.account), + userAgent: userAgent, + nextcloudVersion: capabilities.capabilityServerVersionMajor, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) // get auto upload folder - autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() - autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: activeAccount.urlBase, userId: activeAccount.userId, account: activeAccount.account) - - serverUrl = utilityFileSystem.getHomeServer(urlBase: activeAccount.urlBase, userId: activeAccount.userId) + autoUploadFileName = self.database.getAccountAutoUploadFileName() + autoUploadDirectory = self.database.getAccountAutoUploadDirectory(session: session) - layoutForView = NCManageDatabase.shared.getLayoutForView(account: activeAccount.account, key: keyLayout, serverUrl: serverUrl) + serverUrl = utilityFileSystem.getHomeServer(session: session) reloadDatasource(withLoadFolder: true) setNavigationBar(navigationTitle: NCBrandOptions.shared.brand) - - FileNameValidator.shared.setup( - forbiddenFileNames: NCGlobal.shared.capabilityForbiddenFileNames, - forbiddenFileNameBasenames: NCGlobal.shared.capabilityForbiddenFileNameBasenames, - forbiddenFileNameCharacters: NCGlobal.shared.capabilityForbiddenFileNameCharacters, - forbiddenFileNameExtensions: NCGlobal.shared.capabilityForbiddenFileNameExtensions - ) } } @@ -117,20 +105,8 @@ extension NCShareExtension: NCCreateFormUploadConflictDelegate { } extension NCShareExtension: NCShareCellDelegate { - func removeFile(named fileName: String) { - guard let index = self.filesName.firstIndex(of: fileName) else { - return showAlert(title: "_file_not_found_", description: fileName) - } - self.filesName.remove(at: index) - if self.filesName.isEmpty { - cancel(with: NCShareExtensionError.noFiles) - } else { - self.setCommandView() - } - } - - func renameFile(named fileName: String) { - let alert = UIAlertController.renameFile(fileName: fileName) { [self] newFileName in + func renameFile(named fileName: String, account: String) { + let alert = UIAlertController.renameFile(fileName: fileName, account: account) { [self] newFileName in guard let fileIx = self.filesName.firstIndex(of: fileName), !self.filesName.contains(newFileName), utilityFileSystem.moveFile(atPath: (NSTemporaryDirectory() + fileName), toPath: (NSTemporaryDirectory() + newFileName)) else { @@ -143,4 +119,16 @@ extension NCShareExtension: NCShareCellDelegate { present(alert, animated: true) } + + func removeFile(named fileName: String) { + guard let index = self.filesName.firstIndex(of: fileName) else { + return showAlert(title: "_file_not_found_", description: fileName) + } + self.filesName.remove(at: index) + if self.filesName.isEmpty { + cancel(with: NCShareExtensionError.noFiles) + } else { + self.setCommandView() + } + } } diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift index 47fa2bd13f..cae085e73c 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -25,15 +25,12 @@ import UIKit import NextcloudKit -import JGProgressHUD -import TOPasscodeViewController enum NCShareExtensionError: Error { case cancel, fileUpload, noAccount, noFiles } class NCShareExtension: UIViewController { - @IBOutlet weak var collectionView: UICollectionView! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var cancelButton: UIBarButtonItem! @@ -57,22 +54,33 @@ class NCShareExtension: UIViewController { let keyLayout = NCGlobal.shared.layoutViewShareExtension var metadataFolder: tableMetadata? var dataSourceTask: URLSessionTask? - var dataSource = NCDataSource() - var layoutForView: NCDBLayoutForView? + var dataSource = NCCollectionViewDataSource() let heightRowTableView: CGFloat = 50 let heightCommandView: CGFloat = 170 var autoUploadFileName = "" var autoUploadDirectory = "" let refreshControl = UIRefreshControl() - var activeAccount: tableAccount! var progress: CGFloat = 0 var counterUploaded: Int = 0 var uploadErrors: [tableMetadata] = [] var uploadMetadata: [tableMetadata] = [] var uploadStarted = false - let hud = JGProgressHUD() + let hud = NCHud() let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() + let database = NCManageDatabase.shared + var account: String = "" + var session: NCSession.Session { + if !account.isEmpty, + let tableAccount = self.database.getTableAccount(account: account) { + return NCSession.Session(account: tableAccount.account, urlBase: tableAccount.urlBase, user: tableAccount.user, userId: tableAccount.userId) + } else if let activeTableAccount = self.database.getActiveTableAccount() { + self.account = activeTableAccount.account + return NCSession.Session(account: activeTableAccount.account, urlBase: activeTableAccount.urlBase, user: activeTableAccount.user, userId: activeTableAccount.userId) + } else { + return NCSession.Session(account: "", urlBase: "", user: "", userId: "") + } + } // MARK: - View Life Cycle @@ -86,7 +94,7 @@ class NCShareExtension: UIViewController { collectionView.collectionViewLayout = NCListLayout() collectionView.refreshControl = refreshControl - refreshControl.tintColor = NCBrandColor.shared.brandText + refreshControl.tintColor = NCBrandColor.shared.iconImageColor refreshControl.backgroundColor = .systemBackground refreshControl.addTarget(self, action: #selector(reloadDatasource), for: .valueChanged) @@ -125,33 +133,20 @@ class NCShareExtension: UIViewController { NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Start Share session with level \(levelLog) " + versionNextcloudiOS) } - // Colors - if let activeAccount = NCManageDatabase.shared.getActiveAccount() { - NCBrandColor.shared.settingThemingColor(account: activeAccount.account) - } NCBrandColor.shared.createUserColors() - NCImageCache.shared.createImagesCache() - NCImageCache.shared.createImagesBrandCache() - - hud.indicatorView = JGProgressHUDRingIndicatorView() - if let indicatorView = hud.indicatorView as? JGProgressHUDRingIndicatorView { - indicatorView.ringWidth = 1.5 - indicatorView.ringColor = NCBrandColor.shared.brandElement - } NotificationCenter.default.addObserver(self, selector: #selector(didCreateFolder(_:)), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCreateFolder), object: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - guard serverUrl.isEmpty else { return } - guard let activeAccount = NCManageDatabase.shared.getActiveAccount(), + guard !session.account.isEmpty, !NCPasscode.shared.isPasscodeReset else { return showAlert(description: "_no_active_account_") { self.cancel(with: .noAccount) } } - accountRequestChangeAccount(account: activeAccount.account) + accountRequestChangeAccount(account: account, controller: nil) guard let inputItems = extensionContext?.inputItems as? [NSExtensionItem] else { cancel(with: .noFiles) return @@ -197,7 +192,6 @@ class NCShareExtension: UIViewController { } func setNavigationBar(navigationTitle: String) { - navigationItem.title = navigationTitle cancelButton.title = NSLocalizedString("_cancel_", comment: "") @@ -212,28 +206,26 @@ class NCShareExtension: UIViewController { if !self.uploadStarted { while self.serverUrl.last != "/" { self.serverUrl.removeLast() } self.serverUrl.removeLast() - self.reloadDatasource(withLoadFolder: true) - var navigationTitle = (self.serverUrl as NSString).lastPathComponent - if self.utilityFileSystem.getHomeServer(urlBase: self.activeAccount.urlBase, userId: self.activeAccount.userId) == self.serverUrl { + if self.utilityFileSystem.getHomeServer(session: self.session) == self.serverUrl { navigationTitle = NCBrandOptions.shared.brand } self.setNavigationBar(navigationTitle: navigationTitle) } } - let image = utility.loadUserImage(for: activeAccount.user, displayName: activeAccount.displayName, userBaseUrl: activeAccount) + let tableAccount = self.database.getTableAccount(account: session.account) + let image = utility.loadUserImage(for: session.user, displayName: tableAccount?.displayName, urlBase: session.urlBase) let profileButton = UIButton(type: .custom) profileButton.setImage(image, for: .normal) - if serverUrl == utilityFileSystem.getHomeServer(urlBase: activeAccount.urlBase, userId: activeAccount.userId) { - + if serverUrl == utilityFileSystem.getHomeServer(session: self.session) { var title = " " - if let userAlias = activeAccount?.alias, !userAlias.isEmpty { + if let userAlias = tableAccount?.alias, !userAlias.isEmpty { title += userAlias } else { - title += activeAccount?.displayName ?? "" + title += tableAccount?.displayName ?? "" } profileButton.setTitle(title, for: .normal) @@ -248,7 +240,7 @@ class NCShareExtension: UIViewController { } } var navItems = [UIBarButtonItem(customView: profileButton)] - if serverUrl != utilityFileSystem.getHomeServer(urlBase: activeAccount.urlBase, userId: activeAccount.userId) { + if serverUrl != utilityFileSystem.getHomeServer(session: self.session) { let space = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) space.width = 20 navItems.append(contentsOf: [UIBarButtonItem(customView: backButton), space]) @@ -278,7 +270,7 @@ class NCShareExtension: UIViewController { } @objc func actionCreateFolder() { - let alertController = UIAlertController.createFolder(serverUrl: serverUrl, userBaseUrl: activeAccount) { error in + let alertController = UIAlertController.createFolder(serverUrl: serverUrl, account: session.account) { error in guard error != .success else { return } self.showAlert(title: "_error_createsubfolders_upload_", description: error.errorDescription) } @@ -298,7 +290,7 @@ extension NCShareExtension { var conflicts: [tableMetadata] = [] for fileName in filesName { - if let fileNameError = FileNameValidator.shared.checkFileName(fileName) { + if let fileNameError = FileNameValidator.shared.checkFileName(fileName, account: session.account) { present(UIAlertController.warning(message: "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))"), animated: true) continue @@ -307,27 +299,32 @@ extension NCShareExtension { let ocId = NSUUID().uuidString let toPath = utilityFileSystem.getDirectoryProviderStorageOcId(ocId, fileNameView: fileName) guard utilityFileSystem.copyFile(atPath: (NSTemporaryDirectory() + fileName), toPath: toPath) else { continue } - let metadata = NCManageDatabase.shared.createMetadata( - account: activeAccount.account, user: activeAccount.user, userId: activeAccount.userId, - fileName: fileName, fileNameView: fileName, - ocId: ocId, - serverUrl: serverUrl, urlBase: activeAccount.urlBase, url: "", - contentType: "") - metadata.session = NextcloudKit.shared.nkCommonInstance.sessionIdentifierUpload - metadata.sessionSelector = NCGlobal.shared.selectorUploadFileShareExtension - metadata.size = utilityFileSystem.getFileSize(filePath: toPath) - metadata.status = NCGlobal.shared.metadataStatusWaitUpload - metadata.sessionDate = Date() - if NCManageDatabase.shared.getMetadataConflict(account: activeAccount.account, serverUrl: serverUrl, fileNameView: fileName) != nil { - conflicts.append(metadata) + let metadataForUpload = self.database.createMetadata(fileName: fileName, + fileNameView: fileName, + ocId: ocId, + serverUrl: serverUrl, + url: "", + contentType: "", + session: session, + sceneIdentifier: nil) + + metadataForUpload.session = NCNetworking.shared.sessionUpload + metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFileShareExtension + metadataForUpload.size = utilityFileSystem.getFileSize(filePath: toPath) + metadataForUpload.status = NCGlobal.shared.metadataStatusWaitUpload + metadataForUpload.sessionDate = Date() + if self.database.getMetadataConflict(account: session.account, serverUrl: serverUrl, fileNameView: fileName) != nil { + conflicts.append(metadataForUpload) } else { - uploadMetadata.append(metadata) + uploadMetadata.append(metadataForUpload) } } if !conflicts.isEmpty { guard let conflict = UIStoryboard(name: "NCCreateFormUploadConflict", bundle: nil).instantiateInitialViewController() as? NCCreateFormUploadConflict else { return } + + conflict.account = session.account conflict.serverUrl = self.serverUrl conflict.metadatasUploadInConflict = conflicts conflict.delegate = self @@ -341,8 +338,8 @@ extension NCShareExtension { guard uploadStarted else { return } guard uploadMetadata.count > counterUploaded else { return DispatchQueue.main.async { self.finishedUploading() } } let metadata = uploadMetadata[counterUploaded] + let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, mimeType: metadata.contentType, directory: false, account: session.account) - let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, mimeType: metadata.contentType, directory: false) metadata.contentType = results.mimeType metadata.iconName = results.iconName metadata.classFile = results.classFile @@ -359,22 +356,16 @@ extension NCShareExtension { // E2EE metadata.e2eEncrypted = metadata.isDirectoryE2EE - DispatchQueue.main.async { - self.hud.show(in: self.view) - self.hud.textLabel.text = NSLocalizedString("_upload_file_", comment: "") + " \(self.counterUploaded + 1) " + NSLocalizedString("_of_", comment: "") + " \(self.filesName.count)" - } + hud.initHudRing(view: self.view, + text: NSLocalizedString("_upload_file_", comment: "") + " \(self.counterUploaded + 1) " + NSLocalizedString("_of_", comment: "") + " \(self.filesName.count)") - NCNetworking.shared.upload(metadata: metadata, uploadE2EEDelegate: self, hudView: self.view, hud: JGProgressHUD()) { - DispatchQueue.main.async { - self.hud.progress = 0 - } + NCNetworking.shared.upload(metadata: metadata, uploadE2EEDelegate: self, controller: self) { + self.hud.progress(0) } progressHandler: { _, _, fractionCompleted in - DispatchQueue.main.async { - self.hud.progress = Float(fractionCompleted) - } + self.hud.progress(fractionCompleted) } completion: { _, error in if error != .success { - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) + self.database.deleteMetadataOcId(metadata.ocId) self.utilityFileSystem.removeFile(atPath: self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId)) self.uploadErrors.append(metadata) } @@ -391,9 +382,7 @@ extension NCShareExtension { self.extensionContext?.cancelRequest(withError: NCShareExtensionError.fileUpload) } } else { - hud.indicatorView = JGProgressHUDSuccessIndicatorView() - hud.indicatorView?.tintColor = NCBrandColor.shared.brandElement - hud.textLabel.text = NSLocalizedString("_success_", comment: "") + hud.success() DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self.extensionContext?.completeRequest(returningItems: self.extensionContext?.inputItems, completionHandler: nil) } @@ -403,11 +392,11 @@ extension NCShareExtension { extension NCShareExtension: uploadE2EEDelegate { func start() { - self.hud.progress = 0 + self.hud.progress(0) } func uploadE2EEProgress(_ totalBytesExpected: Int64, _ totalBytes: Int64, _ fractionCompleted: Double) { - self.hud.progress = Float(fractionCompleted) + self.hud.progress(fractionCompleted) } } diff --git a/Share/Share-Bridging-Header.h b/Share/Share-Bridging-Header.h index c19b600db6..d2918d26c8 100644 --- a/Share/Share-Bridging-Header.h +++ b/Share/Share-Bridging-Header.h @@ -4,3 +4,4 @@ #import "NCEndToEndEncryption.h" #import "UIImage+animatedGIF.h" +#import "TOPasscodeViewController.h" diff --git a/Tests/Common/BaseXCTestCase.swift b/Tests/Common/BaseXCTestCase.swift index 8d80e6dab3..5c03bc5384 100644 --- a/Tests/Common/BaseXCTestCase.swift +++ b/Tests/Common/BaseXCTestCase.swift @@ -8,6 +8,7 @@ import XCTest import Foundation +import UIKit import Alamofire import NextcloudKit @testable import Nextcloud diff --git a/Tests/Common/TestConstants.swift b/Tests/Common/TestConstants.swift index 25293bfbbb..9bf27f7be5 100644 --- a/Tests/Common/TestConstants.swift +++ b/Tests/Common/TestConstants.swift @@ -20,6 +20,7 @@ // import Foundation +import UIKit public class TestConstants { static let timeoutLong: Double = 600 diff --git a/Tests/NextcloudIntegrationTests/FilesIntegrationTests.swift b/Tests/NextcloudIntegrationTests/FilesIntegrationTests.swift index f538479081..c3b16c80b8 100644 --- a/Tests/NextcloudIntegrationTests/FilesIntegrationTests.swift +++ b/Tests/NextcloudIntegrationTests/FilesIntegrationTests.swift @@ -24,10 +24,9 @@ import NextcloudKit @testable import Nextcloud final class FilesIntegrationTests: BaseIntegrationXCTestCase { - private let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! override func setUp() { - appDelegate.deleteAllAccounts() + NCAccount().deleteAllAccounts() } func test_createReadDeleteFolder_withProperParams_shouldCreateReadDeleteFolder() throws { @@ -36,11 +35,14 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { let folderName = "TestFolder\(randomInt)" let serverUrl = "\(TestConstants.server)/remote.php/dav/files/\(TestConstants.username)" let serverUrlFileName = "\(serverUrl)/\(folderName)" + let domain = NCDomain.Domain(account: TestConstants.account, urlBase: TestConstants.server, user: TestConstants.username, userId: TestConstants.username, sceneIdentifier: "") - NextcloudKit.shared.setup(account: TestConstants.account, user: TestConstants.username, userId: TestConstants.username, password: appToken, urlBase: TestConstants.server, groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) + NextcloudKit.shared.setup(delegate: NCNetworking.shared) + NextcloudKit.shared.appendAccount(TestConstants.account, urlBase: TestConstants.server, user: TestConstants.username, userId: TestConstants.username, password: appToken, userAgent: userAgent, nextcloudVersion: 0, groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) // Test creating folder - NCNetworking.shared.createFolder(fileName: folderName, serverUrl: serverUrl, account: TestConstants.account, urlBase: TestConstants.server, userId: TestConstants.username, withPush: true, sceneIdentifier: nil) { error in + NCNetworking.shared.createFolder(fileName: folderName, serverUrl: serverUrl, overwrite: true, withPush: true, sceneIdentifier: nil, domain: domain) { error in + XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) @@ -61,7 +63,7 @@ final class FilesIntegrationTests: BaseIntegrationXCTestCase { Task { // Test deleting folder - await _ = NCNetworking.shared.deleteMetadata(metadataFolder!, onlyLocalCache: false) + await _ = NCNetworking.shared.deleteMetadata(metadataFolder!) XCTAssertEqual(NKError.success.errorCode, error.errorCode) XCTAssertEqual(NKError.success.errorDescription, error.errorDescription) diff --git a/Widget/Dashboard/DashboardData.swift b/Widget/Dashboard/DashboardData.swift index ce161954d0..322d896280 100644 --- a/Widget/Dashboard/DashboardData.swift +++ b/Widget/Dashboard/DashboardData.swift @@ -39,6 +39,7 @@ struct DashboardDataEntry: TimelineEntry { let title: String let footerImage: String let footerText: String + let account: String } struct DashboardData: Identifiable, Hashable { @@ -101,45 +102,44 @@ func getDashboardDataEntry(configuration: DashboardIntent?, isPreview: Bool, dis let utility = NCUtility() let dashboardItems = getDashboardItems(displaySize: displaySize, withButton: false) let datasPlaceholder = Array(dashboardDatasTest[0...dashboardItems - 1]) - var account: tableAccount? + var activeTableAccount: tableAccount? if isPreview { - return completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: UIImage(named: "widget")!, title: "Dashboard", footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " dashboard")) + return completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: UIImage(named: "widget")!, title: "Dashboard", footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " dashboard", account: "")) } let accountIdentifier: String = configuration?.accounts?.identifier ?? "active" if accountIdentifier == "active" { - account = NCManageDatabase.shared.getActiveAccount() + activeTableAccount = NCManageDatabase.shared.getActiveTableAccount() } else { - account = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) + activeTableAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) } - guard let account = account else { - return completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: UIImage(named: "widget")!, title: "Dashboard", footerImage: "xmark.icloud", footerText: NSLocalizedString("_no_active_account_", comment: ""))) + guard let activeTableAccount, + let capabilities = NCManageDatabase.shared.setCapabilities(account: activeTableAccount.account) else { + return completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: UIImage(named: "widget")!, title: "Dashboard", footerImage: "xmark.icloud", footerText: NSLocalizedString("_no_active_account_", comment: ""), account: "")) } // Default widget - let result = NCManageDatabase.shared.getDashboardWidgetApplications(account: account.account).first + let result = NCManageDatabase.shared.getDashboardWidgetApplications(account: activeTableAccount.account).first let id: String = configuration?.applications?.identifier ?? (result?.id ?? "recommendations") - // Capabilities - NCManageDatabase.shared.setCapabilities(account: account.account) - - guard NCGlobal.shared.capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion25 else { - return completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: UIImage(named: "widget")!, title: "Dashboard", footerImage: "xmark.icloud", footerText: NSLocalizedString("_widget_available_nc25_", comment: ""))) + guard capabilities.capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion25 else { + return completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: UIImage(named: "widget")!, title: "Dashboard", footerImage: "xmark.icloud", footerText: NSLocalizedString("_widget_available_nc25_", comment: ""), account: activeTableAccount.account)) } // NETWORKING - let password = NCKeychain().getPassword(account: account.account) - NextcloudKit.shared.setup( - account: account.account, - user: account.user, - userId: account.userId, - password: password, - urlBase: account.urlBase, - userAgent: userAgent, - nextcloudVersion: 0, - delegate: NCNetworking.shared) + let password = NCKeychain().getPassword(account: activeTableAccount.account) + + NextcloudKit.shared.setup(delegate: NCNetworking.shared) + NextcloudKit.shared.appendSession(account: activeTableAccount.account, + urlBase: activeTableAccount.urlBase, + user: activeTableAccount.user, + userId: activeTableAccount.userId, + password: password, + userAgent: userAgent, + nextcloudVersion: capabilities.capabilityServerVersionMajor, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) // LOG let levelLog = NCKeychain().logLevel @@ -154,7 +154,7 @@ func getDashboardDataEntry(configuration: DashboardIntent?, isPreview: Bool, dis NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Start \(NCBrandOptions.shared.brand) dashboard widget session with level \(levelLog) " + versionNextcloudiOS) } - let (tableDashboard, tableButton) = NCManageDatabase.shared.getDashboardWidget(account: account.account, id: id) + let (tableDashboard, tableButton) = NCManageDatabase.shared.getDashboardWidget(account: activeTableAccount.account, id: id) let existsButton = (tableButton?.isEmpty ?? true) ? false : true let title = tableDashboard?.title ?? id @@ -168,7 +168,7 @@ func getDashboardDataEntry(configuration: DashboardIntent?, isPreview: Bool, dis let titleImage = imagetmp let options = NKRequestOptions(timeout: 90, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) - NextcloudKit.shared.getDashboardWidgetsApplication(id, account: account.account, options: options) { _, results, data, error in + NextcloudKit.shared.getDashboardWidgetsApplication(id, account: activeTableAccount.account, options: options) { account, results, responseData, error in Task { var datas = [DashboardData]() var numberItems = 0 @@ -225,9 +225,10 @@ func getDashboardDataEntry(configuration: DashboardIntent?, isPreview: Bool, dis if FileManager().fileExists(atPath: fileNamePath), let image = UIImage(contentsOfFile: fileNamePath) { icon = image } else { - let (_, data, error) = await NCNetworking.shared.downloadPreview(url: url, account: account.account) + let (_, data, error) = await NCNetworking.shared.downloadPreview(url: url, account: activeTableAccount.account) if error == .success, - let image = convertDataToImage(data: data, size: CGSize(width: 256, height: 256), fileNameToWrite: fileName) { + let data = responseData?.data, + let image = convertDataToImage(data: data, size: NCGlobal.shared.size256, fileNameToWrite: fileName) { icon = image } } @@ -248,13 +249,13 @@ func getDashboardDataEntry(configuration: DashboardIntent?, isPreview: Bool, dis buttons = tableButton.filter(({ $0.type != "more" })) } - let alias = (account.alias.isEmpty) ? "" : (" (" + account.alias + ")") - let footerText = "Dashboard " + NSLocalizedString("_of_", comment: "") + " " + account.displayName + alias + let alias = (activeTableAccount.alias.isEmpty) ? "" : (" (" + activeTableAccount.alias + ")") + let footerText = "Dashboard " + NSLocalizedString("_of_", comment: "") + " " + activeTableAccount.displayName + alias if error != .success { - completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: tableDashboard, buttons: buttons, isPlaceholder: true, isEmpty: false, titleImage: titleImage, title: title, footerImage: "xmark.icloud", footerText: error.errorDescription)) + completion(DashboardDataEntry(date: Date(), datas: datasPlaceholder, dashboard: tableDashboard, buttons: buttons, isPlaceholder: true, isEmpty: false, titleImage: titleImage, title: title, footerImage: "xmark.icloud", footerText: error.errorDescription, account: account)) } else { - completion(DashboardDataEntry(date: Date(), datas: datas, dashboard: tableDashboard, buttons: buttons, isPlaceholder: false, isEmpty: datas.isEmpty, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: footerText)) + completion(DashboardDataEntry(date: Date(), datas: datas, dashboard: tableDashboard, buttons: buttons, isPlaceholder: false, isEmpty: datas.isEmpty, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: footerText, account: account)) } } } diff --git a/Widget/Dashboard/DashboardWidgetProvider.swift b/Widget/Dashboard/DashboardWidgetProvider.swift index 6a2ad42796..419bedda6c 100644 --- a/Widget/Dashboard/DashboardWidgetProvider.swift +++ b/Widget/Dashboard/DashboardWidgetProvider.swift @@ -35,7 +35,7 @@ struct DashboardWidgetProvider: IntentTimelineProvider { let datasPlaceholder = Array(dashboardDatasTest[0...dashboardItems]) let title = "Dashboard" let titleImage = UIImage(named: "widget")! - return Entry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " widget") + return Entry(date: Date(), datas: datasPlaceholder, dashboard: nil, buttons: nil, isPlaceholder: true, isEmpty: false, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " widget", account: "") } func getSnapshot(for configuration: DashboardIntent, in context: Context, completion: @escaping (DashboardDataEntry) -> Void) { diff --git a/Widget/Dashboard/DashboardWidgetView.swift b/Widget/Dashboard/DashboardWidgetView.swift index 89f98123b1..f7cc722054 100644 --- a/Widget/Dashboard/DashboardWidgetView.swift +++ b/Widget/Dashboard/DashboardWidgetView.swift @@ -25,13 +25,9 @@ import SwiftUI import WidgetKit struct DashboardWidgetView: View { - var entry: DashboardDataEntry - var body: some View { - GeometryReader { geo in - if entry.isEmpty { VStack(alignment: .center) { Image(systemName: "checkmark") @@ -156,9 +152,8 @@ struct DashboardWidgetView: View { if let buttons = entry.buttons, !buttons.isEmpty, !entry.isPlaceholder { HStack(spacing: 10) { - - let brandColor = Color(NCBrandColor.shared.brandElement) - let brandTextColor = Color(NCBrandColor.shared.brandText) + let brandColor = Color(NCBrandColor.shared.getElement(account: entry.account)) + let brandTextColor = Color(NCBrandColor.shared.getText(account: entry.account)) ForEach(buttons, id: \.index) { element in Link(destination: URL(string: element.link)!, label: { @@ -183,12 +178,12 @@ struct DashboardWidgetView: View { .scaledToFit() .frame(width: 15, height: 15) .font(Font.system(.body).weight(.light)) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) Text(entry.footerText) .font(.caption2) .lineLimit(1) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) } .padding(.horizontal, 15.0) .frame(maxWidth: geo.size.width, maxHeight: geo.size.height - 2, alignment: .bottomTrailing) @@ -203,7 +198,7 @@ struct DashboardWidget_Previews: PreviewProvider { let datas = Array(dashboardDatasTest[0...4]) let title = "Dashboard" let titleImage = UIImage(named: "widget")! - let entry = DashboardDataEntry(date: Date(), datas: datas, dashboard: nil, buttons: nil, isPlaceholder: false, isEmpty: true, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: "Nextcloud widget") + let entry = DashboardDataEntry(date: Date(), datas: datas, dashboard: nil, buttons: nil, isPlaceholder: false, isEmpty: true, titleImage: titleImage, title: title, footerImage: "checkmark.icloud", footerText: "Nextcloud widget", account: "") DashboardWidgetView(entry: entry).previewContext(WidgetPreviewContext(family: .systemLarge)) } } diff --git a/Widget/Files/FilesData.swift b/Widget/Files/FilesData.swift index 21922d6682..1f69a2398e 100644 --- a/Widget/Files/FilesData.swift +++ b/Widget/Files/FilesData.swift @@ -33,6 +33,7 @@ struct FilesDataEntry: TimelineEntry { let isEmpty: Bool let userId: String let url: String + let account: String let tile: String let footerImage: String let footerText: String @@ -60,7 +61,7 @@ let filesDatasTest: [FilesData] = [ .init(id: "9", image: UIImage(named: "widget")!, title: "title4", subTitle: "subTitle-description4", url: URL(string: "https://nextcloud.com/")!) ] -func getTitleFilesWidget(account: tableAccount?) -> String { +func getTitleFilesWidget(tableAccount: tableAccount?) -> String { let hour = Calendar.current.component(.hour, from: Date()) var good = "" @@ -72,8 +73,8 @@ func getTitleFilesWidget(account: tableAccount?) -> String { default: good = NSLocalizedString("_good_night_", value: "Good night", comment: "") } - if let account = account { - return good + ", " + account.displayName + if let tableAccount { + return good + ", " + tableAccount.displayName } else { return good } @@ -89,34 +90,35 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi let utility = NCUtility() let filesItems = getFilesItems(displaySize: displaySize) let datasPlaceholder = Array(filesDatasTest[0...filesItems - 1]) - var account: tableAccount? + var activeTableAccount: tableAccount? if isPreview { - return completion(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: "", url: "", tile: getTitleFilesWidget(account: nil), footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " files")) + return completion(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: "", url: "", account: "", tile: getTitleFilesWidget(tableAccount: nil), footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " files")) } let accountIdentifier: String = configuration?.accounts?.identifier ?? "active" if accountIdentifier == "active" { - account = NCManageDatabase.shared.getActiveAccount() + activeTableAccount = NCManageDatabase.shared.getActiveTableAccount() } else { - account = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) + activeTableAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) } - guard let account = account else { - return completion(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: "", url: "", tile: getTitleFilesWidget(account: nil), footerImage: "xmark.icloud", footerText: NSLocalizedString("_no_active_account_", value: "No account found", comment: ""))) + guard let activeTableAccount else { + return completion(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: "", url: "", account: "", tile: getTitleFilesWidget(tableAccount: nil), footerImage: "xmark.icloud", footerText: NSLocalizedString("_no_active_account_", value: "No account found", comment: ""))) } // NETWORKING - let password = NCKeychain().getPassword(account: account.account) - NextcloudKit.shared.setup( - account: account.account, - user: account.user, - userId: account.userId, - password: password, - urlBase: account.urlBase, - userAgent: userAgent, - nextcloudVersion: 0, - delegate: NCNetworking.shared) + let password = NCKeychain().getPassword(account: activeTableAccount.account) + + NextcloudKit.shared.setup(delegate: NCNetworking.shared) + NextcloudKit.shared.appendSession(account: activeTableAccount.account, + urlBase: activeTableAccount.urlBase, + user: activeTableAccount.user, + userId: activeTableAccount.userId, + password: password, + userAgent: userAgent, + nextcloudVersion: NCCapabilities.shared.getCapabilities(account: activeTableAccount.account).capabilityServerVersionMajor, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) let requestBodyRecent = """ @@ -173,7 +175,7 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" let lessDateString = dateFormatter.string(from: Date()) - let requestBody = String(format: requestBodyRecent, "/files/" + account.userId, lessDateString) + let requestBody = String(format: requestBodyRecent, "/files/" + activeTableAccount.userId, lessDateString) // LOG let levelLog = NCKeychain().logLevel @@ -190,11 +192,11 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi } let options = NKRequestOptions(timeout: 30, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) - NextcloudKit.shared.searchBodyRequest(serverUrl: account.urlBase, requestBody: requestBody, showHiddenFiles: NCKeychain().showHiddenFiles, account: account.account, options: options) { _, files, data, error in + NextcloudKit.shared.searchBodyRequest(serverUrl: activeTableAccount.urlBase, requestBody: requestBody, showHiddenFiles: NCKeychain().showHiddenFiles, account: activeTableAccount.account, options: options) { _, files, data, error in Task { var datas: [FilesData] = [] - let title = getTitleFilesWidget(account: account) - let files = files.sorted(by: { ($0.date as Date) > ($1.date as Date) }) + let title = getTitleFilesWidget(tableAccount: activeTableAccount) + let files = files?.sorted(by: { ($0.date as Date) > ($1.date as Date) }) ?? [] for file in files { var useTypeIconFile = false @@ -216,18 +218,18 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi guard let url = URL(string: urlString) else { continue } // IMAGE - let fileNamePreviewLocalPath = utilityFileSystem.getDirectoryProviderStoragePreviewOcId(file.ocId, etag: file.etag) - let fileNameIconLocalPath = utilityFileSystem.getDirectoryProviderStorageIconOcId(file.ocId, etag: file.etag) - if FileManager.default.fileExists(atPath: fileNameIconLocalPath) { - image = UIImage(contentsOfFile: fileNameIconLocalPath) - } + image = utility.getImage(ocId: file.ocId, etag: file.etag, ext: NCGlobal.shared.previewExt512) if image == nil, file.hasPreview { - let sizePreview = NCUtility().getSizePreview(width: Int(file.width), height: Int(file.height)) - let (_, _, imageIcon, _, _, _) = await NCNetworking.shared.downloadPreview(fileId: file.fileId, fileNamePreviewLocalPath: fileNamePreviewLocalPath, fileNameIconLocalPath: fileNameIconLocalPath, widthPreview: Int(sizePreview.width), heightPreview: Int(sizePreview.height), sizeIcon: NCGlobal.shared.sizeIcon, account: account.account, options: options) - image = imageIcon + let result = await NCNetworking.shared.downloadPreview(fileId: file.fileId, + account: activeTableAccount.account, + options: options) + if result.error == .success, let data = result.responseData?.data { + utility.createImageFileFrom(data: data, ocId: file.ocId, etag: file.etag) + image = utility.getImage(ocId: file.ocId, etag: file.etag, ext: NCGlobal.shared.previewExt256) + } } if image == nil { - image = utility.loadImage(named: file.iconName, useTypeIconFile: true) + image = utility.loadImage(named: file.iconName, useTypeIconFile: true, account: file.account) useTypeIconFile = true } @@ -240,13 +242,13 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi if datas.count == filesItems { break} } - let alias = (account.alias.isEmpty) ? "" : (" (" + account.alias + ")") - let footerText = "Files " + NSLocalizedString("_of_", comment: "") + " " + account.displayName + alias + let alias = (activeTableAccount.alias.isEmpty) ? "" : (" (" + activeTableAccount.alias + ")") + let footerText = "Files " + NSLocalizedString("_of_", comment: "") + " " + activeTableAccount.displayName + alias if error != .success { - completion(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: account.userId, url: account.urlBase, tile: title, footerImage: "xmark.icloud", footerText: error.errorDescription)) + completion(FilesDataEntry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: activeTableAccount.userId, url: activeTableAccount.urlBase, account: activeTableAccount.account, tile: title, footerImage: "xmark.icloud", footerText: error.errorDescription)) } else { - completion(FilesDataEntry(date: Date(), datas: datas, isPlaceholder: false, isEmpty: datas.isEmpty, userId: account.userId, url: account.urlBase, tile: title, footerImage: "checkmark.icloud", footerText: footerText)) + completion(FilesDataEntry(date: Date(), datas: datas, isPlaceholder: false, isEmpty: datas.isEmpty, userId: activeTableAccount.userId, url: activeTableAccount.urlBase, account: activeTableAccount.account, tile: title, footerImage: "checkmark.icloud", footerText: footerText)) } } } diff --git a/Widget/Files/FilesWidgetProvider.swift b/Widget/Files/FilesWidgetProvider.swift index 9935708d7b..d49e360709 100644 --- a/Widget/Files/FilesWidgetProvider.swift +++ b/Widget/Files/FilesWidgetProvider.swift @@ -33,8 +33,8 @@ struct FilesWidgetProvider: IntentTimelineProvider { func placeholder(in context: Context) -> Entry { let filesItems = getFilesItems(displaySize: context.displaySize) let datasPlaceholder = Array(filesDatasTest[0...filesItems - 1]) - let title = getTitleFilesWidget(account: nil) - return Entry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: "", url: "", tile: title, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " files") + let title = getTitleFilesWidget(tableAccount: nil) + return Entry(date: Date(), datas: datasPlaceholder, isPlaceholder: true, isEmpty: false, userId: "", url: "", account: "", tile: title, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " files") } func getSnapshot(for configuration: AccountIntent, in context: Context, completion: @escaping (Entry) -> Void) { diff --git a/Widget/Files/FilesWidgetView.swift b/Widget/Files/FilesWidgetView.swift index 7dd6d12fce..bb9e015ddf 100644 --- a/Widget/Files/FilesWidgetView.swift +++ b/Widget/Files/FilesWidgetView.swift @@ -120,9 +120,9 @@ struct FilesWidgetView: View { Image("addImage") .resizable() .renderingMode(.template) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding(11) - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -132,9 +132,9 @@ struct FilesWidgetView: View { Image(systemName: "doc.text.viewfinder") .resizable() .renderingMode(.template) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding(11) - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .font(Font.system(.body).weight(.light)) @@ -145,9 +145,9 @@ struct FilesWidgetView: View { Image("note.text") .resizable() .renderingMode(.template) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding(11) - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -157,9 +157,9 @@ struct FilesWidgetView: View { Image("microphone") .resizable() .renderingMode(.template) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding(11) - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -174,12 +174,12 @@ struct FilesWidgetView: View { .scaledToFit() .frame(width: 15, height: 15) .font(Font.system(.body).weight(.light)) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) Text(entry.footerText) .font(.caption2) .lineLimit(1) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) } .padding(.horizontal, 15.0) .frame(maxWidth: geo.size.width, maxHeight: geo.size.height - 2, alignment: .bottomTrailing) @@ -192,7 +192,7 @@ struct FilesWidgetView: View { struct FilesWidget_Previews: PreviewProvider { static var previews: some View { let datas = Array(filesDatasTest[0...4]) - let entry = FilesDataEntry(date: Date(), datas: datas, isPlaceholder: false, isEmpty: true, userId: "", url: "", tile: "Good afternoon, Marino Faggiana", footerImage: "checkmark.icloud", footerText: "Nextcloud files") + let entry = FilesDataEntry(date: Date(), datas: datas, isPlaceholder: false, isEmpty: true, userId: "", url: "", account: "", tile: "Good afternoon, Marino Faggiana", footerImage: "checkmark.icloud", footerText: "Nextcloud files") FilesWidgetView(entry: entry).previewContext(WidgetPreviewContext(family: .systemLarge)) } } diff --git a/Widget/Lockscreen/LockscreenData.swift b/Widget/Lockscreen/LockscreenData.swift index 0fe7b8476e..42356fb593 100644 --- a/Widget/Lockscreen/LockscreenData.swift +++ b/Widget/Lockscreen/LockscreenData.swift @@ -38,7 +38,7 @@ struct LockscreenData: TimelineEntry { func getLockscreenDataEntry(configuration: AccountIntent?, isPreview: Bool, family: WidgetFamily, completion: @escaping (_ entry: LockscreenData) -> Void) { let utilityFileSystem = NCUtilityFileSystem() - var account: tableAccount? + var activeTableAccount: tableAccount? var quotaRelative: Float = 0 if isPreview { @@ -47,38 +47,37 @@ func getLockscreenDataEntry(configuration: AccountIntent?, isPreview: Bool, fami let accountIdentifier: String = configuration?.accounts?.identifier ?? "active" if accountIdentifier == "active" { - account = NCManageDatabase.shared.getActiveAccount() + activeTableAccount = NCManageDatabase.shared.getActiveTableAccount() } else { - account = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) + activeTableAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) } - guard let account else { + guard let activeTableAccount, + let capabilities = NCManageDatabase.shared.setCapabilities(account: activeTableAccount.account) else { return completion(LockscreenData(date: Date(), isPlaceholder: true, activity: "", link: URL(string: "https://")!, quotaRelative: 0, quotaUsed: "", quotaTotal: "", error: false)) } - // Capabilities - NCManageDatabase.shared.setCapabilities(account: account.account) - - if NCGlobal.shared.capabilityServerVersionMajor < NCGlobal.shared.nextcloudVersion25 { + if capabilities.capabilityServerVersionMajor < NCGlobal.shared.nextcloudVersion25 { completion(LockscreenData(date: Date(), isPlaceholder: false, activity: NSLocalizedString("_widget_available_nc25_", comment: ""), link: URL(string: "https://")!, quotaRelative: 0, quotaUsed: "", quotaTotal: "", error: true)) } // NETWORKING - let password = NCKeychain().getPassword(account: account.account) - NextcloudKit.shared.setup( - account: account.account, - user: account.user, - userId: account.userId, - password: password, - urlBase: account.urlBase, - userAgent: userAgent, - nextcloudVersion: 0, - delegate: NCNetworking.shared) + let password = NCKeychain().getPassword(account: activeTableAccount.account) + + NextcloudKit.shared.setup(delegate: NCNetworking.shared) + NextcloudKit.shared.appendSession(account: activeTableAccount.account, + urlBase: activeTableAccount.urlBase, + user: activeTableAccount.user, + userId: activeTableAccount.userId, + password: password, + userAgent: userAgent, + nextcloudVersion: capabilities.capabilityServerVersionMajor, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) let options = NKRequestOptions(timeout: 90, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) if #available(iOSApplicationExtension 16.0, *) { if family == .accessoryCircular { - NextcloudKit.shared.getUserProfile(account: account.account, options: options) { _, userProfile, _, error in + NextcloudKit.shared.getUserProfile(account: activeTableAccount.account, options: options) { _, userProfile, _, error in if error == .success, let userProfile = userProfile { if userProfile.quotaRelative > 0 { quotaRelative = Float(userProfile.quotaRelative) / 100 @@ -102,7 +101,7 @@ func getLockscreenDataEntry(configuration: AccountIntent?, isPreview: Bool, fami } } } else if family == .accessoryRectangular { - NextcloudKit.shared.getDashboardWidgetsApplication("activity", account: account.account, options: options) { _, results, _, error in + NextcloudKit.shared.getDashboardWidgetsApplication("activity", account: activeTableAccount.account, options: options) { _, results, _, error in var activity: String = NSLocalizedString("_no_data_available_", comment: "") var link = URL(string: "https://")! if error == .success, let result = results?.first { diff --git a/Widget/Toolbar/ToolbarData.swift b/Widget/Toolbar/ToolbarData.swift index 7fd87d29f3..d350c81e0f 100644 --- a/Widget/Toolbar/ToolbarData.swift +++ b/Widget/Toolbar/ToolbarData.swift @@ -29,6 +29,7 @@ struct ToolbarDataEntry: TimelineEntry { let isPlaceholder: Bool let userId: String let url: String + let account: String let footerImage: String let footerText: String } @@ -36,19 +37,21 @@ struct ToolbarDataEntry: TimelineEntry { func getToolbarDataEntry(isPreview: Bool, completion: @escaping (_ entry: ToolbarDataEntry) -> Void) { var userId = "" var url = "" + var account = "" - if let account = NCManageDatabase.shared.getActiveAccount() { - userId = account.userId - url = account.urlBase + if let activeTableAccount = NCManageDatabase.shared.getActiveTableAccount() { + userId = activeTableAccount.userId + url = activeTableAccount.urlBase + account = activeTableAccount.account } if isPreview { - return completion(ToolbarDataEntry(date: Date(), isPlaceholder: true, userId: userId, url: url, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar")) + return completion(ToolbarDataEntry(date: Date(), isPlaceholder: true, userId: userId, url: url, account: account, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar")) } - if NCManageDatabase.shared.getActiveAccount() == nil { - return completion(ToolbarDataEntry(date: Date(), isPlaceholder: true, userId: userId, url: url, footerImage: "xmark.icloud", footerText: NSLocalizedString("_no_active_account_", value: "No account found", comment: ""))) + if NCManageDatabase.shared.getActiveTableAccount() == nil { + return completion(ToolbarDataEntry(date: Date(), isPlaceholder: true, userId: userId, url: url, account: account, footerImage: "xmark.icloud", footerText: NSLocalizedString("_no_active_account_", value: "No account found", comment: ""))) } - completion(ToolbarDataEntry(date: Date(), isPlaceholder: false, userId: userId, url: url, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar")) + completion(ToolbarDataEntry(date: Date(), isPlaceholder: false, userId: userId, url: url, account: account, footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar")) } diff --git a/Widget/Toolbar/ToolbarWidgetProvider.swift b/Widget/Toolbar/ToolbarWidgetProvider.swift index b1842d81d3..0c72967f4e 100644 --- a/Widget/Toolbar/ToolbarWidgetProvider.swift +++ b/Widget/Toolbar/ToolbarWidgetProvider.swift @@ -29,7 +29,7 @@ struct ToolbarWidgetProvider: TimelineProvider { typealias Entry = ToolbarDataEntry func placeholder(in context: Context) -> Entry { - return Entry(date: Date(), isPlaceholder: true, userId: "", url: "", footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar") + return Entry(date: Date(), isPlaceholder: true, userId: "", url: "", account: "", footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar") } func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) { diff --git a/Widget/Toolbar/ToolbarWidgetView.swift b/Widget/Toolbar/ToolbarWidgetView.swift index ea1bfb363e..b16a332b50 100644 --- a/Widget/Toolbar/ToolbarWidgetView.swift +++ b/Widget/Toolbar/ToolbarWidgetView.swift @@ -49,9 +49,9 @@ struct ToolbarWidgetView: View { Image("addImage") .resizable() .renderingMode(.template) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding() - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -62,9 +62,9 @@ struct ToolbarWidgetView: View { .resizable() .renderingMode(.template) .font(Font.system(.body).weight(.light)) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding() - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -74,9 +74,9 @@ struct ToolbarWidgetView: View { Image("note.text") .resizable() .renderingMode(.template) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding() - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -85,9 +85,9 @@ struct ToolbarWidgetView: View { Link(destination: entry.isPlaceholder ? linkNoAction : linkActionVoiceMemo, label: { Image("microphone") .resizable() - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandText)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getText(account: entry.account))) .padding() - .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .background(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) .clipShape(Circle()) .scaledToFit() .frame(width: geo.size.width / 4, height: sizeButton) @@ -102,12 +102,12 @@ struct ToolbarWidgetView: View { .font(Font.system(.body).weight(.light)) .scaledToFit() .frame(width: 15, height: 15) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) Text(entry.footerText) .font(.caption2) .padding(.trailing, 13.0) - .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.brandElement)) + .foregroundColor(entry.isPlaceholder ? Color(.systemGray4) : Color(NCBrandColor.shared.getElement(account: entry.account))) } .frame(maxWidth: geo.size.width - 5, maxHeight: geo.size.height - 2, alignment: .bottomTrailing) } @@ -118,7 +118,7 @@ struct ToolbarWidgetView: View { struct ToolbarWidget_Previews: PreviewProvider { static var previews: some View { - let entry = ToolbarDataEntry(date: Date(), isPlaceholder: false, userId: "", url: "", footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar") + let entry = ToolbarDataEntry(date: Date(), isPlaceholder: false, userId: "", url: "", account: "", footerImage: "checkmark.icloud", footerText: NCBrandOptions.shared.brand + " toolbar") ToolbarWidgetView(entry: entry).previewContext(WidgetPreviewContext(family: .systemMedium)) } } diff --git a/WidgetDashboardIntentHandler/IntentHandler.swift b/WidgetDashboardIntentHandler/IntentHandler.swift index 03d8e669a2..b23d945d9d 100644 --- a/WidgetDashboardIntentHandler/IntentHandler.swift +++ b/WidgetDashboardIntentHandler/IntentHandler.swift @@ -30,7 +30,7 @@ class IntentHandler: INExtension, DashboardIntentHandling, AccountIntentHandling func provideAccountsOptionsCollection(for intent: AccountIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { var accounts: [Accounts] = [] - let results = NCManageDatabase.shared.getAllAccount() + let results = NCManageDatabase.shared.getAllTableAccount() accounts.append(Accounts(identifier: "active", display: "Active account")) @@ -49,7 +49,7 @@ class IntentHandler: INExtension, DashboardIntentHandling, AccountIntentHandling } func defaultAccounts(for intent: AccountIntent) -> Accounts? { - if NCManageDatabase.shared.getActiveAccount() == nil { + if NCManageDatabase.shared.getActiveTableAccount() == nil { return nil } else { return Accounts(identifier: "active", display: "Active account") @@ -61,20 +61,20 @@ class IntentHandler: INExtension, DashboardIntentHandling, AccountIntentHandling // Application func provideApplicationsOptionsCollection(for intent: DashboardIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { var applications: [Applications] = [] - var account: tableAccount? + var activeTableAccount: tableAccount? let accountIdentifier: String = intent.accounts?.identifier ?? "active" if accountIdentifier == "active" { - account = NCManageDatabase.shared.getActiveAccount() + activeTableAccount = NCManageDatabase.shared.getActiveTableAccount() } else { - account = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) + activeTableAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", accountIdentifier)) } - guard let account = account else { + guard let activeTableAccount else { return completion(nil, nil) } - let results = NCManageDatabase.shared.getDashboardWidgetApplications(account: account.account) + let results = NCManageDatabase.shared.getDashboardWidgetApplications(account: activeTableAccount.account) for result in results { let application = Applications(identifier: result.id, display: result.title) applications.append(application) @@ -84,7 +84,7 @@ class IntentHandler: INExtension, DashboardIntentHandling, AccountIntentHandling } func defaultApplications(for intent: DashboardIntent) -> Applications? { - guard let account = NCManageDatabase.shared.getActiveAccount() else { + guard let account = NCManageDatabase.shared.getActiveTableAccount() else { return nil } if let result = NCManageDatabase.shared.getDashboardWidgetApplications(account: account.account).first { @@ -96,7 +96,7 @@ class IntentHandler: INExtension, DashboardIntentHandling, AccountIntentHandling // Account func provideAccountsOptionsCollection(for intent: DashboardIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { var accounts: [Accounts] = [] - let results = NCManageDatabase.shared.getAllAccount() + let results = NCManageDatabase.shared.getAllTableAccount() accounts.append(Accounts(identifier: "active", display: "Active account")) @@ -115,7 +115,7 @@ class IntentHandler: INExtension, DashboardIntentHandling, AccountIntentHandling } func defaultAccounts(for intent: DashboardIntent) -> Accounts? { - if NCManageDatabase.shared.getActiveAccount() == nil { + if NCManageDatabase.shared.getActiveTableAccount() == nil { return nil } else { return Accounts(identifier: "active", display: "Active account") diff --git a/iOSClient/Account Request/NCAccountRequest.swift b/iOSClient/Account Request/NCAccountRequest.swift index 1f5acc8de6..38a370c0d3 100644 --- a/iOSClient/Account Request/NCAccountRequest.swift +++ b/iOSClient/Account Request/NCAccountRequest.swift @@ -26,7 +26,7 @@ import NextcloudKit public protocol NCAccountRequestDelegate: AnyObject { func accountRequestAddAccount() - func accountRequestChangeAccount(account: String) + func accountRequestChangeAccount(account: String, controller: UIViewController?) } class NCAccountRequest: UIViewController { @@ -35,11 +35,12 @@ class NCAccountRequest: UIViewController { @IBOutlet weak var progressView: UIProgressView! public var accounts: [tableAccount] = [] - public var activeAccount: tableAccount? + public var activeAccount: String? public let heightCell: CGFloat = 60 public var enableTimerProgress: Bool = true public var enableAddAccount: Bool = false public var dismissDidEnterBackground: Bool = false + public var controller: UIViewController? public weak var delegate: NCAccountRequestDelegate? let utility = NCUtility() private var timer: Timer? @@ -134,9 +135,9 @@ extension NCAccountRequest: UITableViewDelegate { delegate?.accountRequestAddAccount() } else { let account = accounts[indexPath.row] - if account.account != activeAccount?.account { + if account.account != activeAccount { dismiss(animated: true) { - self.delegate?.accountRequestChangeAccount(account: account.account) + self.delegate?.accountRequestChangeAccount(account: account.account, controller: self.controller) } } else { dismiss(animated: true) @@ -177,10 +178,7 @@ extension NCAccountRequest: UITableViewDataSource { let account = accounts[indexPath.row] - avatarImage?.image = utility.loadUserImage( - for: account.user, - displayName: account.displayName, - userBaseUrl: account) + avatarImage?.image = utility.loadUserImage(for: account.user, displayName: account.displayName, urlBase: account.urlBase) if account.alias.isEmpty { userLabel?.text = account.user.uppercased() diff --git a/iOSClient/Account Settings/NCAccountSettingsModel.swift b/iOSClient/Account Settings/NCAccountSettingsModel.swift index 4cdc26f904..8eb33b49d8 100644 --- a/iOSClient/Account Settings/NCAccountSettingsModel.swift +++ b/iOSClient/Account Settings/NCAccountSettingsModel.swift @@ -27,7 +27,7 @@ import RealmSwift /// Protocol for know when the Account Settings has dimissed protocol NCAccountSettingsModelDelegate: AnyObject { - func accountSettingsDidDismiss(tableAccount: tableAccount?) + func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) } /// A model that allows the user to configure the account @@ -37,36 +37,34 @@ class NCAccountSettingsModel: ObservableObject, ViewOnAppearHandling { /// Root View Controller var controller: NCMainTabBarController? /// All account - var accounts: [tableAccount] = [] + var tblAccounts: [tableAccount] = [] /// Delegate weak var delegate: NCAccountSettingsModelDelegate? - /// Timer change user - var timerChangeAccount: Timer? /// Token observe tableAccount var notificationToken: NotificationToken? - /// Account now active - @Published var activeAccount: tableAccount? + /// Account now + @Published var tblAccount: tableAccount? /// Index @Published var indexActiveAccount: Int = 0 /// Current alias @Published var alias: String = "" /// Set true for dismiss the view @Published var dismissView = false + /// DB + let database = NCManageDatabase.shared /// Initialization code to set up the ViewModel with the active account init(controller: NCMainTabBarController?, delegate: NCAccountSettingsModelDelegate?) { self.controller = controller self.delegate = delegate if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" { - NCManageDatabase.shared.previewCreateDB() + database.previewCreateDB() } onViewAppear() observeTableAccount() } deinit { - timerChangeAccount?.invalidate() - timerChangeAccount = nil notificationToken?.invalidate() notificationToken = nil } @@ -96,45 +94,44 @@ class NCAccountSettingsModel: ObservableObject, ViewOnAppearHandling { /// Triggered when the view appears. func onViewAppear() { var indexActiveAccount = 0 - let accounts = NCManageDatabase.shared.getAllAccount() - var activeAccount = NCManageDatabase.shared.getActiveAccount() + let tableAccounts = database.getAllTableAccount() var alias = "" - for (index, account) in accounts.enumerated() { + for (index, account) in tableAccounts.enumerated() { if account.active { - activeAccount = account + tblAccount = account indexActiveAccount = index alias = account.alias } } self.indexActiveAccount = indexActiveAccount - self.accounts = accounts - self.activeAccount = activeAccount + self.tblAccounts = tableAccounts + self.tblAccount = tblAccount self.alias = alias } /// Func to get the user display name + alias func getUserName() -> String { - guard let activeAccount else { return "" } + guard let tblAccount else { return "" } if alias.isEmpty { - return activeAccount.displayName + return tblAccount.displayName } else { - return activeAccount.displayName + " (\(alias))" + return tblAccount.displayName + " (\(alias))" } } /// Func to set alias func setAlias(_ value: String) { - guard let activeAccount else { return } - NCManageDatabase.shared.setAccountAlias(activeAccount.account, alias: alias) + guard let tblAccount else { return } + database.setAccountAlias(tblAccount.account, alias: alias) } /// Function to update the user data func getUserStatus() -> (statusImage: UIImage?, statusMessage: String, descriptionMessage: String) { - guard let activeAccount else { return (UIImage(), "", "") } - if NCGlobal.shared.capabilityUserStatusEnabled, - let tableAccount = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", activeAccount.account)) { + guard let tblAccount else { return (UIImage(), "", "") } + if NCCapabilities.shared.getCapabilities(account: tblAccount.account).capabilityUserStatusEnabled, + let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", tblAccount.account)) { return NCUtility().getUserStatus(userIcon: tableAccount.userStatusIcon, userStatus: tableAccount.userStatusStatus, userMessage: tableAccount.userStatusMessage) } return (nil, "", "") @@ -142,17 +139,18 @@ class NCAccountSettingsModel: ObservableObject, ViewOnAppearHandling { /// Is the user an Admin func isAdminGroup() -> Bool { - guard let activeAccount else { return false } - let groups = NCManageDatabase.shared.getAccountGroups(account: activeAccount.account) + guard let tblAccount else { return false } + let groups = database.getAccountGroups(account: tblAccount.account) return groups.contains(NCGlobal.shared.groupAdmin) } /// Function to know the height of "account" data func getTableViewHeight() -> CGFloat { - guard let activeAccount else { return 0 } - var height: CGFloat = NCGlobal.shared.capabilityUserStatusEnabled ? 190 : 220 - if NCGlobal.shared.capabilityUserStatusEnabled, - let tableAccount = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", activeAccount.account)) { + guard let tblAccount else { return 0 } + let capabilities = NCCapabilities.shared.getCapabilities(account: tblAccount.account) + var height: CGFloat = capabilities.capabilityUserStatusEnabled ? 190 : 220 + if capabilities.capabilityUserStatusEnabled, + let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", tblAccount.account)) { if !tableAccount.email.isEmpty { height += 30 } if !tableAccount.phone.isEmpty { height += 30 } if !tableAccount.address.isEmpty { height += 30 } @@ -163,33 +161,23 @@ class NCAccountSettingsModel: ObservableObject, ViewOnAppearHandling { /// Function to change account after 1.5 sec of change func setAccount(account: String) { - if let tableAccount = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", account)), self.activeAccount?.account != tableAccount.account { - self.activeAccount = tableAccount + if let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", account)) { + self.tblAccount = tableAccount self.alias = tableAccount.alias - /// Change active account - timerChangeAccount?.invalidate() - timerChangeAccount = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(changeAccount), userInfo: nil, repeats: false) - - } - } - - @objc func changeAccount() { - if let activeAccount { - self.appDelegate.changeAccount(activeAccount.account, userProfile: nil) { } } } /// Function to delete the current account func deleteAccount() { - if let activeAccount { - appDelegate.deleteAccount(activeAccount.account) - if let account = NCManageDatabase.shared.getAllAccount().first?.account { - appDelegate.changeAccount(account, userProfile: nil) { + if let tblAccount { + NCAccount().deleteAccount(tblAccount.account) + if let account = database.getAllTableAccount().first?.account { + NCAccount().changeAccount(account, userProfile: nil, controller: self.controller) { onViewAppear() } } else { dismissView = true - appDelegate.openLogin(selector: NCGlobal.shared.introLogin, openLoginWeb: false) + appDelegate.openLogin(selector: NCGlobal.shared.introLogin) } } } diff --git a/iOSClient/Account Settings/NCAccountSettingsView.swift b/iOSClient/Account Settings/NCAccountSettingsView.swift index 74480985df..297cfcd3ec 100644 --- a/iOSClient/Account Settings/NCAccountSettingsView.swift +++ b/iOSClient/Account Settings/NCAccountSettingsView.swift @@ -41,9 +41,9 @@ struct NCAccountSettingsView: View { Form { Section(content: { TabView(selection: $model.indexActiveAccount) { - ForEach(0.. - + - + @@ -20,28 +20,28 @@ - - + + - + - - - + + + - + diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 6769b26af9..665e9e947f 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -25,6 +25,7 @@ import UIKit import SwiftRichString import NextcloudKit +import SVGKit class NCActivity: UIViewController, NCSharePagingContent { @IBOutlet weak var viewContainerConstraint: NSLayoutConstraint! @@ -36,9 +37,9 @@ class NCActivity: UIViewController, NCSharePagingContent { var metadata: tableMetadata? var showComments: Bool = false - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() + let database = NCManageDatabase.shared var allItems: [DateCompareable] = [] var sectionDates: [Date] = [] var dataSourceTask: URLSessionTask? @@ -46,6 +47,7 @@ class NCActivity: UIViewController, NCSharePagingContent { var insets = UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0) var didSelectItemEnable: Bool = true var objectType: String? + var account: String = "" var isFetchingActivity = false var hasActivityToLoad = true { @@ -53,6 +55,14 @@ class NCActivity: UIViewController, NCSharePagingContent { } var dateAutomaticFetch: Date? + var session: NCSession.Session { + if account.isEmpty { + NCSession.shared.getSession(controller: tabBarController) + } else { + NCSession.shared.getSession(account: account) + } + } + // MARK: - View Life Cycle override func viewDidLoad() { @@ -74,15 +84,12 @@ class NCActivity: UIViewController, NCSharePagingContent { func setupComments() { // Display Name & Quota - guard let activeAccount = NCManageDatabase.shared.getActiveAccount(), height > 0 else { - return - } - + guard let metadata else { return } tableView.register(UINib(nibName: "NCShareCommentsCell", bundle: nil), forCellReuseIdentifier: "cell") commentView = Bundle.main.loadNibNamed("NCActivityCommentView", owner: self, options: nil)?.first as? NCActivityCommentView - commentView?.setup(urlBase: appDelegate, account: activeAccount) { newComment in + commentView?.setup(account: metadata.account) { newComment in guard let newComment = newComment, !newComment.isEmpty, let metadata = self.metadata else { return } - NextcloudKit.shared.putComments(fileId: metadata.fileId, message: newComment, account: self.appDelegate.account) { _, error in + NextcloudKit.shared.putComments(fileId: metadata.fileId, message: newComment, account: metadata.account) { _, _, error in if error == .success { self.commentView?.newCommentField.text?.removeAll() self.loadComments() @@ -145,7 +152,7 @@ extension NCActivity: UITableViewDelegate { } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 80 + return 80.0 } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { @@ -209,34 +216,49 @@ extension NCActivity: UITableViewDataSource { } func makeCommentCell(_ comment: tableComments, for indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? NCShareCommentsCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? NCShareCommentsCell, + let metadata else { return UITableViewCell() } cell.indexPath = indexPath cell.tableComments = comment cell.delegate = self - cell.sizeToFit() - // Image - let fileName = appDelegate.userBaseUrl + "-" + comment.actorId + ".png" - NCNetworking.shared.downloadAvatar(user: comment.actorId, dispalyName: comment.actorDisplayName, fileName: fileName, cell: cell, view: tableView) + // Avatar + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: comment.actorId) + let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) + + if results.image == nil { + cell.fileAvatarImageView?.image = utility.loadUserImage(for: comment.actorId, displayName: comment.actorDisplayName, urlBase: NCSession.shared.getSession(account: account).urlBase) + } else { + cell.fileAvatarImageView?.image = results.image + } + + if let tableAvatar = results.tableAvatar, + !tableAvatar.loaded, + NCNetworking.shared.downloadAvatarQueue.operations.filter({ ($0 as? NCOperationDownloadAvatar)?.fileName == fileName }).isEmpty { + NCNetworking.shared.downloadAvatarQueue.addOperation(NCOperationDownloadAvatar(user: comment.actorId, fileName: fileName, account: account, view: tableView)) + } + // Username cell.labelUser.text = comment.actorDisplayName cell.labelUser.textColor = NCBrandColor.shared.textColor // Date cell.labelDate.text = utility.dateDiff(comment.creationDateTime as Date) - cell.labelDate.textColor = .systemGray4 + cell.labelDate.textColor = .lightGray // Message cell.labelMessage.text = comment.message cell.labelMessage.textColor = NCBrandColor.shared.textColor // Button Menu - if comment.actorId == appDelegate.userId { + if comment.actorId == metadata.userId { cell.buttonMenu.isHidden = false } else { cell.buttonMenu.isHidden = true } + cell.sizeToFit() + return cell } @@ -247,6 +269,7 @@ extension NCActivity: UITableViewDataSource { var orderKeysId: [String] = [] cell.idActivity = activity.idActivity + cell.account = activity.account cell.indexPath = indexPath cell.avatar.image = nil cell.avatar.isHidden = true @@ -256,18 +279,21 @@ extension NCActivity: UITableViewDataSource { // icon if !activity.icon.isEmpty { + activity.icon = activity.icon.replacingOccurrences(of: ".png", with: ".svg") let fileNameIcon = (activity.icon as NSString).lastPathComponent let fileNameLocalPath = utilityFileSystem.directoryUserData + "/" + fileNameIcon if FileManager.default.fileExists(atPath: fileNameLocalPath) { - if let image = UIImage(contentsOfFile: fileNameLocalPath) { + let image = fileNameIcon.contains(".svg") ? SVGKImage(contentsOfFile: fileNameLocalPath)?.uiImage : UIImage(contentsOfFile: fileNameLocalPath) + + if let image { cell.icon.image = image.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) } } else { - NextcloudKit.shared.downloadContent(serverUrl: activity.icon, account: appDelegate.account) { _, data, error in - if error == .success { + NextcloudKit.shared.downloadContent(serverUrl: activity.icon, account: activity.account) { _, responseData, error in + if error == .success, let data = responseData?.data { do { - try data!.write(to: NSURL(fileURLWithPath: fileNameLocalPath) as URL, options: .atomic) + try data.write(to: NSURL(fileURLWithPath: fileNameLocalPath) as URL, options: .atomic) self.tableView.reloadData() } catch { return } } @@ -276,12 +302,24 @@ extension NCActivity: UITableViewDataSource { } // avatar - if !activity.user.isEmpty && activity.user != appDelegate.userId { + if !activity.user.isEmpty && activity.user != session.userId { cell.avatar.isHidden = false cell.fileUser = activity.user - let fileName = appDelegate.userBaseUrl + "-" + activity.user + ".png" - NCNetworking.shared.downloadAvatar(user: activity.user, dispalyName: nil, fileName: fileName, cell: cell, view: tableView) cell.subjectLeadingConstraint.constant = 15 + + let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: activity.user) + let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) + + if results.image == nil { + cell.fileAvatarImageView?.image = utility.loadUserImage(for: activity.user, displayName: nil, urlBase: session.urlBase) + } else { + cell.fileAvatarImageView?.image = results.image + } + + if !(results.tableAvatar?.loaded ?? false), + NCNetworking.shared.downloadAvatarQueue.operations.filter({ ($0 as? NCOperationDownloadAvatar)?.fileName == fileName }).isEmpty { + NCNetworking.shared.downloadAvatarQueue.addOperation(NCOperationDownloadAvatar(user: activity.user, fileName: fileName, account: session.account, view: tableView)) + } } else { cell.subjectLeadingConstraint.constant = -30 } @@ -300,7 +338,7 @@ extension NCActivity: UITableViewDataSource { } for key in keys { - if let result = NCManageDatabase.shared.getActivitySubjectRich(account: appDelegate.account, idActivity: activity.idActivity, key: key) { + if let result = database.getActivitySubjectRich(account: session.account, idActivity: activity.idActivity, key: key) { orderKeysId.append(result.id) subject = subject.replacingOccurrences(of: "{\(key)}", with: "" + result.name + "") } @@ -344,7 +382,7 @@ extension NCActivity { var bottom: CGFloat = 0 if let mainTabBar = self.tabBarController?.tabBar as? NCMainTabBar { - bottom = -mainTabBar.getHeight() + bottom = -mainTabBar.getHeight() } NCActivityIndicator.shared.start(backgroundView: self.view, bottom: bottom - 35, style: .medium) @@ -371,13 +409,13 @@ extension NCActivity { func loadDataSource() { var newItems = [DateCompareable]() - if showComments, let metadata = metadata, let account = NCManageDatabase.shared.getActiveAccount() { - let comments = NCManageDatabase.shared.getComments(account: account.account, objectId: metadata.fileId) + if showComments, let metadata { + let comments = database.getComments(account: metadata.account, objectId: metadata.fileId) newItems += comments } - let activities = NCManageDatabase.shared.getActivity( - predicate: NSPredicate(format: "account == %@", appDelegate.account), + let activities = database.getActivity( + predicate: NSPredicate(format: "account == %@", session.account), filterFileId: metadata?.fileId) newItems += activities.filter @@ -395,7 +433,7 @@ extension NCActivity { NextcloudKit.shared.getComments(fileId: metadata.fileId, account: metadata.account) { _, comments, _, error in if error == .success, let comments = comments { - NCManageDatabase.shared.addComments(comments, account: metadata.account, objectId: metadata.fileId) + self.database.addComments(comments, account: metadata.account, objectId: metadata.fileId) } else if error.errorCode != NCGlobal.shared.errorResourceNotFound { NCContentPresenter().showError(error: error) } @@ -410,7 +448,7 @@ extension NCActivity { /// Check if most recent activivities are loaded, if not trigger reload func checkRecentActivity(disptachGroup: DispatchGroup) { - guard let result = NCManageDatabase.shared.getLatestActivityId(account: appDelegate.account), metadata == nil, hasActivityToLoad else { + guard let result = database.getLatestActivityId(account: session.account), metadata == nil, hasActivityToLoad else { return self.loadActivity(idActivity: 0, disptachGroup: disptachGroup) } let resultActivityId = max(result.activityFirstKnown, result.activityLastGiven) @@ -423,14 +461,14 @@ extension NCActivity { objectId: nil, objectType: objectType, previews: true, - account: appDelegate.account) { task in + account: session.account) { task in self.dataSourceTask = task } completion: { account, _, activityFirstKnown, activityLastGiven, _, error in defer { disptachGroup.leave() } let largestActivityId = max(activityFirstKnown, activityLastGiven) guard error == .success, - account == self.appDelegate.account, + account == self.session.account, largestActivityId > resultActivityId else { self.hasActivityToLoad = error.errorCode == NCGlobal.shared.errorNotModified ? false : self.hasActivityToLoad @@ -452,26 +490,26 @@ extension NCActivity { objectId: metadata?.fileId, objectType: objectType, previews: true, - account: appDelegate.account) { task in + account: session.account) { task in self.dataSourceTask = task } completion: { account, activities, activityFirstKnown, activityLastGiven, _, error in defer { disptachGroup.leave() } guard error == .success, - account == self.appDelegate.account, + account == self.session.account, !activities.isEmpty else { self.hasActivityToLoad = error.errorCode == NCGlobal.shared.errorNotModified ? false : self.hasActivityToLoad return } - NCManageDatabase.shared.addActivity(activities, account: account) + self.database.addActivity(activities, account: account) // update most recently loaded activity only when all activities are loaded (not filtered) let largestActivityId = max(activityFirstKnown, activityLastGiven) - if let result = NCManageDatabase.shared.getLatestActivityId(account: self.appDelegate.account) { + if let result = self.database.getLatestActivityId(account: self.session.account) { resultActivityId = max(result.activityFirstKnown, result.activityLastGiven) } if self.metadata == nil, largestActivityId > resultActivityId { - NCManageDatabase.shared.updateLatestActivityId(activityFirstKnown: activityFirstKnown, activityLastGiven: activityLastGiven, account: account) + self.database.updateLatestActivityId(activityFirstKnown: activityFirstKnown, activityLastGiven: activityLastGiven, account: account) } } } @@ -482,7 +520,7 @@ extension NCActivity: NCShareCommentsCellDelegate { guard let tableComment = tableComment else { return } - self.showProfileMenu(userId: tableComment.actorId) + self.showProfileMenu(userId: tableComment.actorId, session: session) } func tapMenu(with tableComments: tableComments?, sender: Any) { @@ -509,7 +547,7 @@ extension NCActivity: NCShareCommentsCellDelegate { alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in guard let message = alert.textFields?.first?.text, !message.isEmpty else { return } - NextcloudKit.shared.updateComments(fileId: metadata.fileId, messageId: tableComments.messageId, message: message, account: self.appDelegate.account) { _, error in + NextcloudKit.shared.updateComments(fileId: metadata.fileId, messageId: tableComments.messageId, message: message, account: metadata.account) { _, _, error in if error == .success { self.loadComments() } else { @@ -531,7 +569,7 @@ extension NCActivity: NCShareCommentsCellDelegate { action: { _ in guard let metadata = self.metadata, let tableComments = tableComments else { return } - NextcloudKit.shared.deleteComments(fileId: metadata.fileId, messageId: tableComments.messageId, account: metadata.account) { _, error in + NextcloudKit.shared.deleteComments(fileId: metadata.fileId, messageId: tableComments.messageId, account: metadata.account) { _, _, error in if error == .success { self.loadComments() } else { diff --git a/iOSClient/Activity/NCActivityCommentView.swift b/iOSClient/Activity/NCActivityCommentView.swift index 1792676d25..8ad580621d 100644 --- a/iOSClient/Activity/NCActivityCommentView.swift +++ b/iOSClient/Activity/NCActivityCommentView.swift @@ -29,12 +29,13 @@ class NCActivityCommentView: UIView, UITextFieldDelegate { var completionHandler: ((String?) -> Void)? - func setup(urlBase: NCUserBaseUrl, account: tableAccount, completionHandler: @escaping (String?) -> Void) { + func setup(account: String, completionHandler: @escaping (String?) -> Void) { + let session = NCSession.shared.getSession(account: account) self.completionHandler = completionHandler newCommentField.placeholder = NSLocalizedString("_new_comment_", comment: "") newCommentField.delegate = self - let fileName = urlBase.userBaseUrl + "-" + urlBase.user + ".png" + let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: session.user) let fileNameLocalPath = NCUtilityFileSystem().directoryUserData + "/" + fileName if let image = UIImage(contentsOfFile: fileNameLocalPath) { imageItem.image = image diff --git a/iOSClient/Activity/NCActivityTableViewCell.swift b/iOSClient/Activity/NCActivityTableViewCell.swift index 14124d355c..64b002dab6 100644 --- a/iOSClient/Activity/NCActivityTableViewCell.swift +++ b/iOSClient/Activity/NCActivityTableViewCell.swift @@ -22,10 +22,11 @@ // import Foundation +import UIKit import NextcloudKit import FloatingPanel -import JGProgressHUD import Queuer +import Alamofire class NCActivityCollectionViewCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! @@ -44,14 +45,15 @@ class NCActivityTableViewCell: UITableViewCell, NCCellProtocol { @IBOutlet weak var subject: UILabel! @IBOutlet weak var subjectLeadingConstraint: NSLayoutConstraint! - private let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! private var user: String = "" private var index = IndexPath() var idActivity: Int = 0 var activityPreviews: [tableActivityPreview] = [] var didSelectItemEnable: Bool = true - var viewController = UIViewController() + var viewController = NCActivity() + var account: String! + let utility = NCUtility() var indexPath: IndexPath { @@ -75,7 +77,7 @@ class NCActivityTableViewCell: UITableViewCell, NCCellProtocol { @objc func tapAvatarImage() { guard let fileUser = fileUser else { return } - viewController.showProfileMenu(userId: fileUser) + viewController.showProfileMenu(userId: fileUser, session: NCSession.shared.getSession(account: account)) } } @@ -100,9 +102,9 @@ extension NCActivityTableViewCell: UICollectionViewDelegate { } if (responder as? UIViewController)!.navigationController != nil { if let viewController = UIStoryboard(name: "NCTrash", bundle: nil).instantiateInitialViewController() as? NCTrash { - if let result = NCManageDatabase.shared.getTrashItem(fileId: String(activityPreview.fileId), account: activityPreview.account) { - viewController.blinkFileId = result.fileId - viewController.filePath = result.filePath + if let resultTableTrash = NCManageDatabase.shared.getResultTrashItem(fileId: String(activityPreview.fileId), account: activityPreview.account) { + viewController.blinkFileId = resultTableTrash.fileId + viewController.filePath = resultTableTrash.filePath (responder as? UIViewController)!.navigationController?.pushViewController(viewController, animated: true) } else { let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_trash_file_not_found_") @@ -117,7 +119,7 @@ extension NCActivityTableViewCell: UICollectionViewDelegate { guard let activitySubjectRich = NCManageDatabase.shared.getActivitySubjectRich(account: activityPreview.account, idActivity: activityPreview.idActivity, id: String(activityPreview.fileId)) else { return } - NCActionCenter.shared.viewerFile(account: appDelegate.account, fileId: activitySubjectRich.id, viewController: viewController) + NCActionCenter.shared.viewerFile(account: account, fileId: activitySubjectRich.id, viewController: viewController) } } } @@ -133,9 +135,7 @@ extension NCActivityTableViewCell: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell: NCActivityCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as? NCActivityCollectionViewCell else { - return UICollectionViewCell() - } + let cell: NCActivityCollectionViewCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as? NCActivityCollectionViewCell)! cell.imageView.image = nil cell.indexPath = indexPath @@ -147,22 +147,22 @@ extension NCActivityTableViewCell: UICollectionViewDataSource { if activityPreview.view == "trashbin" { let source = activityPreview.source - utility.convertSVGtoPNGWriteToUserData(svgUrlString: source, width: 100, rewrite: false, account: appDelegate.account, id: idActivity) { imageNamePath, id in + utility.convertSVGtoPNGWriteToUserData(svgUrlString: source, width: 100, rewrite: false, account: account, id: idActivity) { imageNamePath, id in if let imageNamePath = imageNamePath, id == self.idActivity, let image = UIImage(contentsOfFile: imageNamePath) { cell.imageView.image = image } else { - cell.imageView.image = NCImageCache.images.file + cell.imageView.image = NCImageCache.shared.getImageFile() } } } else { if activityPreview.isMimeTypeIcon { let source = activityPreview.source - utility.convertSVGtoPNGWriteToUserData(svgUrlString: source, width: 150, rewrite: false, account: appDelegate.account, id: idActivity) { imageNamePath, id in + utility.convertSVGtoPNGWriteToUserData(svgUrlString: source, width: 150, rewrite: false, account: account, id: idActivity) { imageNamePath, id in if let imageNamePath = imageNamePath, id == self.idActivity, let image = UIImage(contentsOfFile: imageNamePath) { cell.imageView.image = image } else { - cell.imageView.image = NCImageCache.images.file + cell.imageView.image = NCImageCache.shared.getImageFile() } } } else { @@ -178,7 +178,7 @@ extension NCActivityTableViewCell: UICollectionViewDataSource { cell.fileId = fileId if !FileManager.default.fileExists(atPath: fileNamePath) { if NCNetworking.shared.downloadThumbnailActivityQueue.operations.filter({ ($0 as? NCOperationDownloadThumbnailActivity)?.fileId == fileId }).isEmpty { - NCNetworking.shared.downloadThumbnailActivityQueue.addOperation(NCOperationDownloadThumbnailActivity(fileId: fileId, fileNamePreviewLocalPath: fileNamePath, account: appDelegate.account, cell: cell, collectionView: collectionView)) + NCNetworking.shared.downloadThumbnailActivityQueue.addOperation(NCOperationDownloadThumbnailActivity(fileId: fileId, fileNamePreviewLocalPath: fileNamePath, account: account, collectionView: collectionView)) } } } @@ -203,18 +203,16 @@ extension NCActivityTableViewCell: UICollectionViewDelegateFlowLayout { } } -class NCOperationDownloadThumbnailActivity: ConcurrentOperation { - var cell: NCActivityCollectionViewCell? +class NCOperationDownloadThumbnailActivity: ConcurrentOperation, @unchecked Sendable { var collectionView: UICollectionView? var fileNamePreviewLocalPath: String var fileId: String var account: String - init(fileId: String, fileNamePreviewLocalPath: String, account: String, cell: NCActivityCollectionViewCell?, collectionView: UICollectionView?) { + init(fileId: String, fileNamePreviewLocalPath: String, account: String, collectionView: UICollectionView?) { self.fileNamePreviewLocalPath = fileNamePreviewLocalPath self.fileId = fileId self.account = account - self.cell = cell self.collectionView = collectionView } @@ -222,23 +220,19 @@ class NCOperationDownloadThumbnailActivity: ConcurrentOperation { guard !isCancelled else { return self.finish() } NextcloudKit.shared.downloadPreview(fileId: fileId, - fileNamePreviewLocalPath: fileNamePreviewLocalPath, - account: account, - options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { _, imagePreview, _, _, _, error in - if error == .success, let imagePreview = imagePreview { - DispatchQueue.main.async { - if self.fileId == self.cell?.fileId, let imageView = self.cell?.imageView { - UIView.transition(with: imageView, + account: account) { _, _, _, _, responseData, error in + if error == .success, let data = responseData?.data, let collectionView = self.collectionView { + for case let cell as NCActivityCollectionViewCell in collectionView.visibleCells { + if self.fileId == cell.fileId { + UIView.transition(with: cell.imageView, duration: 0.75, options: .transitionCrossDissolve, - animations: { imageView.image = imagePreview }, + animations: { cell.imageView.image = UIImage(data: data) }, completion: nil) - } else { - self.collectionView?.reloadData() } } } self.finish() } - } + } } diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index fefc6466f7..350f474d3c 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -32,20 +32,11 @@ import EasyTipView import SwiftUI @UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, NCUserBaseUrl { - - var account: String = "" - var urlBase: String = "" - var user: String = "" - var userId: String = "" - var password: String = "" - +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var tipView: EasyTipView? var backgroundSessionCompletionHandler: (() -> Void)? var activeLogin: NCLogin? var activeLoginWeb: NCLoginProvider? - var timerErrorNetworking: Timer? - var timerErrorNetworkingDisabled: Bool = false var taskAutoUploadDate: Date = Date() var isUiTestingEnabled: Bool { return ProcessInfo.processInfo.arguments.contains("UI_TESTING") @@ -58,15 +49,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if isUiTestingEnabled { - deleteAllAccounts() + NCAccount().deleteAllAccounts() } let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() + var levelLog = 0 + let versionNextcloudiOS = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, utility.getVersionApp()) NCSettingsBundleHelper.checkAndExecuteSettings(delay: 0) - let versionNextcloudiOS = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, utility.getVersionApp()) - UserDefaults.standard.register(defaults: ["UserAgent": userAgent]) if !NCKeychain().disableCrashservice, !NCBrandOptions.shared.disable_crash_service { FirebaseApp.configure() @@ -76,14 +67,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD utilityFileSystem.emptyTemporaryDirectory() utilityFileSystem.clearCacheDirectory("com.limit-point.LivePhoto") - // Activated singleton - _ = NCActionCenter.shared - _ = NCNetworking.shared + // Create users colors + NCBrandColor.shared.createUserColors() NextcloudKit.shared.setup(delegate: NCNetworking.shared) - NextcloudKit.shared.setup(userAgent: userAgent) - - var levelLog = 0 NextcloudKit.shared.nkCommonInstance.pathLog = utilityFileSystem.directoryGroup if NCBrandOptions.shared.disable_log { @@ -96,37 +83,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Start session with level \(levelLog) " + versionNextcloudiOS) } - if let activeAccount = NCManageDatabase.shared.getActiveAccount() { - NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Account active \(activeAccount.account)") - - if NCKeychain().getPassword(account: activeAccount.account).isEmpty { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] PASSWORD NOT FOUND for \(activeAccount.account)") - } - - account = activeAccount.account - urlBase = activeAccount.urlBase - user = activeAccount.user - userId = activeAccount.userId - password = NCKeychain().getPassword(account: account) - - NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase) - NCManageDatabase.shared.setCapabilities(account: account) - - NCBrandColor.shared.settingThemingColor(account: activeAccount.account) - DispatchQueue.global().async { - NCImageCache.shared.createMediaCache(account: self.account, withCacheSize: true) - } - } else { - NCKeychain().removeAll() - if let bundleID = Bundle.main.bundleIdentifier { - UserDefaults.standard.removePersistentDomain(forName: bundleID) - } - } - - NCBrandColor.shared.createUserColors() - NCImageCache.shared.createImagesCache() - - // Push Notification & display notification + /// Push Notification & display notification UNUserNotificationCenter.current().getNotificationSettings { settings in self.notificationSettings = settings } @@ -140,7 +97,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD review.showStoreReview() } - // Background task register + /// Background task register BGTaskScheduler.shared.register(forTaskWithIdentifier: NCGlobal.shared.refreshTask, using: nil) { task in self.handleAppRefresh(task) } @@ -148,12 +105,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.handleProcessingTask(task) } - FileNameValidator.shared.setup( - forbiddenFileNames: NCGlobal.shared.capabilityForbiddenFileNames, - forbiddenFileNameBasenames: NCGlobal.shared.capabilityForbiddenFileNameBasenames, - forbiddenFileNameCharacters: NCGlobal.shared.capabilityForbiddenFileNameCharacters, - forbiddenFileNameExtensions: NCGlobal.shared.capabilityForbiddenFileNameExtensions - ) + /// Activation singleton + _ = NCActionCenter.shared + _ = NCNetworkingProcess.shared return true } @@ -235,39 +189,46 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func handleAppRefreshProcessingTask(taskText: String, completion: @escaping () -> Void = {}) { Task { - var itemsAutoUpload = 0 + var numAutoUpload = 0 + guard let account = NCManageDatabase.shared.getActiveTableAccount()?.account else { + return + } NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) start handle") // Test every > 1 min if Date() > self.taskAutoUploadDate.addingTimeInterval(60) { self.taskAutoUploadDate = Date() - itemsAutoUpload = await NCAutoUpload.shared.initAutoUpload() - NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) auto upload with \(itemsAutoUpload) uploads") + numAutoUpload = await NCAutoUpload.shared.initAutoUpload(account: account) + NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) auto upload with \(numAutoUpload) uploads") } else { NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) disabled auto upload") } - let results = await NCNetworkingProcess.shared.start(scene: nil) + let results = await NCNetworkingProcess.shared.refreshProcessingTask() NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) networking process with download: \(results.counterDownloading) upload: \(results.counterUploading)") if taskText == "ProcessingTask", - itemsAutoUpload == 0, + numAutoUpload == 0, results.counterDownloading == 0, results.counterUploading == 0, - let directories = NCManageDatabase.shared.getTablesDirectory(predicate: NSPredicate(format: "account == %@ AND offline == true", self.account), sorted: "offlineDate", ascending: true) { + let directories = NCManageDatabase.shared.getTablesDirectory(predicate: NSPredicate(format: "account == %@ AND offline == true", account), sorted: "offlineDate", ascending: true) { for directory: tableDirectory in directories { // test only 3 time for day (every 8 h.) if let offlineDate = directory.offlineDate, offlineDate.addingTimeInterval(28800) > Date() { NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) skip synchronization for \(directory.serverUrl) in date \(offlineDate)") continue } - let results = await NCNetworking.shared.synchronization(account: self.account, serverUrl: directory.serverUrl, add: false) - NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) end synchronization for \(directory.serverUrl), errorCode: \(results.errorCode), item: \(results.items)") + let results = await NCNetworking.shared.synchronization(account: account, serverUrl: directory.serverUrl, add: false) + NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) end synchronization for \(directory.serverUrl), errorCode: \(results.errorCode), item: \(results.num)") } } - let counter = NCManageDatabase.shared.getResultsMetadatas(predicate: NSPredicate(format: "account == %@ AND (session == %@ || session == %@) AND status != %d", self.account, NCNetworking.shared.sessionDownloadBackground, NCNetworking.shared.sessionUploadBackground, NCGlobal.shared.metadataStatusNormal))?.count ?? 0 + let counter = NCManageDatabase.shared.getResultsMetadatas(predicate: NSPredicate(format: "account == %@ AND (session == %@ || session == %@) AND status != %d", + account, + NCNetworking.shared.sessionDownloadBackground, + NCNetworking.shared.sessionUploadBackground, + NCGlobal.shared.metadataStatusNormal))?.count ?? 0 UIApplication.shared.applicationIconBadgeNumber = counter NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] \(taskText) completion handle") @@ -315,28 +276,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func nextcloudPushNotificationAction(data: [String: AnyObject]) { guard let data = NCApplicationHandle().nextcloudPushNotificationAction(data: data) else { return } - var findAccount: Bool = false + var findAccount: String? if let accountPush = data["account"] as? String { - if accountPush == self.account { - findAccount = true - } else { - let accounts = NCManageDatabase.shared.getAllAccount() - for account in accounts { - if account.account == accountPush { - self.changeAccount(account.account, userProfile: nil) { - findAccount = true + for tableAccount in NCManageDatabase.shared.getAllTableAccount() { + if tableAccount.account == accountPush { + for controller in SceneManager.shared.getControllers() { + if controller.account == accountPush { + NCAccount().changeAccount(tableAccount.account, userProfile: nil, controller: controller) { + findAccount = tableAccount.account + } } } } } - if findAccount, let viewController = UIStoryboard(name: "NCNotification", bundle: nil).instantiateInitialViewController() as? NCNotification { + if let account = findAccount, let viewController = UIStoryboard(name: "NCNotification", bundle: nil).instantiateInitialViewController() as? NCNotification { + viewController.session = NCSession.shared.getSession(account: account) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { let navigationController = UINavigationController(rootViewController: viewController) navigationController.modalPresentationStyle = .fullScreen UIApplication.shared.firstWindow?.rootViewController?.present(navigationController, animated: true) } - } else if !findAccount { + } else { let message = NSLocalizedString("_the_account_", comment: "") + " " + accountPush + " " + NSLocalizedString("_does_not_exist_", comment: "") let alertController = UIAlertController(title: NSLocalizedString("_info_", comment: ""), message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) @@ -347,7 +308,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - Login - func openLogin(selector: Int, openLoginWeb: Bool, windowForRootViewController: UIWindow? = nil) { + func openLogin(selector: Int) { + UIApplication.shared.allSceneSessionDestructionExceptFirst() + func showLoginViewController(_ viewController: UIViewController?) { guard let viewController else { return } let navigationController = NCLoginNavigationController(rootViewController: viewController) @@ -358,20 +321,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD navigationController.navigationBar.barTintColor = NCBrandColor.shared.customer navigationController.navigationBar.isTranslucent = false - if let window = windowForRootViewController { - window.rootViewController = navigationController - window.makeKeyAndVisible() - } else { - UIApplication.shared.allSceneSessionDestructionExceptFirst() - - if let rootVC = UIApplication.shared.firstWindow?.rootViewController { - if let presentedVC = rootVC.presentedViewController, !(presentedVC is NCLoginNavigationController) { - presentedVC.dismiss(animated: false) { - rootVC.present(navigationController, animated: true) - } - } else { - rootVC.present(navigationController, animated: true) + if let controller = UIApplication.shared.firstWindow?.rootViewController { + if let presentedVC = controller.presentedViewController, !(presentedVC is NCLoginNavigationController) { + presentedVC.dismiss(animated: false) { + controller.present(navigationController, animated: true) } + } else { + controller.present(navigationController, animated: true) } } } @@ -379,14 +335,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Nextcloud standard login if selector == NCGlobal.shared.introSignup { if activeLogin?.view.window == nil { - activeLogin = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLogin") as? NCLogin if selector == NCGlobal.shared.introSignup { - activeLogin?.urlBase = NCBrandOptions.shared.linkloginPreferredProviders let web = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginProvider") as? NCLoginProvider web?.urlBase = NCBrandOptions.shared.linkloginPreferredProviders showLoginViewController(web) } else { - activeLogin?.urlBase = self.urlBase + activeLogin = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLogin") as? NCLogin + if let controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController, !controller.account.isEmpty { + let session = NCSession.shared.getSession(account: controller.account) + activeLogin?.urlBase = session.urlBase + } showLoginViewController(activeLogin) } } @@ -399,35 +357,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - // MARK: - Error Networking - - func startTimerErrorNetworking(scene: UIScene) { - timerErrorNetworkingDisabled = false - timerErrorNetworking = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(checkErrorNetworking(_:)), userInfo: nil, repeats: true) - } - - @objc private func checkErrorNetworking(_ notification: NSNotification) { - guard !self.timerErrorNetworkingDisabled, - !account.isEmpty, - NCKeychain().getPassword(account: account).isEmpty else { return } - - let description = String.localizedStringWithFormat(NSLocalizedString("_error_check_remote_user_", comment: "")) - let error = NKError(errorCode: NCKeychain().getPassword(account: account).isEmpty ? NCGlobal.shared.errorUnauthorized997 : NCGlobal.shared.errorInternalServerError, errorDescription: description) - NCContentPresenter().showError(error: error, priority: .max) - - deleteAccount(account) - - let accounts = NCManageDatabase.shared.getAccounts() - - if accounts?.count ?? 0 > 0, let newAccount = accounts?.first { - changeAccount(newAccount, userProfile: nil) { } - } else { - openLogin(selector: NCGlobal.shared.introLogin, openLoginWeb: false) - } - } + // MARK: - func trustCertificateError(host: String) { - guard let currentHost = URL(string: self.urlBase)?.host, + guard let activeTableAccount = NCManageDatabase.shared.getActiveTableAccount(), + let currentHost = URL(string: activeTableAccount.urlBase)?.host, let pushNotificationServerProxyHost = URL(string: NCBrandOptions.shared.pushNotificationServerProxy)?.host, host != pushNotificationServerProxyHost, host == currentHost @@ -459,136 +393,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD UIApplication.shared.firstWindow?.rootViewController?.present(alertController, animated: true) } - // MARK: - Account - - func createAccount(urlBase: String, - user: String, - password: String, - completion: @escaping (_ error: NKError) -> Void) { - var urlBase = urlBase - if urlBase.last == "/" { urlBase = String(urlBase.dropLast()) } - let account: String = "\(user) \(urlBase)" - - NextcloudKit.shared.setup(account: account, user: user, userId: user, password: password, urlBase: urlBase) - NextcloudKit.shared.getUserProfile(account: account) { account, userProfile, _, error in - if error == .success, let userProfile { - NCManageDatabase.shared.deleteAccount(account) - NCManageDatabase.shared.addAccount(account, urlBase: urlBase, user: user, userId: userProfile.userId, password: password) - NCKeychain().setClientCertificate(account: account, p12Data: NCNetworking.shared.p12Data, p12Password: NCNetworking.shared.p12Password) - self.changeAccount(account, userProfile: userProfile) { - completion(error) - } - } else { - NextcloudKit.shared.setup(account: self.account, user: self.user, userId: self.userId, password: self.password, urlBase: self.urlBase) - let alertController = UIAlertController(title: NSLocalizedString("_error_", comment: ""), message: error.errorDescription, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in })) - UIApplication.shared.firstWindow?.rootViewController?.present(alertController, animated: true) - completion(error) - } - } - } - - func changeAccount(_ account: String, - userProfile: NKUserProfile?, - completion: () -> Void) { - guard let tableAccount = NCManageDatabase.shared.setAccountActive(account) else { - return completion() - } - - NCNetworking.shared.cancelAllQueue() - NCNetworking.shared.cancelDataTask() - NCNetworking.shared.cancelDownloadTasks() - NCNetworking.shared.cancelUploadTasks() - - if account != self.account { - DispatchQueue.global().async { - if NCManageDatabase.shared.getAccounts()?.count == 1 { - NCImageCache.shared.createMediaCache(account: account, withCacheSize: true) - } else { - NCImageCache.shared.createMediaCache(account: account, withCacheSize: false) - } - } - } - - self.account = tableAccount.account - self.urlBase = tableAccount.urlBase - self.user = tableAccount.user - self.userId = tableAccount.userId - self.password = NCKeychain().getPassword(account: tableAccount.account) - - NextcloudKit.shared.setup(account: account, user: user, userId: userId, password: password, urlBase: urlBase) - NCManageDatabase.shared.setCapabilities(account: account) - - if let userProfile { - NCManageDatabase.shared.setAccountUserProfile(account: account, userProfile: userProfile) - } - - NCPushNotification.shared.pushNotification() - NCService().startRequestServicesServer(account: self.account, user: self.user, userId: self.userId) - - NCAutoUpload.shared.initAutoUpload(viewController: nil) { items in - NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Initialize Auto upload with \(items) uploads") - } - - FileNameValidator.shared.setup( - forbiddenFileNames: NCGlobal.shared.capabilityForbiddenFileNames, - forbiddenFileNameBasenames: NCGlobal.shared.capabilityForbiddenFileNameBasenames, - forbiddenFileNameCharacters: NCGlobal.shared.capabilityForbiddenFileNameCharacters, - forbiddenFileNameExtensions: NCGlobal.shared.capabilityForbiddenFileNameExtensions - ) - - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeUser) - completion() - } - - func deleteAccount(_ account: String) { - UIApplication.shared.allSceneSessionDestructionExceptFirst() - - if let account = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", account)) { - NCPushNotification.shared.unsubscribingNextcloudServerPushNotification(account: account.account, urlBase: account.urlBase, user: account.user, withSubscribing: false) - } - - let results = NCManageDatabase.shared.getTableLocalFiles(predicate: NSPredicate(format: "account == %@", account), sorted: "ocId", ascending: false) - let utilityFileSystem = NCUtilityFileSystem() - for result in results { - utilityFileSystem.removeFile(atPath: utilityFileSystem.getDirectoryProviderStorageOcId(result.ocId)) - } - NCManageDatabase.shared.clearDatabase(account: account, removeAccount: true) - - NCKeychain().clearAllKeysEndToEnd(account: account) - NCKeychain().clearAllKeysPushNotification(account: account) - NCKeychain().setPassword(account: account, password: nil) - - self.account = "" - self.urlBase = "" - self.user = "" - self.userId = "" - self.password = "" - } - - func deleteAllAccounts() { - let accounts = NCManageDatabase.shared.getAccounts() - accounts?.forEach({ account in - deleteAccount(account) - }) - } - - func updateShareAccounts() -> Error? { - guard let dirGroupApps = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: NCBrandOptions.shared.capabilitiesGroupApps) else { return nil } - let tableAccount = NCManageDatabase.shared.getAllAccount() - var accounts = [NKShareAccounts.DataAccounts]() - - for account in tableAccount { - let name = account.alias.isEmpty ? account.displayName : account.alias - let userBaseUrl = account.user + "-" + (URL(string: account.urlBase)?.host ?? "") - let avatarFileName = userBaseUrl + "-\(account.user).png" - let pathAvatarFileName = NCUtilityFileSystem().directoryUserData + "/" + avatarFileName - let image = UIImage(contentsOfFile: pathAvatarFileName) - accounts.append(NKShareAccounts.DataAccounts(withUrl: account.urlBase, user: account.user, name: name, image: image)) - } - return NKShareAccounts().putShareAccounts(at: dirGroupApps, app: NCGlobal.shared.appScheme, dataAccounts: accounts) - } - // MARK: - Reset Application func resetApplication() { @@ -596,8 +400,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD NCNetworking.shared.cancelAllTask() - URLCache.shared.memoryCapacity = 0 - URLCache.shared.diskCapacity = 0 + URLCache.shared.removeAllCachedResponses() utilityFileSystem.removeGroupDirectoryProviderStorage() utilityFileSystem.removeGroupApplicationSupport() @@ -605,6 +408,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD utilityFileSystem.removeTemporaryDirectory() NCKeychain().removeAll() + NCNetworking.shared.removeAllKeyUserDefaultsData(account: nil) + exit(0) } diff --git a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift index 80e2d69ce8..953c6bbf19 100644 --- a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift +++ b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift @@ -55,7 +55,7 @@ struct NCAssistantCreateNewTask: View { } #Preview { - let model = NCAssistantTask() + let model = NCAssistantTask(controller: nil) return NCAssistantCreateNewTask() .environmentObject(model) diff --git a/iOSClient/Assistant/Models/NCAssistantTask.swift b/iOSClient/Assistant/Models/NCAssistantTask.swift index c0ce427865..ceaf400b52 100644 --- a/iOSClient/Assistant/Models/NCAssistantTask.swift +++ b/iOSClient/Assistant/Models/NCAssistantTask.swift @@ -7,6 +7,7 @@ // import Foundation +import UIKit import NextcloudKit import SwiftUI @@ -17,12 +18,16 @@ class NCAssistantTask: ObservableObject { @Published var selectedTask: NKTextProcessingTask? @Published var hasError: Bool = false @Published var isLoading: Bool = false + @Published var controller: NCMainTabBarController? private var tasks: [NKTextProcessingTask] = [] private let excludedTypeIds = ["OCA\\ContextChat\\TextProcessing\\ContextChatTaskType"] - private let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + private var session: NCSession.Session { + NCSession.shared.getSession(controller: controller) + } - init() { + init(controller: NCMainTabBarController?) { + self.controller = controller load() } @@ -50,7 +55,7 @@ class NCAssistantTask: ObservableObject { guard let id = task.id else { return } isLoading = true - NextcloudKit.shared.textProcessingGetTask(taskId: id, account: appDelegate.account) { _, task, _, error in + NextcloudKit.shared.textProcessingGetTask(taskId: id, account: session.account) { _, task, _, error in self.isLoading = false if error != .success { @@ -65,7 +70,7 @@ class NCAssistantTask: ObservableObject { func scheduleTask(input: String) { isLoading = true - NextcloudKit.shared.textProcessingSchedule(input: input, typeId: selectedType?.id ?? "", identifier: "assistant", account: appDelegate.account) { _, task, _, error in + NextcloudKit.shared.textProcessingSchedule(input: input, typeId: selectedType?.id ?? "", identifier: "assistant", account: session.account) { _, task, _, error in self.isLoading = false if error != .success { @@ -86,7 +91,7 @@ class NCAssistantTask: ObservableObject { guard let id = task.id else { return } isLoading = true - NextcloudKit.shared.textProcessingDeleteTask(taskId: id, account: appDelegate.account) { _, task, _, error in + NextcloudKit.shared.textProcessingDeleteTask(taskId: id, account: session.account) { _, task, _, error in self.isLoading = false if error != .success { @@ -105,7 +110,7 @@ class NCAssistantTask: ObservableObject { private func loadAllTypes() { isLoading = true - NextcloudKit.shared.textProcessingGetTypes(account: appDelegate.account) { _, types, _, error in + NextcloudKit.shared.textProcessingGetTypes(account: session.account) { _, types, _, error in self.isLoading = false if error != .success { @@ -130,7 +135,7 @@ class NCAssistantTask: ObservableObject { private func loadAllTasks(appId: String = "assistant") { isLoading = true - NextcloudKit.shared.textProcessingTaskList(appId: appId, account: appDelegate.account) { _, tasks, _, error in + NextcloudKit.shared.textProcessingTaskList(appId: appId, account: session.account) { _, tasks, _, error in self.isLoading = false if error != .success { diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index b8a8f2f7c9..cfe7fc492b 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -81,7 +81,7 @@ struct NCAssistant: View { } #Preview { - let model = NCAssistantTask() + let model = NCAssistantTask(controller: nil) return NCAssistant() .environmentObject(model) @@ -125,7 +125,7 @@ struct TypeButton: View { .padding(.vertical, 7) .foregroundStyle(model.selectedType?.id == taskType?.id ? .white : .primary) .if(model.selectedType?.id == taskType?.id) { view in - view.background(Color(NCBrandColor.shared.brandElement)) + view.background(Color(NCBrandColor.shared.getElement(account: model.controller?.account))) } .if(model.selectedType?.id != taskType?.id) { view in view.background(.ultraThinMaterial) diff --git a/iOSClient/Assistant/NCAssistantEmptyView.swift b/iOSClient/Assistant/NCAssistantEmptyView.swift index 0c826863b4..d8781d2c91 100644 --- a/iOSClient/Assistant/NCAssistantEmptyView.swift +++ b/iOSClient/Assistant/NCAssistantEmptyView.swift @@ -9,6 +9,7 @@ import SwiftUI struct NCAssistantEmptyView: View { + @EnvironmentObject var model: NCAssistantTask let titleKey, subtitleKey: String var body: some View { @@ -17,7 +18,7 @@ struct NCAssistantEmptyView: View { .renderingMode(.template) .resizable() .aspectRatio(contentMode: .fit) - .foregroundStyle(Color(NCBrandColor.shared.brandElement)) + .foregroundStyle(Color(NCBrandColor.shared.getElement(account: model.controller?.account))) .font(Font.system(.body).weight(.light)) .frame(height: 100) diff --git a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift index c8bbb07501..3544fc6fc8 100644 --- a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift +++ b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift @@ -28,7 +28,7 @@ struct NCAssistantTaskDetail: View { } #Preview { - let model = NCAssistantTask() + let model = NCAssistantTask(controller: nil) return NCAssistantTaskDetail(task: NKTextProcessingTask(id: 1, type: "OCP\\TextProcessing\\FreePromptTaskType", status: 1, userId: "christine", appId: "assistant", input: "", output: "", identifier: "", completionExpectedAt: 1712666412)) .environmentObject(model) diff --git a/iOSClient/AudioRecorder/NCAudioRecorderViewController.swift b/iOSClient/AudioRecorder/NCAudioRecorderViewController.swift index 6ae7328808..ce6572903c 100644 --- a/iOSClient/AudioRecorder/NCAudioRecorderViewController.swift +++ b/iOSClient/AudioRecorder/NCAudioRecorderViewController.swift @@ -27,20 +27,22 @@ import UIKit import AVFoundation import QuartzCore +import NextcloudKit class NCAudioRecorderViewController: UIViewController, NCAudioRecorderDelegate { - - var recording: NCAudioRecorder! - var startDate: Date = Date() - var fileName: String = "" - var serverUrl = "" - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - @IBOutlet weak var contentContainerView: UIView! @IBOutlet weak var durationLabel: UILabel! @IBOutlet weak var startStopLabel: UILabel! @IBOutlet weak var voiceRecordHUD: VoiceRecordHUD! + var recording: NCAudioRecorder! + var startDate: Date = Date() + var fileName: String = "" + var controller: NCMainTabBarController! + var session: NCSession.Session { + NCSession.shared.getSession(controller: controller) + } + // MARK: - View Life Cycle override func viewDidLoad() { @@ -55,7 +57,7 @@ class NCAudioRecorderViewController: UIViewController, NCAudioRecorderDelegate { voiceRecordHUD.fillColor = UIColor.green Task { - self.fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + ".m4a", account: self.appDelegate.account, serverUrl: self.serverUrl) + self.fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + ".m4a", account: self.session.account, serverUrl: controller.currentServerUrl()) recording = NCAudioRecorder(to: self.fileName) recording.delegate = self do { @@ -97,7 +99,15 @@ class NCAudioRecorderViewController: UIViewController, NCAudioRecorderDelegate { func uploadMetadata() { let fileNamePath = NSTemporaryDirectory() + self.fileName - let metadata = NCManageDatabase.shared.createMetadata(account: appDelegate.account, user: appDelegate.user, userId: appDelegate.userId, fileName: fileName, fileNameView: fileName, ocId: UUID().uuidString, serverUrl: self.serverUrl, urlBase: appDelegate.urlBase, url: "", contentType: "") + let metadata = NCManageDatabase.shared.createMetadata(fileName: fileName, + fileNameView: fileName, + ocId: UUID().uuidString, + serverUrl: controller.currentServerUrl(), + url: "", + contentType: "", + session: self.session, + sceneIdentifier: self.controller?.sceneIdentifier) + metadata.session = NCNetworking.shared.sessionUploadBackground metadata.sessionSelector = NCGlobal.shared.selectorUploadFile metadata.status = NCGlobal.shared.metadataStatusWaitUpload diff --git a/iOSClient/BrowserWeb/NCBrowserWeb.swift b/iOSClient/BrowserWeb/NCBrowserWeb.swift index fc45fdee5b..1cdabbcc65 100644 --- a/iOSClient/BrowserWeb/NCBrowserWeb.swift +++ b/iOSClient/BrowserWeb/NCBrowserWeb.swift @@ -22,7 +22,7 @@ // import UIKit -import WebKit +@preconcurrency import WebKit @objc protocol NCBrowserWebDelegate: AnyObject { @objc optional func browserWebDismiss() diff --git a/iOSClient/Color/NCColorPicker.swift b/iOSClient/Color/NCColorPicker.swift index 61951e0fba..8b6c85a7bb 100644 --- a/iOSClient/Color/NCColorPicker.swift +++ b/iOSClient/Color/NCColorPicker.swift @@ -129,7 +129,7 @@ class NCColorPicker: UIViewController { systemIndigoButton.layer.cornerRadius = 5 systemIndigoButton.layer.masksToBounds = true - defaultButton.backgroundColor = NCBrandColor.shared.brandElement + defaultButton.backgroundColor = NCBrandColor.shared.customer defaultButton.layer.cornerRadius = 5 defaultButton.layer.masksToBounds = true } @@ -214,7 +214,7 @@ class NCColorPicker: UIViewController { let serverUrl = metadata.serverUrl + "/" + metadata.fileName NCManageDatabase.shared.setDirectory(serverUrl: serverUrl, colorFolder: hexColor, metadata: metadata) self.dismiss(animated: true) - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource, userInfo: ["serverUrl": metadata.serverUrl, "clearDataSource": true]) } self.dismiss(animated: true) } diff --git a/iOSClient/Data/NCManageDatabase+Account.swift b/iOSClient/Data/NCManageDatabase+Account.swift index 91038bee94..4d599d6f75 100644 --- a/iOSClient/Data/NCManageDatabase+Account.swift +++ b/iOSClient/Data/NCManageDatabase+Account.swift @@ -22,10 +22,11 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit -class tableAccount: Object, NCUserBaseUrl { +class tableAccount: Object { @objc dynamic var account = "" @objc dynamic var active: Bool = false @objc dynamic var address = "" @@ -84,13 +85,16 @@ extension NCManageDatabase { do { let realm = try Realm() try realm.write { - let addObject = tableAccount() - addObject.account = account + if let result = realm.objects(tableAccount.self).filter("account == %@", account).first { + realm.delete(result) + } + let tableAccount = tableAccount() + tableAccount.account = account NCKeychain().setPassword(account: account, password: password) - addObject.urlBase = urlBase - addObject.user = user - addObject.userId = userId - realm.add(addObject, update: .all) + tableAccount.urlBase = urlBase + tableAccount.user = user + tableAccount.userId = userId + realm.add(tableAccount, update: .all) } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") @@ -108,22 +112,21 @@ extension NCManageDatabase { } } - func deleteAccount(_ account: String) { + func getActiveTableAccount() -> tableAccount? { do { let realm = try Realm() - try realm.write { - let result = realm.objects(tableAccount.self).filter("account == %@", account) - realm.delete(result) - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + guard let result = realm.objects(tableAccount.self).filter("active == true").first else { return nil } + return tableAccount.init(value: result) + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } + return nil } - func getActiveAccount() -> tableAccount? { + func getTableAccount(account: String) -> tableAccount? { do { let realm = try Realm() - guard let result = realm.objects(tableAccount.self).filter("active == true").first else { return nil } + guard let result = realm.objects(tableAccount.self).filter("account == %@", account).first else { return nil } return tableAccount.init(value: result) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") @@ -144,7 +147,7 @@ extension NCManageDatabase { return nil } - func getAccount(predicate: NSPredicate) -> tableAccount? { + func getTableAccount(predicate: NSPredicate) -> tableAccount? { do { let realm = try Realm() guard let result = realm.objects(tableAccount.self).filter(predicate).first else { return nil } @@ -155,7 +158,7 @@ extension NCManageDatabase { return nil } - func getAllAccount() -> [tableAccount] { + func getAllTableAccount() -> [tableAccount] { do { let realm = try Realm() let sorted = [SortDescriptor(keyPath: "active", ascending: false), SortDescriptor(keyPath: "user", ascending: true)] @@ -194,16 +197,16 @@ extension NCManageDatabase { return "" } - func getAccountAutoUploadDirectory(urlBase: String, userId: String, account: String) -> String { + func getAccountAutoUploadDirectory(session: NCSession.Session) -> String { do { let realm = try Realm() guard let result = realm.objects(tableAccount.self).filter("active == true").first else { return "" } if result.autoUploadDirectory.isEmpty { - return utilityFileSystem.getHomeServer(urlBase: urlBase, userId: userId) + return utilityFileSystem.getHomeServer(session: session) } else { // FIX change webdav -> /dav/files/ if result.autoUploadDirectory.contains("/webdav") { - return utilityFileSystem.getHomeServer(urlBase: urlBase, userId: userId) + return utilityFileSystem.getHomeServer(session: session) } else { return result.autoUploadDirectory } @@ -214,9 +217,9 @@ extension NCManageDatabase { return "" } - func getAccountAutoUploadPath(urlBase: String, userId: String, account: String) -> String { + func getAccountAutoUploadPath(session: NCSession.Session) -> String { let cameraFileName = self.getAccountAutoUploadFileName() - let cameraDirectory = self.getAccountAutoUploadDirectory(urlBase: urlBase, userId: userId, account: account) + let cameraDirectory = self.getAccountAutoUploadDirectory(session: session) let folderPhotos = utilityFileSystem.stringAppendServerUrl(cameraDirectory, addFileName: cameraFileName) return folderPhotos } @@ -232,9 +235,7 @@ extension NCManageDatabase { return NCGlobal.shared.subfolderGranularityMonthly } - func setAccountActive(_ account: String) -> tableAccount? { - var accountReturn = tableAccount() - + func setAccountActive(_ account: String) { do { let realm = try Realm() try realm.write { @@ -242,27 +243,11 @@ extension NCManageDatabase { for result in results { if result.account == account { result.active = true - accountReturn = result } else { result.active = false } } } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - return nil - } - return tableAccount.init(value: accountReturn) - } - - func removePasswordAccount(_ account: String) { - do { - let realm = try Realm() - try realm.write { - if let result = realm.objects(tableAccount.self).filter("account == %@", account).first { - result.password = "********" - } - } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } @@ -309,7 +294,7 @@ extension NCManageDatabase { } } - func setAccountAutoUploadDirectory(_ serverUrl: String?, urlBase: String, userId: String, account: String) { + func setAccountAutoUploadDirectory(_ serverUrl: String?, session: NCSession.Session) { do { let realm = try Realm() try realm.write { @@ -317,7 +302,7 @@ extension NCManageDatabase { if let serverUrl = serverUrl { result.autoUploadDirectory = serverUrl } else { - result.autoUploadDirectory = self.getAccountAutoUploadDirectory(urlBase: urlBase, userId: userId, account: account) + result.autoUploadDirectory = self.getAccountAutoUploadDirectory(session: session) } } } diff --git a/iOSClient/Data/NCManageDatabase+Activity.swift b/iOSClient/Data/NCManageDatabase+Activity.swift index 543a02b40c..5592b93593 100644 --- a/iOSClient/Data/NCManageDatabase+Activity.swift +++ b/iOSClient/Data/NCManageDatabase+Activity.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit import SwiftyJSON diff --git a/iOSClient/Data/NCManageDatabase+Avatar.swift b/iOSClient/Data/NCManageDatabase+Avatar.swift index 6f398043a0..fce4d20d2f 100644 --- a/iOSClient/Data/NCManageDatabase+Avatar.swift +++ b/iOSClient/Data/NCManageDatabase+Avatar.swift @@ -104,8 +104,9 @@ extension NCManageDatabase { return image } - func getImageAvatarLoaded(fileName: String) -> UIImage? { + func getImageAvatarLoaded(fileName: String) -> (image: UIImage?, tableAvatar: tableAvatar?) { let fileNameLocalPath = utilityFileSystem.directoryUserData + "/" + fileName + let image = UIImage(contentsOfFile: fileNameLocalPath) do { let realm = try Realm() @@ -113,16 +114,13 @@ extension NCManageDatabase { let result = realm.objects(tableAvatar.self).filter("fileName == %@", fileName).first if result == nil { utilityFileSystem.removeFile(atPath: fileNameLocalPath) - return nil - } else if result?.loaded == false { - return nil } - return UIImage(contentsOfFile: fileNameLocalPath) + return (image, result) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } utilityFileSystem.removeFile(atPath: fileNameLocalPath) - return nil + return (nil, nil) } } diff --git a/iOSClient/Data/NCManageDatabase+Capabilities.swift b/iOSClient/Data/NCManageDatabase+Capabilities.swift index 1a67389ee5..059847c1fa 100644 --- a/iOSClient/Data/NCManageDatabase+Capabilities.swift +++ b/iOSClient/Data/NCManageDatabase+Capabilities.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit @@ -62,7 +63,8 @@ extension NCManageDatabase { return nil } - func setCapabilities(account: String, data: Data? = nil) { + @discardableResult + func setCapabilities(account: String, data: Data? = nil) -> NCCapabilities.Capabilities? { let jsonData: Data? struct CapabilityNextcloud: Codable { @@ -285,97 +287,108 @@ extension NCManageDatabase { let ocs: Ocs } - if let data = data { + if let data { jsonData = data } else { do { let realm = try Realm() guard let result = realm.objects(tableCapabilities.self).filter("account == %@", account).first, let data = result.jsondata else { - return + return nil } jsonData = data } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") - return + return nil } } - guard let jsonData = jsonData else { return } + guard let jsonData = jsonData else { + return nil + } do { - let global = NCGlobal.shared let json = try JSONDecoder().decode(CapabilityNextcloud.self, from: jsonData) let data = json.ocs.data + let capabilities = NCCapabilities.Capabilities() - global.capabilityServerVersion = data.version.string - global.capabilityServerVersionMajor = data.version.major + capabilities.capabilityServerVersion = data.version.string + capabilities.capabilityServerVersionMajor = data.version.major - if global.capabilityServerVersionMajor > 0 { - NextcloudKit.shared.setup(nextcloudVersion: global.capabilityServerVersionMajor) + if capabilities.capabilityServerVersionMajor > 0 { + NextcloudKit.shared.updateSession(account: account, nextcloudVersion: capabilities.capabilityServerVersionMajor) } - global.capabilityFileSharingApiEnabled = data.capabilities.filessharing?.apienabled ?? false - global.capabilityFileSharingDefaultPermission = data.capabilities.filessharing?.defaultpermissions ?? 0 - global.capabilityFileSharingPubPasswdEnforced = data.capabilities.filessharing?.ncpublic?.password?.enforced ?? false - global.capabilityFileSharingPubExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredate?.enforced ?? false - global.capabilityFileSharingPubExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredate?.days ?? 0 - global.capabilityFileSharingInternalExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.enforced ?? false - global.capabilityFileSharingInternalExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 - global.capabilityFileSharingRemoteExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateremote?.enforced ?? false - global.capabilityFileSharingRemoteExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateremote?.days ?? 0 - - global.capabilityThemingColor = data.capabilities.theming?.color ?? "" - global.capabilityThemingColorElement = data.capabilities.theming?.colorelement ?? "" - global.capabilityThemingColorText = data.capabilities.theming?.colortext ?? "" - global.capabilityThemingName = data.capabilities.theming?.name ?? "" - global.capabilityThemingSlogan = data.capabilities.theming?.slogan ?? "" - - global.capabilityE2EEEnabled = data.capabilities.endtoendencryption?.enabled ?? false - global.capabilityE2EEApiVersion = data.capabilities.endtoendencryption?.apiversion ?? "" - - global.capabilityRichDocumentsEnabled = json.ocs.data.capabilities.richdocuments?.directediting ?? false - global.capabilityRichDocumentsMimetypes.removeAll() + capabilities.capabilityFileSharingApiEnabled = data.capabilities.filessharing?.apienabled ?? false + capabilities.capabilityFileSharingDefaultPermission = data.capabilities.filessharing?.defaultpermissions ?? 0 + capabilities.capabilityFileSharingPubPasswdEnforced = data.capabilities.filessharing?.ncpublic?.password?.enforced ?? false + capabilities.capabilityFileSharingPubExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredate?.enforced ?? false + capabilities.capabilityFileSharingPubExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredate?.days ?? 0 + capabilities.capabilityFileSharingInternalExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.enforced ?? false + capabilities.capabilityFileSharingInternalExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 + capabilities.capabilityFileSharingRemoteExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateremote?.enforced ?? false + capabilities.capabilityFileSharingRemoteExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateremote?.days ?? 0 + + capabilities.capabilityThemingColor = data.capabilities.theming?.color ?? "" + capabilities.capabilityThemingColorElement = data.capabilities.theming?.colorelement ?? "" + capabilities.capabilityThemingColorText = data.capabilities.theming?.colortext ?? "" + capabilities.capabilityThemingName = data.capabilities.theming?.name ?? "" + capabilities.capabilityThemingSlogan = data.capabilities.theming?.slogan ?? "" + + capabilities.capabilityE2EEEnabled = data.capabilities.endtoendencryption?.enabled ?? false + capabilities.capabilityE2EEApiVersion = data.capabilities.endtoendencryption?.apiversion ?? "" + + capabilities.capabilityRichDocumentsEnabled = json.ocs.data.capabilities.richdocuments?.directediting ?? false + capabilities.capabilityRichDocumentsMimetypes.removeAll() if let mimetypes = data.capabilities.richdocuments?.mimetypes { for mimetype in mimetypes { - global.capabilityRichDocumentsMimetypes.append(mimetype) + capabilities.capabilityRichDocumentsMimetypes.append(mimetype) } } - global.capabilityAssistantEnabled = data.capabilities.assistant?.enabled ?? false + capabilities.capabilityAssistantEnabled = data.capabilities.assistant?.enabled ?? false - global.capabilityActivity.removeAll() + capabilities.capabilityActivity.removeAll() if let activities = data.capabilities.activity?.apiv2 { for activity in activities { - global.capabilityActivity.append(activity) + capabilities.capabilityActivity.append(activity) } } - global.capabilityNotification.removeAll() + capabilities.capabilityNotification.removeAll() if let notifications = data.capabilities.notifications?.ocsendpoints { for notification in notifications { - global.capabilityNotification.append(notification) + capabilities.capabilityNotification.append(notification) } } - global.capabilityFilesUndelete = data.capabilities.files?.undelete ?? false - global.capabilityFilesLockVersion = data.capabilities.files?.locking ?? "" - global.capabilityFilesComments = data.capabilities.files?.comments ?? false - global.capabilityFilesBigfilechunking = data.capabilities.files?.bigfilechunking ?? false + capabilities.capabilityFilesUndelete = data.capabilities.files?.undelete ?? false + capabilities.capabilityFilesLockVersion = data.capabilities.files?.locking ?? "" + capabilities.capabilityFilesComments = data.capabilities.files?.comments ?? false + capabilities.capabilityFilesBigfilechunking = data.capabilities.files?.bigfilechunking ?? false - global.capabilityUserStatusEnabled = data.capabilities.userstatus?.enabled ?? false + capabilities.capabilityUserStatusEnabled = data.capabilities.userstatus?.enabled ?? false if data.capabilities.external != nil { - global.capabilityExternalSites = true + capabilities.capabilityExternalSites = true + } + capabilities.capabilityGroupfoldersEnabled = data.capabilities.groupfolders?.hasGroupFolders ?? false + + if capabilities.capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion28 { + capabilities.isLivePhotoServerAvailable = true } - global.capabilityGroupfoldersEnabled = data.capabilities.groupfolders?.hasGroupFolders ?? false - global.capabilitySecurityGuardDiagnostics = data.capabilities.securityguard?.diagnostics ?? false - global.capabilityForbiddenFileNames = data.capabilities.files?.forbiddenFileNames ?? [] - global.capabilityForbiddenFileNameBasenames = data.capabilities.files?.forbiddenFileNameBasenames ?? [] - global.capabilityForbiddenFileNameCharacters = data.capabilities.files?.forbiddenFileNameCharacters ?? [] - global.capabilityForbiddenFileNameExtensions = data.capabilities.files?.forbiddenFileNameExtensions ?? [] + capabilities.capabilitySecurityGuardDiagnostics = data.capabilities.securityguard?.diagnostics ?? false + + capabilities.capabilityForbiddenFileNames = data.capabilities.files?.forbiddenFileNames ?? [] + capabilities.capabilityForbiddenFileNameBasenames = data.capabilities.files?.forbiddenFileNameBasenames ?? [] + capabilities.capabilityForbiddenFileNameCharacters = data.capabilities.files?.forbiddenFileNameCharacters ?? [] + capabilities.capabilityForbiddenFileNameExtensions = data.capabilities.files?.forbiddenFileNameExtensions ?? [] + + NCCapabilities.shared.appendCapabilities(account: account, capabilities: capabilities) + + return capabilities } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") - return + return nil } } } diff --git a/iOSClient/Data/NCManageDatabase+Chunk.swift b/iOSClient/Data/NCManageDatabase+Chunk.swift index 58468af836..d72206f286 100644 --- a/iOSClient/Data/NCManageDatabase+Chunk.swift +++ b/iOSClient/Data/NCManageDatabase+Chunk.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Comments.swift b/iOSClient/Data/NCManageDatabase+Comments.swift index 0af3955943..e2023dc556 100644 --- a/iOSClient/Data/NCManageDatabase+Comments.swift +++ b/iOSClient/Data/NCManageDatabase+Comments.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+DashboardWidget.swift b/iOSClient/Data/NCManageDatabase+DashboardWidget.swift index 70f0c0ec50..792ad179d0 100644 --- a/iOSClient/Data/NCManageDatabase+DashboardWidget.swift +++ b/iOSClient/Data/NCManageDatabase+DashboardWidget.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+DirectEditing.swift b/iOSClient/Data/NCManageDatabase+DirectEditing.swift index 6226375539..22b0493020 100644 --- a/iOSClient/Data/NCManageDatabase+DirectEditing.swift +++ b/iOSClient/Data/NCManageDatabase+DirectEditing.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Directory.swift b/iOSClient/Data/NCManageDatabase+Directory.swift index d67a0c9877..448c9ba0ba 100644 --- a/iOSClient/Data/NCManageDatabase+Directory.swift +++ b/iOSClient/Data/NCManageDatabase+Directory.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit @@ -102,7 +103,7 @@ extension NCManageDatabase { let results = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl BEGINSWITH %@", account, serverUrl) for result in results { self.deleteMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", result.account, result.serverUrl)) - self.deleteLocalFile(predicate: NSPredicate(format: "ocId == %@", result.ocId)) + self.deleteLocalFileOcId(result.ocId) } try realm.write { realm.delete(results) @@ -142,19 +143,6 @@ extension NCManageDatabase { } } - func cleanEtagDirectory(account: String, serverUrl: String) { - do { - let realm = try Realm() - try realm.write { - if let result = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", account, serverUrl).first { - result.etag = "" - } - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - func getTableDirectory(predicate: NSPredicate) -> tableDirectory? { do { let realm = try Realm() diff --git a/iOSClient/Data/NCManageDatabase+E2EE.swift b/iOSClient/Data/NCManageDatabase+E2EE.swift index 86346cc5c3..a077bb1159 100644 --- a/iOSClient/Data/NCManageDatabase+E2EE.swift +++ b/iOSClient/Data/NCManageDatabase+E2EE.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+ExternalSites.swift b/iOSClient/Data/NCManageDatabase+ExternalSites.swift index 3cb09b85f8..5aae7b9446 100644 --- a/iOSClient/Data/NCManageDatabase+ExternalSites.swift +++ b/iOSClient/Data/NCManageDatabase+ExternalSites.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+GPS.swift b/iOSClient/Data/NCManageDatabase+GPS.swift index 4e0c3928b6..997c186e03 100644 --- a/iOSClient/Data/NCManageDatabase+GPS.swift +++ b/iOSClient/Data/NCManageDatabase+GPS.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Groupfolders.swift b/iOSClient/Data/NCManageDatabase+Groupfolders.swift index 6251e02800..9c01149fff 100644 --- a/iOSClient/Data/NCManageDatabase+Groupfolders.swift +++ b/iOSClient/Data/NCManageDatabase+Groupfolders.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+LayoutForView.swift b/iOSClient/Data/NCManageDatabase+LayoutForView.swift index 4286a84695..eaa4391a02 100644 --- a/iOSClient/Data/NCManageDatabase+LayoutForView.swift +++ b/iOSClient/Data/NCManageDatabase+LayoutForView.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+LocalFile.swift b/iOSClient/Data/NCManageDatabase+LocalFile.swift index 608bae383b..f6fa14e157 100644 --- a/iOSClient/Data/NCManageDatabase+LocalFile.swift +++ b/iOSClient/Data/NCManageDatabase+LocalFile.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit @@ -100,11 +101,13 @@ extension NCManageDatabase { } } - func deleteLocalFile(predicate: NSPredicate) { + func deleteLocalFileOcId(_ ocId: String?) { + guard let ocId else { return } + do { let realm = try Realm() try realm.write { - let results = realm.objects(tableLocalFile.self).filter(predicate) + let results = realm.objects(tableLocalFile.self).filter("ocId == %@", ocId) realm.delete(results) } } catch let error { diff --git a/iOSClient/Data/NCManageDatabase+Metadata+Session.swift b/iOSClient/Data/NCManageDatabase+Metadata+Session.swift index 2d4328ed35..c547ed1122 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata+Session.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata+Session.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit @@ -29,6 +30,7 @@ extension NCManageDatabase { func setMetadataSession(ocId: String, newFileName: String? = nil, session: String? = nil, + sessionTaskIdentifier: Int? = nil, sessionError: String? = nil, selector: String? = nil, status: Int? = nil, @@ -46,6 +48,9 @@ extension NCManageDatabase { if let session { result.session = session } + if let sessionTaskIdentifier { + result.sessionTaskIdentifier = sessionTaskIdentifier + } if let sessionError { result.sessionError = sessionError if sessionError.isEmpty { @@ -76,31 +81,6 @@ extension NCManageDatabase { } } - func setMetadataSession(ocId: String, - status: Int? = nil, - taskIdentifier: Int? = nil) { - do { - let realm = try Realm() - try realm.write { - if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first { - if let status { - result.status = status - if status == NCGlobal.shared.metadataStatusWaitDownload || status == NCGlobal.shared.metadataStatusWaitUpload { - result.sessionDate = Date() - } else if status == NCGlobal.shared.metadataStatusNormal { - result.sessionDate = nil - } - } - if let taskIdentifier { - result.sessionTaskIdentifier = taskIdentifier - } - } - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - @discardableResult func setMetadatasSessionInWaitDownload(metadatas: [tableMetadata], session: String, selector: String, sceneIdentifier: String? = nil) -> tableMetadata? { if metadatas.isEmpty { return nil } @@ -113,6 +93,7 @@ extension NCManageDatabase { if let result = realm.objects(tableMetadata.self).filter("ocId == %@", metadata.ocId).first { result.sceneIdentifier = sceneIdentifier result.session = session + result.sessionTaskIdentifier = 0 result.sessionError = "" result.sessionSelector = selector result.status = NCGlobal.shared.metadataStatusWaitDownload @@ -121,6 +102,7 @@ extension NCManageDatabase { } else { metadata.sceneIdentifier = sceneIdentifier metadata.session = session + metadata.sessionTaskIdentifier = 0 metadata.sessionError = "" metadata.sessionSelector = selector metadata.status = NCGlobal.shared.metadataStatusWaitDownload @@ -137,17 +119,39 @@ extension NCManageDatabase { return metadataUpdated } - func clearMetadataSession(metadatas: Results) { + func clearMetadataSession(metadatas: [tableMetadata]) { do { let realm = try Realm() try realm.write { for metadata in metadatas { - metadata.sceneIdentifier = nil - metadata.session = "" - metadata.sessionError = "" - metadata.sessionSelector = "" - metadata.sessionDate = nil - metadata.status = NCGlobal.shared.metadataStatusNormal + if let result = realm.objects(tableMetadata.self).filter("ocId == %@", metadata.ocId).first { + result.sceneIdentifier = nil + result.session = "" + result.sessionTaskIdentifier = 0 + result.sessionError = "" + result.sessionSelector = "" + result.sessionDate = nil + result.status = NCGlobal.shared.metadataStatusNormal + } + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + + func clearMetadataSession(metadata: tableMetadata) { + do { + let realm = try Realm() + try realm.write { + if let result = realm.objects(tableMetadata.self).filter("ocId == %@", metadata.ocId).first { + result.sceneIdentifier = nil + result.session = "" + result.sessionTaskIdentifier = 0 + result.sessionError = "" + result.sessionSelector = "" + result.sessionDate = nil + result.status = NCGlobal.shared.metadataStatusNormal } } } catch let error { @@ -164,6 +168,12 @@ extension NCManageDatabase { try realm.write { result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first result?.status = status + + if status == NCGlobal.shared.metadataStatusNormal { + result?.sessionDate = nil + } else { + result?.sessionDate = Date() + } } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") @@ -184,6 +194,9 @@ extension NCManageDatabase { if serverUrl.hasSuffix("/") { serverUrl = String(serverUrl.dropLast()) } - return NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "serverUrl == %@ AND fileName == %@ AND sessionTaskIdentifier == %d", serverUrl, fileName, sessionTaskIdentifier)) + return getMetadata(predicate: NSPredicate(format: "serverUrl == %@ AND fileName == %@ AND sessionTaskIdentifier == %d", + serverUrl, + fileName, + sessionTaskIdentifier)) } } diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 71ce34f006..e450cd161f 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -22,10 +22,11 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit -class tableMetadata: Object, NCUserBaseUrl { +class tableMetadata: Object { override func isEqual(_ object: Any?) -> Bool { if let object = object as? tableMetadata, self.account == object.account, @@ -35,6 +36,7 @@ class tableMetadata: Object, NCUserBaseUrl { self.fileName == object.fileName, self.fileNameView == object.fileNameView, self.date == object.date, + self.datePhotosOriginal == object.datePhotosOriginal, self.permissions == object.permissions, self.hasPreview == object.hasPreview, self.note == object.note, @@ -47,6 +49,7 @@ class tableMetadata: Object, NCUserBaseUrl { self.latitude == object.latitude, self.longitude == object.longitude, self.altitude == object.altitude, + self.status == object.status, Array(self.tags).elementsEqual(Array(object.tags)), Array(self.shareType).elementsEqual(Array(object.shareType)), Array(self.sharePermissionsCloudMesh).elementsEqual(Array(object.sharePermissionsCloudMesh)) { @@ -66,12 +69,14 @@ class tableMetadata: Object, NCUserBaseUrl { @objc dynamic var creationDate = NSDate() @objc dynamic var dataFingerprint = "" @objc dynamic var date = NSDate() + @objc dynamic var datePhotosOriginal = NSDate() @objc dynamic var directory: Bool = false @objc dynamic var downloadURL = "" @objc dynamic var e2eEncrypted: Bool = false @objc dynamic var edited: Bool = false @objc dynamic var etag = "" @objc dynamic var etagResource = "" + let exifPhotos = List() @objc dynamic var favorite: Bool = false @objc dynamic var fileId = "" @objc dynamic var fileName = "" @@ -87,7 +92,7 @@ class tableMetadata: Object, NCUserBaseUrl { @objc dynamic var name = "" // for unifiedSearch is the provider.id @objc dynamic var note = "" @objc dynamic var ocId = "" - @objc dynamic var ocIdTemp = "" + @objc dynamic var ocIdTransfer = "" @objc dynamic var ownerId = "" @objc dynamic var ownerDisplayName = "" @objc public var lock = false @@ -99,12 +104,15 @@ class tableMetadata: Object, NCUserBaseUrl { @objc public var lockTimeOut: Date? @objc dynamic var path = "" @objc dynamic var permissions = "" + @objc dynamic var placePhotos: String? @objc dynamic var quotaUsedBytes: Int64 = 0 @objc dynamic var quotaAvailableBytes: Int64 = 0 @objc dynamic var resourceType = "" @objc dynamic var richWorkspace: String? @objc dynamic var sceneIdentifier: String? @objc dynamic var serverUrl = "" + @objc dynamic var serveUrlFileName = "" + @objc dynamic var serverUrlTo = "" @objc dynamic var session = "" @objc dynamic var sessionDate: Date? @objc dynamic var sessionError = "" @@ -115,6 +123,7 @@ class tableMetadata: Object, NCUserBaseUrl { let shareType = List() @objc dynamic var size: Int64 = 0 @objc dynamic var status: Int = 0 + @objc dynamic var storeFlag: String? @objc dynamic var subline: String? let tags = List() @objc dynamic var trashbinFileName = "" @@ -178,6 +187,10 @@ extension tableMetadata { return classFile == NKCommon.TypeClassFile.audio.rawValue || classFile == NKCommon.TypeClassFile.video.rawValue } + var isImageOrVideo: Bool { + return classFile == NKCommon.TypeClassFile.image.rawValue || classFile == NKCommon.TypeClassFile.video.rawValue + } + var isVideo: Bool { return classFile == NKCommon.TypeClassFile.video.rawValue } @@ -232,28 +245,17 @@ extension tableMetadata { return !isDirectoryE2EE && directory && size == 0 && e2eEncrypted && NCKeychain().isEndToEndEnabled(account: account) } - var isWaitingTransfer: Bool { - status == NCGlobal.shared.metadataStatusWaitDownload || status == NCGlobal.shared.metadataStatusWaitUpload || status == NCGlobal.shared.metadataStatusUploadError - } - - var isInTransfer: Bool { - status == NCGlobal.shared.metadataStatusDownloading || status == NCGlobal.shared.metadataStatusUploading - } - - var isTransferInForeground: Bool { - (status > 0 && (chunk > 0 || e2eEncrypted)) - } - var isDownload: Bool { - status == NCGlobal.shared.metadataStatusDownloading + status == NCGlobal.shared.metadataStatusWaitDownload || status == NCGlobal.shared.metadataStatusDownloading } var isUpload: Bool { - status == NCGlobal.shared.metadataStatusUploading + status == NCGlobal.shared.metadataStatusWaitUpload || status == NCGlobal.shared.metadataStatusUploading } @objc var isDirectoryE2EE: Bool { - NCUtilityFileSystem().isDirectoryE2EE(account: account, urlBase: urlBase, userId: userId, serverUrl: serverUrl) + let session = NCSession.Session(account: account, urlBase: urlBase, user: user, userId: userId) + return NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) } var isDirectoryE2EETop: Bool { @@ -273,7 +275,7 @@ extension tableMetadata { } var hasPreviewBorder: Bool { - !isImage && !isAudioOrVideo && hasPreview && NCUtilityFileSystem().fileProviderStoragePreviewIconExists(ocId, etag: etag) + !isImage && !isAudioOrVideo && hasPreview && NCUtilityFileSystem().fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt1024) } var isAvailableEditorView: Bool { @@ -284,7 +286,7 @@ extension tableMetadata { let directEditingEditors = utility.editorsDirectEditing(account: account, contentType: contentType) let richDocumentEditor = utility.isTypeFileRichDocument(self) - if NCGlobal.shared.capabilityRichDocumentsEnabled && richDocumentEditor && directEditingEditors.isEmpty { + if NCCapabilities.shared.getCapabilities(account: account).capabilityRichDocumentsEnabled && richDocumentEditor && directEditingEditors.isEmpty { // RichDocument: Collabora return true } else if directEditingEditors.contains(NCGlobal.shared.editorText) || directEditingEditors.contains(NCGlobal.shared.editorOnlyoffice) { @@ -295,7 +297,9 @@ extension tableMetadata { } var isAvailableRichDocumentEditorView: Bool { - guard (classFile == NKCommon.TypeClassFile.document.rawValue) && NCGlobal.shared.capabilityRichDocumentsEnabled && NextcloudKit.shared.isNetworkReachable() else { return false } + guard (classFile == NKCommon.TypeClassFile.document.rawValue), + NCCapabilities.shared.getCapabilities(account: account).capabilityRichDocumentsEnabled, + NextcloudKit.shared.isNetworkReachable() else { return false } if NCUtility().isTypeFileRichDocument(self) { return true @@ -324,7 +328,7 @@ extension tableMetadata { // Return if is sharable func isSharable() -> Bool { - if !NCGlobal.shared.capabilityFileSharingApiEnabled || (NCGlobal.shared.capabilityE2EEEnabled && isDirectoryE2EE) { + if !NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingApiEnabled || (NCCapabilities.shared.getCapabilities(account: account).capabilityE2EEEnabled && isDirectoryE2EE) { return false } return true @@ -346,10 +350,23 @@ extension NCManageDatabase { } metadata.dataFingerprint = file.dataFingerprint metadata.date = file.date as NSDate + if let datePhotosOriginal = file.datePhotosOriginal { + metadata.datePhotosOriginal = datePhotosOriginal as NSDate + } else { + metadata.datePhotosOriginal = metadata.date + } metadata.directory = file.directory metadata.downloadURL = file.downloadURL metadata.e2eEncrypted = file.e2eEncrypted metadata.etag = file.etag + for dict in file.exifPhotos { + for (key, value) in dict { + let keyValue = NCKeyValue() + keyValue.key = key + keyValue.value = value + metadata.exifPhotos.append(keyValue) + } + } metadata.favorite = file.favorite metadata.fileId = file.fileId metadata.fileName = file.fileName @@ -361,6 +378,7 @@ extension NCManageDatabase { metadata.name = file.name metadata.note = file.note metadata.ocId = file.ocId + metadata.ocIdTransfer = file.ocId metadata.ownerId = file.ownerId metadata.ownerDisplayName = file.ownerDisplayName metadata.lock = file.lock @@ -372,11 +390,13 @@ extension NCManageDatabase { metadata.lockTimeOut = file.lockTimeOut metadata.path = file.path metadata.permissions = file.permissions + metadata.placePhotos = file.placePhotos metadata.quotaUsedBytes = file.quotaUsedBytes metadata.quotaAvailableBytes = file.quotaAvailableBytes metadata.richWorkspace = file.richWorkspace metadata.resourceType = file.resourceType metadata.serverUrl = file.serverUrl + metadata.serveUrlFileName = file.serverUrl + "/" + file.fileName metadata.sharePermissionsCollaborationServices = file.sharePermissionsCollaborationServices for element in file.sharePermissionsCloudMesh { metadata.sharePermissionsCloudMesh.append(element) @@ -411,9 +431,9 @@ extension NCManageDatabase { // E2EE find the fileName for fileNameView if isDirectoryE2EE || file.e2eEncrypted { - if let tableE2eEncryption = NCManageDatabase.shared.getE2eEncryption(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameIdentifier == %@", file.account, file.serverUrl, file.fileName)) { + if let tableE2eEncryption = getE2eEncryption(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameIdentifier == %@", file.account, file.serverUrl, file.fileName)) { metadata.fileNameView = tableE2eEncryption.fileName - let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, mimeType: file.contentType, directory: file.directory) + let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadata.fileNameView, mimeType: file.contentType, directory: file.directory, account: file.account) metadata.contentType = results.mimeType metadata.iconName = results.iconName metadata.classFile = results.classFile @@ -450,6 +470,14 @@ extension NCManageDatabase { completion(metadataFolder, metadatas) } + func getMetadataDirectoryFrom(files: [NKFile]) -> tableMetadata? { + guard let file = files.first else { return nil } + let isDirectoryE2EE = NCUtilityFileSystem().isDirectoryE2EE(file: file) + let metadata = convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE) + + return metadata + } + func convertFilesToMetadatas(_ files: [NKFile], useFirstAsMetadataFolder: Bool) async -> (metadataFolder: tableMetadata, metadatas: [tableMetadata]) { await withUnsafeContinuation({ continuation in convertFilesToMetadatas(files, useFirstAsMetadataFolder: useFirstAsMetadataFolder) { metadataFolder, metadatas in @@ -458,7 +486,7 @@ extension NCManageDatabase { }) } - func createMetadata(account: String, user: String, userId: String, fileName: String, fileNameView: String, ocId: String, serverUrl: String, urlBase: String, url: String, contentType: String, isUrl: Bool = false, name: String = NCGlobal.shared.appName, subline: String? = nil, iconName: String? = nil, iconUrl: String? = nil) -> tableMetadata { + func createMetadata(fileName: String, fileNameView: String, ocId: String, serverUrl: String, url: String, contentType: String, isUrl: Bool = false, name: String = NCGlobal.shared.appName, subline: String? = nil, iconName: String? = nil, iconUrl: String? = nil, directory: Bool = false, session: NCSession.Session, sceneIdentifier: String?) -> tableMetadata { let metadata = tableMetadata() if isUrl { @@ -470,7 +498,7 @@ extension NCManageDatabase { } metadata.classFile = NKCommon.TypeClassFile.url.rawValue } else { - let (mimeType, classFile, iconName, _, _, _) = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: fileName, mimeType: contentType, directory: false) + let (mimeType, classFile, iconName, _, _, _) = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: fileName, mimeType: contentType, directory: directory, account: session.account) metadata.contentType = mimeType metadata.iconName = iconName metadata.classFile = classFile @@ -486,24 +514,27 @@ extension NCManageDatabase { let fileName = fileName.trimmingCharacters(in: .whitespacesAndNewlines) - metadata.account = account + metadata.account = session.account metadata.creationDate = Date() as NSDate metadata.date = Date() as NSDate + metadata.directory = directory metadata.hasPreview = true metadata.etag = ocId metadata.fileName = fileName metadata.fileNameView = fileName metadata.name = name metadata.ocId = ocId - metadata.ocIdTemp = ocId + metadata.ocIdTransfer = ocId metadata.permissions = "RGDNVW" metadata.serverUrl = serverUrl + metadata.serveUrlFileName = serverUrl + "/" + fileName metadata.subline = subline metadata.uploadDate = Date() as NSDate metadata.url = url - metadata.urlBase = urlBase - metadata.user = user - metadata.userId = userId + metadata.urlBase = session.urlBase + metadata.user = session.user + metadata.userId = session.userId + metadata.sceneIdentifier = sceneIdentifier if !metadata.urlBase.isEmpty, metadata.serverUrl.hasPrefix(metadata.urlBase) { metadata.path = String(metadata.serverUrl.dropFirst(metadata.urlBase.count)) + "/" @@ -511,38 +542,40 @@ extension NCManageDatabase { return metadata } - @discardableResult - func addMetadata(_ metadata: tableMetadata) -> tableMetadata? { - let result = tableMetadata(value: metadata) + func isMetadataShareOrMounted(metadata: tableMetadata, metadataFolder: tableMetadata?) -> Bool { + let permissions = NCPermissions() + var isShare = false + var isMounted = false - do { - let realm = try Realm() - try realm.write { - realm.add(result, update: .all) - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - return nil + if metadataFolder != nil { + isShare = metadata.permissions.contains(permissions.permissionShared) && !metadataFolder!.permissions.contains(permissions.permissionShared) + isMounted = metadata.permissions.contains(permissions.permissionMounted) && !metadataFolder!.permissions.contains(permissions.permissionMounted) + } else if let directory = getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) { + isShare = metadata.permissions.contains(permissions.permissionShared) && !directory.permissions.contains(permissions.permissionShared) + isMounted = metadata.permissions.contains(permissions.permissionMounted) && !directory.permissions.contains(permissions.permissionMounted) + } + + if isShare || isMounted { + return true + } else { + return false } - return tableMetadata(value: result) } - func addMetadatasWithoutUpdate(_ metadatas: [tableMetadata]) { - if metadatas.isEmpty { return } + // MARK: - Set + @discardableResult + func addMetadata(_ metadata: tableMetadata) -> tableMetadata? { do { let realm = try Realm() - for metadata in metadatas { - if realm.objects(tableMetadata.self).filter("ocId == %@", metadata.ocId).first != nil { - return - } - try realm.write { - realm.add(metadata, update: .all) - } + try realm.write { + realm.add(tableMetadata(value: metadata), update: .all) } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + return nil } + return tableMetadata(value: metadata) } func addMetadatas(_ metadatas: [tableMetadata]) { @@ -568,41 +601,79 @@ extension NCManageDatabase { } } - func deleteMetadata(results: Results) { + func deleteMetadataOcId(_ ocId: String?) { + guard let ocId else { return } + do { let realm = try Realm() try realm.write { + let results = realm.objects(tableMetadata.self).filter("ocId == %@", ocId) realm.delete(results) } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } } - func moveMetadata(ocId: String, serverUrlTo: String) { + func deleteMetadatas(_ metadatas: [tableMetadata]) { do { let realm = try Realm() try realm.write { - if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first { - result.serverUrl = serverUrlTo - } + realm.delete(metadatas) } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } } - func renameMetadata(fileNameTo: String, ocId: String) { + func renameMetadata(fileNameNew: String, ocId: String, status: Int = NCGlobal.shared.metadataStatusNormal) { do { let realm = try Realm() try realm.write { if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first { - let resultsType = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: fileNameTo, mimeType: "", directory: result.directory) - result.fileName = fileNameTo - result.fileNameView = fileNameTo + let fileNameView = result.fileNameView + let fileIdMOV = result.livePhotoFile + let directoryServerUrl = self.utilityFileSystem.stringAppendServerUrl(result.serverUrl, addFileName: fileNameView) + let resultsType = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: fileNameNew, mimeType: "", directory: result.directory, account: result.account) + + result.fileName = fileNameNew + result.fileNameView = fileNameNew result.iconName = resultsType.iconName result.contentType = resultsType.mimeType result.classFile = resultsType.classFile + result.status = status + + if status == NCGlobal.shared.metadataStatusNormal { + result.sessionDate = nil + } else { + result.sessionDate = Date() + } + + if result.directory, + let resultDirectory = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", result.account, directoryServerUrl).first { + let serverUrlTo = self.utilityFileSystem.stringAppendServerUrl(result.serverUrl, addFileName: fileNameNew) + + resultDirectory.serverUrl = serverUrlTo + } else { + let atPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(result.ocId) + "/" + fileNameView + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(result.ocId) + "/" + fileNameNew + + self.utilityFileSystem.moveFile(atPath: atPath, toPath: toPath) + } + + if result.isLivePhoto, + let resultMOV = realm.objects(tableMetadata.self).filter("fileId == %@ AND account == %@", fileIdMOV, result.account).first { + let fileNameView = resultMOV.fileNameView + let fileName = (fileNameNew as NSString).deletingPathExtension + let ext = (resultMOV.fileName as NSString).pathExtension + resultMOV.fileName = fileName + "." + ext + resultMOV.fileNameView = fileName + "." + ext + + let atPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(resultMOV.ocId) + "/" + fileNameView + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(resultMOV.ocId) + "/" + fileName + "." + ext + + self.utilityFileSystem.moveFile(atPath: atPath, toPath: toPath) + } } } } catch let error { @@ -610,26 +681,79 @@ extension NCManageDatabase { } } - func setMetadataEtagResource(ocId: String, etagResource: String?) { - guard let etagResource = etagResource else { return } + func restoreMetadataFileName(ocId: String) { + do { + let realm = try Realm() + try realm.write { + if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first, + let encodedURLString = result.serveUrlFileName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: encodedURLString) { + let fileIdMOV = result.livePhotoFile + let directoryServerUrl = self.utilityFileSystem.stringAppendServerUrl(result.serverUrl, addFileName: result.fileNameView) + let lastPathComponent = url.lastPathComponent + let fileName = lastPathComponent.removingPercentEncoding ?? lastPathComponent + let fileNameView = result.fileNameView + + result.fileName = fileName + result.fileNameView = fileName + result.status = NCGlobal.shared.metadataStatusNormal + result.sessionDate = nil + + if result.directory, + let resultDirectory = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", result.account, directoryServerUrl).first { + let serverUrlTo = self.utilityFileSystem.stringAppendServerUrl(result.serverUrl, addFileName: fileName) + + resultDirectory.serverUrl = serverUrlTo + } else { + let atPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(result.ocId) + "/" + fileNameView + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(result.ocId) + "/" + fileName + + self.utilityFileSystem.moveFile(atPath: atPath, toPath: toPath) + } + + if result.isLivePhoto, + let resultMOV = realm.objects(tableMetadata.self).filter("fileId == %@ AND account == %@", fileIdMOV, result.account).first { + let fileNameView = resultMOV.fileNameView + let fileName = (fileName as NSString).deletingPathExtension + let ext = (resultMOV.fileName as NSString).pathExtension + resultMOV.fileName = fileName + "." + ext + resultMOV.fileNameView = fileName + "." + ext + + let atPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(resultMOV.ocId) + "/" + fileNameView + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(resultMOV.ocId) + "/" + fileName + "." + ext + + self.utilityFileSystem.moveFile(atPath: atPath, toPath: toPath) + } + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + func setMetadataServeUrlFileNameStatusNormal(ocId: String) { do { let realm = try Realm() try realm.write { - let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first - result?.etagResource = etagResource + if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first { + result.serveUrlFileName = self.utilityFileSystem.stringAppendServerUrl(result.serverUrl, addFileName: result.fileName) + result.status = NCGlobal.shared.metadataStatusNormal + result.sessionDate = nil + } } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } } - func setMetadataFavorite(ocId: String, favorite: Bool) { + func setMetadataEtagResource(ocId: String, etagResource: String?) { + guard let etagResource else { return } + do { let realm = try Realm() try realm.write { let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first - result?.favorite = favorite + result?.etagResource = etagResource } } catch let error { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") @@ -671,6 +795,24 @@ extension NCManageDatabase { } } + func updateMetadatasFiles(_ metadatas: [tableMetadata], serverUrl: String, account: String) { + do { + let realm = try Realm() + try realm.write { + let results = realm.objects(tableMetadata.self).filter(NSPredicate(format: "account == %@ AND serverUrl == %@ AND status == %d", account, serverUrl, NCGlobal.shared.metadataStatusNormal)) + realm.delete(results) + for metadata in metadatas { + if realm.objects(tableMetadata.self).filter(NSPredicate(format: "ocId == %@ AND status != %d", metadata.ocId, NCGlobal.shared.metadataStatusNormal)).first != nil { + continue + } + realm.add(tableMetadata(value: metadata), update: .all) + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + func setMetadataEncrypted(ocId: String, encrypted: Bool) { do { let realm = try Realm() @@ -695,82 +837,105 @@ extension NCManageDatabase { } } - func getMetadata(predicate: NSPredicate) -> tableMetadata? { + func moveMetadata(ocId: String, serverUrlTo: String) { do { let realm = try Realm() - realm.refresh() - guard let result = realm.objects(tableMetadata.self).filter(predicate).first else { return nil } - return tableMetadata(value: result) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + try realm.write { + if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first { + result.serverUrl = serverUrlTo + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } - - return nil } - func getMetadata(predicate: NSPredicate, sorted: String, ascending: Bool) -> tableMetadata? { + func clearAssetLocalIdentifiers(_ assetLocalIdentifiers: [String]) { do { let realm = try Realm() - realm.refresh() - guard let result = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sorted, ascending: ascending).first else { return nil } - return tableMetadata(value: result) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + try realm.write { + let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier IN %@", assetLocalIdentifiers) + for result in results { + result.assetLocalIdentifier = "" + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } - return nil } - func getMetadatas(predicate: NSPredicate) -> [tableMetadata] { + func setMetadataFavorite(ocId: String, favorite: Bool?, saveOldFavorite: String?, status: Int) { do { let realm = try Realm() - realm.refresh() - let results = realm.objects(tableMetadata.self).filter(predicate) - return Array(results.map { tableMetadata(value: $0) }) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + try realm.write { + let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first + if let favorite { + result?.favorite = favorite + } + result?.storeFlag = saveOldFavorite + result?.status = status + + if status == NCGlobal.shared.metadataStatusNormal { + result?.sessionDate = nil + } else { + result?.sessionDate = Date() + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } - return [] } - func getMetadatas(predicate: NSPredicate, sorted: String, ascending: Bool = false) -> [tableMetadata]? { + func setMetadataCopyMove(ocId: String, serverUrlTo: String, overwrite: String?, status: Int) { do { let realm = try Realm() - let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sorted, ascending: ascending) - return Array(results.map { tableMetadata(value: $0) }) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + try realm.write { + let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first + result?.serverUrlTo = serverUrlTo + result?.storeFlag = overwrite + result?.status = status + + if status == NCGlobal.shared.metadataStatusNormal { + result?.sessionDate = nil + } else { + result?.sessionDate = Date() + } + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") } - return nil } - func getResultsMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> Results? { + // MARK: - GetMetadata + + func getMetadata(predicate: NSPredicate) -> tableMetadata? { do { let realm = try Realm() - if let sorted { - return realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sorted, ascending: ascending) - } else { - return realm.objects(tableMetadata.self).filter(predicate) - } + guard let result = realm.objects(tableMetadata.self).filter(predicate).first else { return nil } + return tableMetadata(value: result) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } + return nil } - func getResultsMetadatas(predicate: NSPredicate, sorted: [RealmSwift.SortDescriptor]) -> Results? { + func getMetadatas(predicate: NSPredicate) -> [tableMetadata] { do { let realm = try Realm() - return realm.objects(tableMetadata.self).filter(predicate).sorted(by: sorted) + let results = realm.objects(tableMetadata.self).filter(predicate) + return Array(results.map { tableMetadata(value: $0) }) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } - return nil + return [] } - func getResultMetadata(predicate: NSPredicate) -> tableMetadata? { + func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, ascending: Bool = false) -> [tableMetadata]? { do { let realm = try Realm() - return realm.objects(tableMetadata.self).filter(predicate).first + let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sortedByKeyPath, ascending: ascending) + return Array(results.map { tableMetadata(value: $0) }) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } @@ -783,7 +948,6 @@ extension NCManageDatabase { do { let realm = try Realm() - realm.refresh() let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sorted, ascending: ascending) for result in results where counter < numItems { metadatas.append(tableMetadata(value: result)) @@ -795,28 +959,11 @@ extension NCManageDatabase { return metadatas } - func getMetadataAtIndex(predicate: NSPredicate, sorted: String, ascending: Bool, index: Int) -> tableMetadata? { - do { - let realm = try Realm() - realm.refresh() - let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sorted, ascending: ascending) - if results.isEmpty { - return nil - } else { - return tableMetadata(value: results[index]) - } - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") - } - return nil - } - func getMetadataFromOcId(_ ocId: String?) -> tableMetadata? { guard let ocId else { return nil } do { let realm = try Realm() - realm.refresh() guard let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first else { return nil } return tableMetadata(value: result) } catch let error as NSError { @@ -825,16 +972,15 @@ extension NCManageDatabase { return nil } - func getMetadataFromOcIdAndOcIdTemp(_ ocId: String?) -> tableMetadata? { + func getMetadataFromOcIdAndocIdTransfer(_ ocId: String?) -> tableMetadata? { guard let ocId else { return nil } do { let realm = try Realm() - realm.refresh() if let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first { return tableMetadata(value: result) } - if let result = realm.objects(tableMetadata.self).filter("ocIdTemp == %@", ocId).first { + if let result = realm.objects(tableMetadata.self).filter("ocIdTransfer == %@", ocId).first { return tableMetadata(value: result) } } catch let error as NSError { @@ -843,23 +989,40 @@ extension NCManageDatabase { return nil } - func getResultMetadataFromOcId(_ ocId: String?) -> tableMetadata? { - guard let ocId else { return nil } + func getMetadataFolder(session: NCSession.Session, serverUrl: String) -> tableMetadata? { + var serverUrl = serverUrl + var fileName = "" + let serverUrlHome = utilityFileSystem.getHomeServer(session: session) + + if serverUrlHome == serverUrl { + fileName = "." + serverUrl = ".." + } else { + fileName = (serverUrl as NSString).lastPathComponent + if let path = utilityFileSystem.deleteLastPath(serverUrlPath: serverUrl) { + serverUrl = path + } + } do { let realm = try Realm() - return realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first + guard let result = realm.objects(tableMetadata.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@", session.account, serverUrl, fileName).first else { return nil } + return tableMetadata(value: result) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - func getMetadataFromFileName(_ fileName: String, serverUrl: String) -> tableMetadata? { + func getMetadataLivePhoto(metadata: tableMetadata) -> tableMetadata? { + guard metadata.isLivePhoto else { return nil } + do { let realm = try Realm() - realm.refresh() - guard let result = realm.objects(tableMetadata.self).filter("fileName == %@ AND serverUrl == %@", fileName, serverUrl).first else { return nil } + guard let result = realm.objects(tableMetadata.self).filter(NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileId == %@", + metadata.account, + metadata.serverUrl, + metadata.livePhotoFile)).first else { return nil } return tableMetadata(value: result) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") @@ -867,70 +1030,101 @@ extension NCManageDatabase { return nil } - func getMetadataFromFileNameLocalPath(_ fileNameLocalPath: String?) -> tableMetadata? { - let components = fileNameLocalPath?.components(separatedBy: "/") + func getMetadataConflict(account: String, serverUrl: String, fileNameView: String) -> tableMetadata? { + let fileNameExtension = (fileNameView as NSString).pathExtension.lowercased() + let fileNameNoExtension = (fileNameView as NSString).deletingPathExtension + var fileNameConflict = fileNameView - if let count = components?.count, - components?.count ?? 0 > 2, - let ocId = components?[count - 2] { - do { - let realm = try Realm() - realm.refresh() - guard let result = realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first else { return nil } - return tableMetadata(value: result) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + if fileNameExtension == "heic", NCKeychain().formatCompatibility { + fileNameConflict = fileNameNoExtension + ".jpg" + } + return getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameView == %@", + account, + serverUrl, + fileNameConflict)) + } + + // MARK: - GetResult(s)Metadata + + func getResultsMetadatasPredicate(_ predicate: NSPredicate, layoutForView: NCDBLayoutForView?) -> Results? { + do { + let realm = try Realm() + var results = realm.objects(tableMetadata.self).filter(predicate) + if let layoutForView { + if layoutForView.directoryOnTop { + results = results.sorted(byKeyPath: layoutForView.sort, ascending: layoutForView.ascending).sorted(byKeyPath: "directory", ascending: false).sorted(byKeyPath: "favorite", ascending: false) + } else { + results = results.sorted(byKeyPath: layoutForView.sort, ascending: layoutForView.ascending).sorted(byKeyPath: "favorite", ascending: false) + } } + return results + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - func getTableMetadataFromOcId(_ ocId: String?) -> tableMetadata? { - guard let ocId else { return nil } + func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String, ascending: Bool, arraySlice: Int) -> [tableMetadata] { + do { + let realm = try Realm() + let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sortedByKeyPath, ascending: ascending).prefix(arraySlice) + return Array(results) + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + } + return [] + } + func getResultMetadata(predicate: NSPredicate) -> tableMetadata? { do { let realm = try Realm() - realm.refresh() - return realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first + return realm.objects(tableMetadata.self).filter(predicate).first } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - func getMetadataFromFileId(_ fileId: String?) -> tableMetadata? { + func getResultMetadataFromFileName(_ fileName: String, serverUrl: String, sessionTaskIdentifier: Int) -> tableMetadata? { do { let realm = try Realm() - realm.refresh() - guard let fileId = fileId else { return nil } - guard let result = realm.objects(tableMetadata.self).filter("fileId == %@", fileId).first else { return nil } - return tableMetadata(value: result) + return realm.objects(tableMetadata.self).filter("fileName == %@ AND serverUrl == %@ AND sessionTaskIdentifier == %d", fileName, serverUrl, sessionTaskIdentifier).first } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - func getMetadataFolder(account: String, urlBase: String, userId: String, serverUrl: String) -> tableMetadata? { - var serverUrl = serverUrl - var fileName = "" - let serverUrlHome = utilityFileSystem.getHomeServer(urlBase: urlBase, userId: userId) + func getResultsMetadatasFromGroupfolders(session: NCSession.Session) -> Results? { + var ocId: [String] = [] + let homeServerUrl = utilityFileSystem.getHomeServer(session: session) - if serverUrlHome == serverUrl { - fileName = "." - serverUrl = ".." - } else { - fileName = (serverUrl as NSString).lastPathComponent - if let path = utilityFileSystem.deleteLastPath(serverUrlPath: serverUrl) { - serverUrl = path + do { + let realm = try Realm() + let groupfolders = realm.objects(TableGroupfolders.self).filter("account == %@", session.account).sorted(byKeyPath: "mountPoint", ascending: true) + + for groupfolder in groupfolders { + let mountPoint = groupfolder.mountPoint.hasPrefix("/") ? groupfolder.mountPoint : "/" + groupfolder.mountPoint + let serverUrlFileName = homeServerUrl + mountPoint + + if let directory = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", session.account, serverUrlFileName).first, + let result = realm.objects(tableMetadata.self).filter("ocId == %@", directory.ocId).first { + ocId.append(result.ocId) + } } + + return realm.objects(tableMetadata.self).filter("ocId IN %@", ocId) + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } + return nil + } + + func getResultsImageCacheMetadatas(predicate: NSPredicate) -> Results? { do { let realm = try Realm() - realm.refresh() - guard let result = realm.objects(tableMetadata.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName).first else { return nil } - return tableMetadata(value: result) + return realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: "date", ascending: false) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } @@ -943,7 +1137,6 @@ extension NCManageDatabase { do { let realm = try Realm() - realm.refresh() let results = realm.objects(tableMetadata.self).filter("account == %@ AND directory == true AND favorite == true", account).sorted(byKeyPath: "fileNameView", ascending: true) for result in results { counter += 1 @@ -955,7 +1148,7 @@ extension NCManageDatabase { return listIdentifierRank } - @objc func clearMetadatasUpload(account: String) { + func clearMetadatasUpload(account: String) { do { let realm = try Realm() try realm.write { @@ -967,26 +1160,12 @@ extension NCManageDatabase { } } - func readMarkerMetadata(account: String, fileId: String) { - do { - let realm = try Realm() - try realm.write { - let results = realm.objects(tableMetadata.self).filter("account == %@ AND fileId == %@", account, fileId) - for result in results { - result.commentsUnread = false - } - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - - func getAssetLocalIdentifiersUploaded(account: String) -> [String]? { + func getAssetLocalIdentifiersUploaded() -> [String]? { var assetLocalIdentifiers: [String] = [] do { let realm = try Realm() - let results = realm.objects(tableMetadata.self).filter("account == %@ AND assetLocalIdentifier != ''", account) + let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier != ''") for result in results { assetLocalIdentifiers.append(result.assetLocalIdentifier) } @@ -997,179 +1176,67 @@ extension NCManageDatabase { return nil } - func clearAssetLocalIdentifiers(_ assetLocalIdentifiers: [String], account: String) { - do { - let realm = try Realm() - try realm.write { - let results = realm.objects(tableMetadata.self).filter("account == %@ AND assetLocalIdentifier IN %@", account, assetLocalIdentifiers) - for result in results { - result.assetLocalIdentifier = "" - } - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - - func getMetadataLivePhoto(metadata: tableMetadata) -> tableMetadata? { - guard metadata.isLivePhoto else { return nil } - + func getMetadataFromDirectory(account: String, serverUrl: String) -> Bool { do { let realm = try Realm() - guard let result = realm.objects(tableMetadata.self).filter(NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileId == %@", metadata.account, metadata.serverUrl, metadata.livePhotoFile)).first else { return nil } - return tableMetadata(value: result) - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") - } - return nil - } - - func isMetadataShareOrMounted(metadata: tableMetadata, metadataFolder: tableMetadata?) -> Bool { - let permissions = NCPermissions() - var isShare = false - var isMounted = false - - if metadataFolder != nil { - isShare = metadata.permissions.contains(permissions.permissionShared) && !metadataFolder!.permissions.contains(permissions.permissionShared) - isMounted = metadata.permissions.contains(permissions.permissionMounted) && !metadataFolder!.permissions.contains(permissions.permissionMounted) - } else if let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) { - isShare = metadata.permissions.contains(permissions.permissionShared) && !directory.permissions.contains(permissions.permissionShared) - isMounted = metadata.permissions.contains(permissions.permissionMounted) && !directory.permissions.contains(permissions.permissionMounted) - } - - if isShare || isMounted { + guard let directory = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", account, serverUrl).first, + realm.objects(tableMetadata.self).filter("ocId == %@", directory.ocId).first != nil else { return false } return true - } else { - return false - } - } - - func getMetadataConflict(account: String, serverUrl: String, fileNameView: String) -> tableMetadata? { - // verify exists conflict - let fileNameExtension = (fileNameView as NSString).pathExtension.lowercased() - let fileNameNoExtension = (fileNameView as NSString).deletingPathExtension - var fileNameConflict = fileNameView - - if fileNameExtension == "heic", NCKeychain().formatCompatibility { - fileNameConflict = fileNameNoExtension + ".jpg" - } - return getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameView == %@", account, serverUrl, fileNameConflict)) - } - - func getNumMetadatasInUpload() -> Int { - do { - let realm = try Realm() - realm.refresh() - return realm.objects(tableMetadata.self).filter(NSPredicate(format: "status == %i", NCGlobal.shared.metadataStatusUploading)).count } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } - return 0 + return false } - func getMetadataFromDirectory(account: String, serverUrl: String) -> tableMetadata? { + func getResultMetadataFromFileId(_ fileId: String?) -> tableMetadata? { + guard let fileId else { return nil } + do { let realm = try Realm() - realm.refresh() - guard let directory = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", account, serverUrl).first else { return nil } - guard let result = realm.objects(tableMetadata.self).filter("ocId == %@", directory.ocId).first else { return nil } - return tableMetadata(value: result) + return realm.objects(tableMetadata.self).filter("fileId == %@", fileId).first } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - func getMetadatasFromGroupfolders(account: String, urlBase: String, userId: String) -> [tableMetadata] { - var metadatas: [tableMetadata] = [] - let homeServerUrl = utilityFileSystem.getHomeServer(urlBase: urlBase, userId: userId) - - do { - let realm = try Realm() - realm.refresh() - let groupfolders = realm.objects(TableGroupfolders.self).filter("account == %@", account) - for groupfolder in groupfolders { - let mountPoint = groupfolder.mountPoint.hasPrefix("/") ? groupfolder.mountPoint : "/" + groupfolder.mountPoint - let serverUrlFileName = homeServerUrl + mountPoint - if let directory = realm.objects(tableDirectory.self).filter("account == %@ AND serverUrl == %@", account, serverUrlFileName).first, - let metadata = realm.objects(tableMetadata.self).filter("ocId == %@", directory.ocId).first { - metadatas.append(tableMetadata(value: metadata)) - } - } - } catch let error as NSError { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") - } - return metadatas - } + func getResultMetadataFromOcId(_ ocId: String?) -> tableMetadata? { + guard let ocId else { return nil } - func getMetadatasInError(account: String) -> Results? { do { let realm = try Realm() - let results = realm.objects(tableMetadata.self).filter("account == %@ AND errorCodeCounter > 1", account) - return results + return realm.objects(tableMetadata.self).filter("ocId == %@", ocId).first } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } - @discardableResult - func updateMetadatas(_ metadatas: [tableMetadata], predicate: NSPredicate) -> (metadatasDifferentCount: Int, metadatasModified: Int) { - var metadatasDifferentCount: Int = 0 - var metadatasModified: Int = 0 - + func getResultsMetadatas(predicate: NSPredicate, sortedByKeyPath: String? = nil, ascending: Bool = false, freeze: Bool = false) -> Results? { do { let realm = try Realm() - try realm.write { - let results = realm.objects(tableMetadata.self).filter(predicate) - metadatasDifferentCount = metadatas.count - results.count - for metadata in metadatas { - if let result = results.first(where: { $0.ocId == metadata.ocId }) { - // before realm.add copy the value not available from server - metadata.assetLocalIdentifier = result.assetLocalIdentifier - if !metadata.isEqual(result) { metadatasModified += 1 } - } - } - if metadatasDifferentCount != 0 || metadatasModified > 0 { - realm.delete(results) - for metadata in metadatas { - realm.add(tableMetadata(value: metadata), update: .all) - } + realm.refresh() + if let sortedByKeyPath { + let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sortedByKeyPath, ascending: ascending) + if freeze { + return results.freeze() } - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - return (metadatasDifferentCount, metadatasModified) - } - - func replaceMetadata(_ metadatas: [tableMetadata], predicate: NSPredicate) { - do { - let realm = try Realm() - try realm.write { + return results + } else { let results = realm.objects(tableMetadata.self).filter(predicate) - realm.delete(results) - for metadata in metadatas { - if results.where({ $0.ocId == metadata.ocId }).isEmpty { - realm.add(tableMetadata(value: metadata), update: .modified) - } else { - continue - } + if freeze { + return results.freeze() } + return results } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - - func getMediaMetadatas(predicate: NSPredicate) -> ThreadSafeArray? { - do { - let realm = try Realm() - let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: "date", ascending: false) - return ThreadSafeArray(results.map { tableMetadata(value: $0) }) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } return nil } + + func getCalculateCumulativeHash(for metadatas: [tableMetadata], account: String, serverUrl: String) -> String { + let concatenatedEtags = metadatas.map { $0.etag }.joined(separator: "-") + return sha256Hash(concatenatedEtags) + } } diff --git a/iOSClient/Data/NCManageDatabase+SecurityGuard.swift b/iOSClient/Data/NCManageDatabase+SecurityGuard.swift index 647ef65592..f48a19f855 100644 --- a/iOSClient/Data/NCManageDatabase+SecurityGuard.swift +++ b/iOSClient/Data/NCManageDatabase+SecurityGuard.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Share.swift b/iOSClient/Data/NCManageDatabase+Share.swift index 9618a80ad6..c518597d42 100644 --- a/iOSClient/Data/NCManageDatabase+Share.swift +++ b/iOSClient/Data/NCManageDatabase+Share.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Tag.swift b/iOSClient/Data/NCManageDatabase+Tag.swift index b92db45f0d..264313700b 100644 --- a/iOSClient/Data/NCManageDatabase+Tag.swift +++ b/iOSClient/Data/NCManageDatabase+Tag.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Tip.swift b/iOSClient/Data/NCManageDatabase+Tip.swift index 163a8d2a36..eb449d30ea 100644 --- a/iOSClient/Data/NCManageDatabase+Tip.swift +++ b/iOSClient/Data/NCManageDatabase+Tip.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Trash.swift b/iOSClient/Data/NCManageDatabase+Trash.swift index a34b21cf77..f152c72618 100644 --- a/iOSClient/Data/NCManageDatabase+Trash.swift +++ b/iOSClient/Data/NCManageDatabase+Trash.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit @@ -113,24 +114,22 @@ extension NCManageDatabase { } } - func getTrash(filePath: String, account: String) -> [tableTrash] { + func getResultsTrash(filePath: String, account: String) -> Results? { do { let realm = try Realm() realm.refresh() - let results = realm.objects(tableTrash.self).filter("account == %@ AND filePath == %@", account, filePath).sorted(byKeyPath: "trashbinDeletionTime", ascending: false) - return Array(results.map { tableTrash.init(value: $0) }) + return realm.objects(tableTrash.self).filter("account == %@ AND filePath == %@", account, filePath).sorted(byKeyPath: "trashbinDeletionTime", ascending: false) } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access to database: \(error)") } - return [] + return nil } - func getTrashItem(fileId: String, account: String) -> tableTrash? { + func getResultTrashItem(fileId: String, account: String) -> tableTrash? { do { let realm = try Realm() realm.refresh() - guard let result = realm.objects(tableTrash.self).filter("account == %@ AND fileId == %@", account, fileId).first else { return nil } - return tableTrash.init(value: result) + return realm.objects(tableTrash.self).filter("account == %@ AND fileId == %@", account, fileId).first } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access to database: \(error)") } diff --git a/iOSClient/Data/NCManageDatabase+UserStatus.swift b/iOSClient/Data/NCManageDatabase+UserStatus.swift index 67babd1c31..ed71dc7e85 100644 --- a/iOSClient/Data/NCManageDatabase+UserStatus.swift +++ b/iOSClient/Data/NCManageDatabase+UserStatus.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase+Video.swift b/iOSClient/Data/NCManageDatabase+Video.swift index 9034b2cddb..030c34443a 100644 --- a/iOSClient/Data/NCManageDatabase+Video.swift +++ b/iOSClient/Data/NCManageDatabase+Video.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import RealmSwift import NextcloudKit diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index 466173bcc4..2934948612 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -27,6 +27,7 @@ import RealmSwift import NextcloudKit import CoreMedia import Photos +import CommonCrypto protocol DateCompareable { var dateKey: Date { get } @@ -41,17 +42,20 @@ class NCManageDatabase: NSObject { override init() { func migrationSchema(_ migration: Migration, _ oldSchemaVersion: UInt64) { - if oldSchemaVersion < 354 { - migration.deleteData(forType: NCDBLayoutForView.className()) + if oldSchemaVersion < 365 { + migration.deleteData(forType: tableMetadata.className()) + migration.enumerateObjects(ofType: tableDirectory.className()) { _, newObject in + newObject?["etag"] = "" + } } } func compactDB(_ totalBytes: Int, _ usedBytes: Int) -> Bool { - // totalBytes refers to the size of the file on disk in bytes (data + free space) - // usedBytes refers to the number of bytes used by data in the file - // Compact if the file is over 100MB in size and less than 50% 'used' - let oneHundredMB = 100 * 1024 * 1024 - return (totalBytes > oneHundredMB) && (Double(usedBytes) / Double(totalBytes)) < 0.5 + let usedPercentage = (Double(usedBytes) / Double(totalBytes)) * 100 + /// Compact the database if more than 25% of the space is free + let shouldCompact = (usedPercentage < 75.0) && (totalBytes > 100 * 1024 * 1024) + + return shouldCompact } var realm: Realm? let dirGroup = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: NCBrandOptions.shared.capabilitiesGroup) @@ -60,7 +64,8 @@ class NCManageDatabase: NSObject { let bundlePathExtension: String = bundleUrl.pathExtension let bundleFileName: String = (bundleUrl.path as NSString).lastPathComponent let isAppex: Bool = bundlePathExtension == "appex" - var objectTypesAppex = [tableMetadata.self, + var objectTypesAppex = [NCKeyValue.self, + tableMetadata.self, tableLocalFile.self, tableDirectory.self, tableTag.self, @@ -94,12 +99,14 @@ class NCManageDatabase: NSObject { if isAppex { if bundleFileName == "File Provider Extension.appex" { - objectTypesAppex = [tableMetadata.self, + objectTypesAppex = [NCKeyValue.self, + tableMetadata.self, tableLocalFile.self, tableDirectory.self, tableTag.self, tableAccount.self, - tableCapabilities.self] + tableCapabilities.self, + tableE2eEncryption.self] } do { Realm.Configuration.defaultConfiguration = @@ -145,7 +152,6 @@ class NCManageDatabase: NSObject { let realm = try Realm() try realm.write { var results: Results - if let account = account { results = realm.objects(table).filter("account == %@", account) } else { @@ -159,7 +165,7 @@ class NCManageDatabase: NSObject { } } - func clearDatabase(account: String?, removeAccount: Bool) { + func clearDatabase(account: String? = nil, removeAccount: Bool = false) { if removeAccount { self.clearTable(tableAccount.self, account: account) } @@ -216,6 +222,24 @@ class NCManageDatabase: NSObject { return nil } + func realmRefresh() { + do { + let realm = try Realm() + realm.refresh() + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not refresh database: \(error)") + } + } + + func sha256Hash(_ input: String) -> String { + let data = Data(input.utf8) + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &digest) + } + return digest.map { String(format: "%02hhx", $0) }.joined() + } + // MARK: - // MARK: Func T @@ -250,17 +274,22 @@ class NCManageDatabase: NSObject { /// Account let account = "marinofaggiana https://cloudtest.nextcloud.com" let account2 = "mariorossi https://cloudtest.nextcloud.com" - NCManageDatabase.shared.addAccount(account, urlBase: "https://cloudtest.nextcloud.com", user: "marinofaggiana", userId: "marinofaggiana", password: "password") - NCManageDatabase.shared.addAccount(account2, urlBase: "https://cloudtest.nextcloud.com", user: "mariorossi", userId: "mariorossi", password: "password") + addAccount(account, urlBase: "https://cloudtest.nextcloud.com", user: "marinofaggiana", userId: "marinofaggiana", password: "password") + addAccount(account2, urlBase: "https://cloudtest.nextcloud.com", user: "mariorossi", userId: "mariorossi", password: "password") let userProfile = NKUserProfile() userProfile.displayName = "Marino Faggiana" userProfile.address = "Hirschstrasse 26, 70192 Stuttgart, Germany" userProfile.phone = "+49 (711) 252 428 - 90" userProfile.email = "cloudtest@nextcloud.com" - NCManageDatabase.shared.setAccountUserProfile(account: account, userProfile: userProfile) + setAccountUserProfile(account: account, userProfile: userProfile) let userProfile2 = NKUserProfile() userProfile2.displayName = "Mario Rossi" userProfile2.email = "cloudtest@nextcloud.com" - NCManageDatabase.shared.setAccountUserProfile(account: account2, userProfile: userProfile2) + setAccountUserProfile(account: account2, userProfile: userProfile2) } } + +class NCKeyValue: Object { + @Persisted var key: String = "" + @Persisted var value: String? +} diff --git a/iOSClient/DeepLink/NCDeepLinkHandler.swift b/iOSClient/DeepLink/NCDeepLinkHandler.swift index cca6971d63..bc0759767f 100644 --- a/iOSClient/DeepLink/NCDeepLinkHandler.swift +++ b/iOSClient/DeepLink/NCDeepLinkHandler.swift @@ -103,6 +103,7 @@ class NCDeepLinkHandler { controller.selectedIndex = ControllerConstants.filesIndex guard let navigationController = controller.viewControllers?[controller.selectedIndex] as? UINavigationController, let viewController = UIStoryboard(name: ControllerConstants.notification, bundle: nil).instantiateInitialViewController() as? NCNotification else { return } + viewController.session = NCSession.shared.getSession(controller: controller) navigationController.pushViewController(viewController, animated: true) } @@ -111,10 +112,11 @@ class NCDeepLinkHandler { controller.selectedIndex = ControllerConstants.filesIndex DispatchQueue.main.asyncAfter(deadline: .now() + 4) { let serverUrl = controller.currentServerUrl() - let fileFolderPath = NCUtilityFileSystem().getFileNamePath("", serverUrl: serverUrl, urlBase: appDelegate.urlBase, userId: appDelegate.userId) + let session = NCSession.shared.getSession(controller: controller) + let fileFolderPath = NCUtilityFileSystem().getFileNamePath("", serverUrl: serverUrl, session: session) let fileFolderName = (serverUrl as NSString).lastPathComponent - if !FileNameValidator.shared.checkFolderPath(folderPath: fileFolderPath) { + if !FileNameValidator.shared.checkFolderPath(fileFolderPath, account: controller.account) { controller.present(UIAlertController.warning(message: "\(String(format: NSLocalizedString("_file_name_validator_error_reserved_name_", comment: ""), fileFolderName)) \(NSLocalizedString("_please_rename_file_", comment: ""))"), animated: true) return diff --git a/iOSClient/Extensions/NotificationCenter+MainThread.swift b/iOSClient/Extensions/NotificationCenter+MainThread.swift index bbc33ba521..3286a76f50 100644 --- a/iOSClient/Extensions/NotificationCenter+MainThread.swift +++ b/iOSClient/Extensions/NotificationCenter+MainThread.swift @@ -25,7 +25,6 @@ import Foundation import UIKit extension NotificationCenter { - func postOnMainThread(name: String, object anObject: Any? = nil, userInfo aUserInfo: [AnyHashable: Any]? = nil, second: Double = 0) { DispatchQueue.main.asyncAfter(deadline: .now() + second) { NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: anObject, userInfo: aUserInfo) diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 97357bb94b..f808e585a7 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -32,16 +32,21 @@ extension UIAlertController { /// - urlBase: UrlBase object /// - completion: If not` nil` it overrides the default behavior which shows an error using `NCContentPresenter` /// - Returns: The presentable alert controller - static func createFolder(serverUrl: String, userBaseUrl: NCUserBaseUrl, markE2ee: Bool = false, sceneIdentifier: String? = nil, completion: ((_ error: NKError) -> Void)? = nil) -> UIAlertController { + static func createFolder(serverUrl: String, account: String, markE2ee: Bool = false, sceneIdentifier: String? = nil, completion: ((_ error: NKError) -> Void)? = nil) -> UIAlertController { let alertController = UIAlertController(title: NSLocalizedString("_create_folder_", comment: ""), message: nil, preferredStyle: .alert) + let session = NCSession.shared.getSession(account: account) + let isDirectoryEncrypted = NCUtilityFileSystem().isDirectoryE2EE(session: session, serverUrl: serverUrl) let okAction = UIAlertAction(title: NSLocalizedString("_save_", comment: ""), style: .default, handler: { _ in guard let fileNameFolder = alertController.textFields?.first?.text else { return } if markE2ee { + if NCNetworking.shared.isOffline { + return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + } Task { - let createFolderResults = await NCNetworking.shared.createFolder(serverUrlFileName: serverUrl + "/" + fileNameFolder, account: userBaseUrl.account) + let createFolderResults = await NCNetworking.shared.createFolder(serverUrlFileName: serverUrl + "/" + fileNameFolder, account: session.account) if createFolderResults.error == .success { - let error = await NCNetworkingE2EEMarkFolder().markFolderE2ee(account: userBaseUrl.account, fileName: fileNameFolder, serverUrl: serverUrl, userId: userBaseUrl.userId) + let error = await NCNetworkingE2EEMarkFolder().markFolderE2ee(account: session.account, fileName: fileNameFolder, serverUrl: serverUrl, userId: session.userId) if error != .success { NCContentPresenter().showError(error: error) } @@ -49,14 +54,29 @@ extension UIAlertController { NCContentPresenter().showError(error: createFolderResults.error) } } - } else { - NCNetworking.shared.createFolder(fileName: fileNameFolder, serverUrl: serverUrl, account: userBaseUrl.account, urlBase: userBaseUrl.urlBase, userId: userBaseUrl.userId, overwrite: false, withPush: true, sceneIdentifier: sceneIdentifier) { error in - if let completion = completion { - completion(error) - } else if error != .success { - NCContentPresenter().showError(error: error) - } // else: successful, no action + } else if isDirectoryEncrypted { + if NCNetworking.shared.isOffline { + return NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_offline_not_allowed_")) + } + #if !EXTENSION + Task { + await NCNetworkingE2EECreateFolder().createFolder(fileName: fileNameFolder, serverUrl: serverUrl, withPush: true, sceneIdentifier: sceneIdentifier, session: session) } + #endif + } else { + let metadataForCreateFolder = NCManageDatabase.shared.createMetadata(fileName: fileNameFolder, + fileNameView: fileNameFolder, + ocId: NSUUID().uuidString, + serverUrl: serverUrl, + url: "", + contentType: "httpd/unix-directory", + directory: true, + session: session, + sceneIdentifier: sceneIdentifier) + metadataForCreateFolder.status = NCGlobal.shared.metadataStatusWaitCreateFolder + metadataForCreateFolder.sessionDate = Date() + NCManageDatabase.shared.addMetadata(metadataForCreateFolder) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateFolder, userInfo: ["ocId": metadataForCreateFolder.ocId, "serverUrl": metadataForCreateFolder.serverUrl, "account": metadataForCreateFolder.account, "withPush": true, "sceneIdentifier": sceneIdentifier as Any]) } }) @@ -76,7 +96,7 @@ extension UIAlertController { guard let text = alertController.textFields?.first?.text else { return } let folderName = text.trimmingCharacters(in: .whitespaces) - let textCheck = FileNameValidator.shared.checkFileName(folderName) + let textCheck = FileNameValidator.shared.checkFileName(folderName, account: account) okAction.isEnabled = textCheck?.error == nil && !folderName.isEmpty alertController.message = textCheck?.error.localizedDescription } @@ -107,24 +127,15 @@ extension UIAlertController { }, completion: completion) } - static func deleteFileOrFolder(titleString: String, message: String?, canDeleteServer: Bool, selectedMetadatas: [tableMetadata], completion: @escaping (_ cancelled: Bool) -> Void) -> UIAlertController { + static func deleteFileOrFolder(titleString: String, message: String?, canDeleteServer: Bool, selectedMetadatas: [tableMetadata], sceneIdentifier: String?, completion: @escaping (_ cancelled: Bool) -> Void) -> UIAlertController { let alertController = UIAlertController( title: titleString, message: message, preferredStyle: .alert) if canDeleteServer { alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .destructive) { (_: UIAlertAction) in - Task { - var error = NKError() - var ocId: [String] = [] - for metadata in selectedMetadatas where error == .success { - error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: false) - if error == .success { - ocId.append(metadata.ocId) - } - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "onlyLocalCache": false, "error": error]) - } + NCNetworking.shared.deleteMetadatas(selectedMetadatas, sceneIdentifier: sceneIdentifier) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) completion(false) }) } @@ -134,15 +145,12 @@ extension UIAlertController { var error = NKError() var ocId: [String] = [] for metadata in selectedMetadatas where error == .success { - error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: true) + error = await NCNetworking.shared.deleteCache(metadata, sceneIdentifier: sceneIdentifier) if error == .success { ocId.append(metadata.ocId) } } - if error != .success { - NCContentPresenter().showError(error: error) - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "onlyLocalCache": true, "error": error]) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "error": error]) } completion(false) }) @@ -153,7 +161,7 @@ extension UIAlertController { return alertController } - static func renameFile(fileName: String, completion: @escaping (_ newFileName: String) -> Void) -> UIAlertController { + static func renameFile(fileName: String, account: String, completion: @escaping (_ newFileName: String) -> Void) -> UIAlertController { let alertController = UIAlertController(title: NSLocalizedString("_rename_", comment: ""), message: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: NSLocalizedString("_save_", comment: ""), style: .default, handler: { _ in @@ -190,7 +198,7 @@ extension UIAlertController { queue: .main) { _ in guard let text = alertController.textFields?.first?.text else { return } - let textCheck = FileNameValidator.shared.checkFileName(text) + let textCheck = FileNameValidator.shared.checkFileName(text, account: account) okAction.isEnabled = textCheck?.error == nil && !text.isEmpty alertController.message = textCheck?.error.localizedDescription } @@ -200,28 +208,21 @@ extension UIAlertController { return alertController } - static func renameFile(metadata: tableMetadata, indexPath: IndexPath) -> UIAlertController { + static func renameFile(metadata: tableMetadata) -> UIAlertController { let alertController = UIAlertController(title: NSLocalizedString("_rename_", comment: ""), message: nil, preferredStyle: .alert) let okAction = UIAlertAction(title: NSLocalizedString("_save_", comment: ""), style: .default, handler: { _ in - guard let newFileName = alertController.textFields?.first?.text else { return } + guard let fileNameNew = alertController.textFields?.first?.text else { return } // verify if already exists - if NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, newFileName)) != nil { + if NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, fileNameNew)) != nil { NCContentPresenter().showError(error: NKError(errorCode: 0, errorDescription: "_rename_already_exists_")) return } - NCActivityIndicator.shared.start() - - NCNetworking.shared.renameMetadata(metadata, fileNameNew: newFileName, indexPath: indexPath) { error in + NCNetworking.shared.renameMetadata(metadata, fileNameNew: fileNameNew) - NCActivityIndicator.shared.stop() - - if error != .success { - NCContentPresenter().showError(error: error) - } - } + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource, userInfo: ["serverUrl": metadata.serverUrl]) }) // text field is initially empty, no action @@ -229,7 +230,7 @@ extension UIAlertController { let cancelAction = UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) alertController.addTextField { textField in - textField.text = metadata.fileName + textField.text = metadata.fileNameView textField.autocapitalizationType = .words } @@ -252,7 +253,7 @@ extension UIAlertController { queue: .main) { _ in guard let text = alertController.textFields?.first?.text else { return } - let textCheck = FileNameValidator.shared.checkFileName(text) + let textCheck = FileNameValidator.shared.checkFileName(text, account: NCManageDatabase.shared.getActiveTableAccount()?.account) okAction.isEnabled = textCheck?.error == nil && !text.isEmpty alertController.message = textCheck?.error.localizedDescription } diff --git a/iOSClient/Extensions/UIImage+Extension.swift b/iOSClient/Extensions/UIImage+Extension.swift index d24e907d9a..3ea4d04296 100644 --- a/iOSClient/Extensions/UIImage+Extension.swift +++ b/iOSClient/Extensions/UIImage+Extension.swift @@ -26,9 +26,7 @@ import UIKit import Accelerate extension UIImage { - - @objc func resizeImage(size: CGSize, isAspectRation: Bool = true) -> UIImage? { - + func resizeImage(size: CGSize, isAspectRation: Bool = true) -> UIImage? { let originRatio = self.size.width / self.size.height let newRatio = size.width / size.height var newSize = size @@ -44,13 +42,9 @@ extension UIImage { } UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0) - self.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) - let newImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - if let image = newImage { - return image - } - return self + self.draw(in: CGRect(origin: .zero, size: newSize)) + defer { UIGraphicsEndImageContext() } + return UIGraphicsGetImageFromCurrentImageContext() } func fixedOrientation() -> UIImage? { diff --git a/iOSClient/Favorites/NCFavorite.swift b/iOSClient/Favorites/NCFavorite.swift index 6dbf0324a2..9386d0ec33 100644 --- a/iOSClient/Favorites/NCFavorite.swift +++ b/iOSClient/Favorites/NCFavorite.swift @@ -33,7 +33,8 @@ class NCFavorite: NCCollectionViewCommon { layoutKey = NCGlobal.shared.layoutViewFavorite enableSearchBar = false headerRichWorkspaceDisable = true - emptyImage = utility.loadImage(named: "star.fill", colors: [NCBrandColor.shared.yellowFavorite]) + emptyImageName = "star.fill" + emptyImageColors = [NCBrandColor.shared.yellowFavorite] emptyTitle = "_favorite_no_files_" emptyDescription = "_tutorial_favorite_view_" } @@ -43,43 +44,44 @@ class NCFavorite: NCCollectionViewCommon { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if dataSource.metadatas.isEmpty { - reloadDataSource() - } - reloadDataSourceNetwork() + reloadDataSource() } - // MARK: - DataSource + NC Endpoint + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + getServerData() + } - override func queryDB() { - super.queryDB() + // MARK: - DataSource - var metadatas: [tableMetadata] = [] + override func reloadDataSource() { + var predicate = self.defaultPredicate if self.serverUrl.isEmpty { - metadatas = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND favorite == true", self.appDelegate.account)) - } else { - metadatas = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", self.appDelegate.account, self.serverUrl)) + predicate = NSPredicate(format: "account == %@ AND favorite == true", session.account) } - self.dataSource = NCDataSource(metadatas: metadatas, account: self.appDelegate.account, layoutForView: layoutForView, providers: self.providers, searchResults: self.searchResults) - } + let results = self.database.getResultsMetadatasPredicate(predicate, layoutForView: layoutForView) + self.dataSource = NCCollectionViewDataSource(results: results, layoutForView: layoutForView) - override func reloadDataSourceNetwork(withQueryDB: Bool = false) { - super.reloadDataSourceNetwork() + super.reloadDataSource() + } - NextcloudKit.shared.listingFavorites(showHiddenFiles: NCKeychain().showHiddenFiles, account: self.appDelegate.account, options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { task in + override func getServerData() { + NextcloudKit.shared.listingFavorites(showHiddenFiles: NCKeychain().showHiddenFiles, account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + if self.dataSource.isEmpty() { + self.collectionView.reloadData() + } } completion: { account, files, _, error in - if error == .success { - NCManageDatabase.shared.convertFilesToMetadatas(files, useFirstAsMetadataFolder: false) { _, metadatas in - NCManageDatabase.shared.updateMetadatasFavorite(account: account, metadatas: metadatas) - self.reloadDataSource() + if error == .success, let files { + self.database.convertFilesToMetadatas(files, useFirstAsMetadataFolder: false) { _, metadatas in + self.database.updateMetadatasFavorite(account: account, metadatas: metadatas) } - } else { - self.reloadDataSource(withQueryDB: withQueryDB) + self.reloadDataSource() } + self.refreshControl.endRefreshing() } } } diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index 3fd5d3a08e..18b9fb83fd 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -23,11 +23,13 @@ import UIKit import NextcloudKit +import RealmSwift class NCFiles: NCCollectionViewCommon { internal var isRoot: Bool = true internal var fileNameBlink: String? internal var fileNameOpen: String? + internal var matadatasHash: String = "" required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) @@ -37,7 +39,6 @@ class NCFiles: NCCollectionViewCommon { enableSearchBar = true headerRichWorkspaceDisable = false headerMenuTransferView = true - emptyImage = NCImageCache.images.folder emptyTitle = "_files_no_files_" emptyDescription = "_no_file_pull_down_" } @@ -48,44 +49,65 @@ class NCFiles: NCCollectionViewCommon { super.viewDidLoad() if isRoot { - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { _ in + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { notification in - self.navigationController?.popToRootViewController(animated: false) + if let userInfo = notification.userInfo, let account = userInfo["account"] as? String { + if let controller = userInfo["controller"] as? NCMainTabBarController, + controller == self.controller { + controller.account = account + } else { + return + } + } - self.serverUrl = self.utilityFileSystem.getHomeServer(urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId) + self.navigationController?.popToRootViewController(animated: false) + self.serverUrl = self.utilityFileSystem.getHomeServer(session: self.session) self.isSearchingMode = false self.isEditMode = false - self.selectOcId.removeAll() + self.fileSelect.removeAll() + self.layoutForView = self.database.getLayoutForView(account: self.session.account, key: self.layoutKey, serverUrl: self.serverUrl) - self.layoutForView = NCManageDatabase.shared.getLayoutForView(account: self.appDelegate.account, key: self.layoutKey, serverUrl: self.serverUrl) - if self.layoutForView?.layout == NCGlobal.shared.layoutList { + if self.isLayoutList { self.collectionView?.collectionViewLayout = self.listLayout - } else if self.layoutForView?.layout == NCGlobal.shared.layoutGrid { + } else if self.isLayoutGrid { self.collectionView?.collectionViewLayout = self.gridLayout - } else if self.layoutForView?.layout == NCGlobal.shared.layoutPhotoSquare || self.layoutForView?.layout == NCGlobal.shared.layoutPhotoRatio { + } else if self.isLayoutPhoto { self.collectionView?.collectionViewLayout = self.mediaLayout } self.titleCurrentFolder = self.getNavigationTitle() self.setNavigationLeftItems() + self.dataSource.removeAll() self.reloadDataSource() - self.reloadDataSourceNetwork() + self.getServerData() } } } override func viewWillAppear(_ animated: Bool) { if isRoot { - serverUrl = utilityFileSystem.getHomeServer(urlBase: appDelegate.urlBase, userId: appDelegate.userId) + serverUrl = utilityFileSystem.getHomeServer(session: session) titleCurrentFolder = getNavigationTitle() } super.viewWillAppear(animated) - if dataSource.metadatas.isEmpty { - reloadDataSource(withQueryDB: true) + reloadDataSource() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !self.dataSource.isEmpty() { + self.blinkCell(fileName: self.fileNameBlink) + self.openFile(fileName: self.fileNameOpen) + self.fileNameBlink = nil + self.fileNameOpen = nil + } + + if !isSearchingMode { + getServerData() } - reloadDataSourceNetwork(withQueryDB: true) } override func viewDidDisappear(_ animated: Bool) { @@ -95,38 +117,36 @@ class NCFiles: NCCollectionViewCommon { fileNameOpen = nil } - // MARK: - DataSource + NC Endpoint + // MARK: - DataSource - override func queryDB() { - super.queryDB() + override func reloadDataSource() { + var predicate = self.defaultPredicate + let predicateDirectory = NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, self.serverUrl) + let dataSourceMetadatas = self.dataSource.getMetadatas() - var metadatas: [tableMetadata] = [] - if NCKeychain().getPersonalFilesOnly(account: self.appDelegate.account) { - metadatas = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == ''", self.appDelegate.account, self.serverUrl, self.appDelegate.userId)) - } else { - metadatas = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", self.appDelegate.account, self.serverUrl)) - } - let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", self.appDelegate.account, self.serverUrl)) - if self.metadataFolder == nil { - self.metadataFolder = NCManageDatabase.shared.getMetadataFolder(account: self.appDelegate.account, urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId, serverUrl: self.serverUrl) + if NCKeychain().getPersonalFilesOnly(account: session.account) { + predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND (ownerId == %@ || ownerId == '') AND mountType == '' AND NOT (status IN %@)", session.account, self.serverUrl, session.userId, global.metadataStatusHideInView) } - self.richWorkspaceText = directory?.richWorkspace - self.dataSource = NCDataSource(metadatas: metadatas, account: self.appDelegate.account, layoutForView: layoutForView, providers: self.providers, searchResults: self.searchResults) - } + self.metadataFolder = database.getMetadataFolder(session: session, serverUrl: self.serverUrl) + self.richWorkspaceText = database.getTableDirectory(predicate: predicateDirectory)?.richWorkspace - override func reloadDataSource(withQueryDB: Bool = true) { - super.reloadDataSource(withQueryDB: withQueryDB) + let results = self.database.getResultsMetadatasPredicate(predicate, layoutForView: layoutForView) + self.dataSource = NCCollectionViewDataSource(results: results, layoutForView: layoutForView) - if !self.dataSource.metadatas.isEmpty { - self.blinkCell(fileName: self.fileNameBlink) - self.openFile(fileName: self.fileNameOpen) - self.fileNameBlink = nil - self.fileNameOpen = nil + guard let results else { + return super.reloadDataSource() + } + let metadatas = Array(results.freeze()) + + self.dataSource.caching(metadatas: metadatas, dataSourceMetadatas: dataSourceMetadatas) { updated in + if updated || self.isNumberOfItemsInAllSectionsNull || self.numberOfItemsInAllSections != metadatas.count { + super.reloadDataSource() + } } } - override func reloadDataSourceNetwork(withQueryDB: Bool = false) { + override func getServerData() { if UIApplication.shared.applicationState == .background { NextcloudKit.shared.nkCommonInstance.writeLog("[DEBUG] Files not reload datasource network with the application in background") return @@ -136,125 +156,137 @@ class NCFiles: NCCollectionViewCommon { } func downloadMetadata(_ metadata: tableMetadata) -> Bool { - let fileSize = utilityFileSystem.fileProviderStorageSize(metadata.ocId, fileNameView: metadata.fileNameView) guard fileSize > 0 else { return false } - if let localFile = NCManageDatabase.shared.getResultsTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId))?.first { + if let localFile = database.getResultsTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId))?.first { if localFile.etag != metadata.etag { return true } } - return false } - super.reloadDataSourceNetwork() + DispatchQueue.global().async { + self.networkReadFolder { metadatas, isChanged, error in + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + + if isChanged || self.isNumberOfItemsInAllSectionsNull { + self.reloadDataSource() + } + } - networkReadFolder { tableDirectory, metadatas, metadatasDifferentCount, metadatasModified, error in - DispatchQueue.global(qos: .userInteractive).async { if error == .success { - for metadata in metadatas ?? [] where !metadata.directory && downloadMetadata(metadata) { + let metadatas: [tableMetadata] = metadatas ?? self.dataSource.getMetadatas() + for metadata in metadatas where !metadata.directory && downloadMetadata(metadata) { if NCNetworking.shared.downloadQueue.operations.filter({ ($0 as? NCOperationDownload)?.metadata.ocId == metadata.ocId }).isEmpty { NCNetworking.shared.downloadQueue.addOperation(NCOperationDownload(metadata: metadata, selector: NCGlobal.shared.selectorDownloadFile)) } } - self.richWorkspaceText = tableDirectory?.richWorkspace - if metadatasDifferentCount != 0 || metadatasModified != 0 { - self.reloadDataSource() - } else { - self.reloadDataSource(withQueryDB: withQueryDB) - } - } else { - self.reloadDataSource(withQueryDB: withQueryDB) } } } } - private func networkReadFolder(completion: @escaping(_ tableDirectory: tableDirectory?, _ metadatas: [tableMetadata]?, _ metadatasDifferentCount: Int, _ metadatasModified: Int, _ error: NKError) -> Void) { - var tableDirectory: tableDirectory? - - NCNetworking.shared.readFile(serverUrlFileName: serverUrl, account: appDelegate.account) { task in + private func networkReadFolder(completion: @escaping (_ metadatas: [tableMetadata]?, _ isDataChanged: Bool, _ error: NKError) -> Void) { + NCNetworking.shared.readFile(serverUrlFileName: serverUrl, account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + if self.dataSource.isEmpty() { + self.collectionView.reloadData() + } } completion: { account, metadata, error in + let isDirectoryE2EE = NCUtilityFileSystem().isDirectoryE2EE(session: self.session, serverUrl: self.serverUrl) guard error == .success, let metadata else { - return completion(nil, nil, 0, 0, error) + return completion(nil, false, error) + } + /// Check change eTag or E2EE or DataSource empty + let tableDirectory = self.database.setDirectory(serverUrl: self.serverUrl, richWorkspace: metadata.richWorkspace, account: account) + guard tableDirectory?.etag != metadata.etag || metadata.e2eEncrypted || self.dataSource.isEmpty() else { + return completion(nil, false, NKError()) + } + /// Check Response DataC hanged + var checkResponseDataChanged = true + if tableDirectory?.etag.isEmpty ?? true || isDirectoryE2EE { + checkResponseDataChanged = false } - tableDirectory = NCManageDatabase.shared.setDirectory(serverUrl: self.serverUrl, richWorkspace: metadata.richWorkspace, account: account) - // swiftlint:disable empty_string - let forceReplaceMetadatas = tableDirectory?.etag == "" - // swiftlint:enable empty_string - - if tableDirectory?.etag != metadata.etag || metadata.e2eEncrypted { - NCNetworking.shared.readFolder(serverUrl: self.serverUrl, - account: self.appDelegate.account, - forceReplaceMetadatas: forceReplaceMetadatas) { task in - self.dataSourceTask = task + + NCNetworking.shared.readFolder(serverUrl: self.serverUrl, + account: metadata.account, + checkResponseDataChanged: checkResponseDataChanged, + queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue) { task in + self.dataSourceTask = task + if self.dataSource.isEmpty() { self.collectionView.reloadData() - } completion: { account, metadataFolder, metadatas, metadatasDifferentCount, metadatasModified, error in - guard account == self.appDelegate.account, error == .success else { - return completion(tableDirectory, nil, 0, 0, error) - } + } + } completion: { account, metadataFolder, metadatas, isDataChanged, error in + /// Error + guard error == .success else { + return completion(nil, false, error) + } + /// Updata folder + if let metadataFolder { self.metadataFolder = metadataFolder - // E2EE - if let metadataFolder = metadataFolder, - metadataFolder.e2eEncrypted, - NCKeychain().isEndToEndEnabled(account: account), - !NCNetworkingE2EE().isInUpload(account: account, serverUrl: self.serverUrl) { - let lock = NCManageDatabase.shared.getE2ETokenLock(account: account, serverUrl: self.serverUrl) - NCNetworkingE2EE().getMetadata(fileId: metadataFolder.ocId, e2eToken: lock?.e2eToken, account: account) { account, version, e2eMetadata, signature, _, error in - if account == self.appDelegate.account, error == .success, let e2eMetadata = e2eMetadata { - let error = NCEndToEndMetadata().decodeMetadata(e2eMetadata, signature: signature, serverUrl: self.serverUrl, account: account, urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId) - if error == .success { - if version == "v1", NCGlobal.shared.capabilityE2EEApiVersion == NCGlobal.shared.e2eeVersionV20 { - NextcloudKit.shared.nkCommonInstance.writeLog("[E2EE] Conversion v1 to v2") - NCActivityIndicator.shared.start() - Task { - let serverUrl = metadataFolder.serverUrl + "/" + metadataFolder.fileName - let error = await NCNetworkingE2EE().uploadMetadata(account: metadataFolder.account, serverUrl: serverUrl, userId: metadataFolder.userId, updateVersionV1V2: true) - if error != .success { - NCContentPresenter().showError(error: error) - } - NCActivityIndicator.shared.stop() - self.reloadDataSource() - } - } else { - self.reloadDataSource() - } - } else { - // Client Diagnostic - NCManageDatabase.shared.addDiagnostic(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors) - NCContentPresenter().showError(error: error) - } - } else if error.errorCode == NCGlobal.shared.errorResourceNotFound { - // no metadata found, send a new metadata + self.richWorkspaceText = metadataFolder.richWorkspace + } + /// check Response Data Changed + if !isDataChanged { + return completion(nil, false, error) + } + + guard let metadataFolder, + isDirectoryE2EE, + NCKeychain().isEndToEndEnabled(account: account), + !NCNetworkingE2EE().isInUpload(account: account, serverUrl: self.serverUrl) else { + return completion(metadatas, true, error) + } + + /// E2EE + let lock = self.database.getE2ETokenLock(account: account, serverUrl: self.serverUrl) + NCNetworkingE2EE().getMetadata(fileId: metadataFolder.ocId, e2eToken: lock?.e2eToken, account: account) { account, version, e2eMetadata, signature, _, error in + + if error == .success, let e2eMetadata = e2eMetadata { + let error = NCEndToEndMetadata().decodeMetadata(e2eMetadata, signature: signature, serverUrl: self.serverUrl, session: self.session) + + if error == .success { + if version == "v1", NCCapabilities.shared.getCapabilities(account: account).capabilityE2EEApiVersion == NCGlobal.shared.e2eeVersionV20 { + NextcloudKit.shared.nkCommonInstance.writeLog("[E2EE] Conversion v1 to v2") + NCActivityIndicator.shared.start() Task { let serverUrl = metadataFolder.serverUrl + "/" + metadataFolder.fileName - let error = await NCNetworkingE2EE().uploadMetadata(account: metadataFolder.account, serverUrl: serverUrl, userId: metadataFolder.userId) + let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, updateVersionV1V2: true, account: account) if error != .success { NCContentPresenter().showError(error: error) } + NCActivityIndicator.shared.stop() } - } else { - NCContentPresenter().showError(error: NKError(errorCode: NCGlobal.shared.errorE2EEKeyDecodeMetadata, errorDescription: "_e2e_error_")) } - completion(tableDirectory, metadatas, metadatasDifferentCount, metadatasModified, error) + } else { + // Client Diagnostic + self.database.addDiagnostic(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors) + NCContentPresenter().showError(error: error) + } + } else if error.errorCode == NCGlobal.shared.errorResourceNotFound { + // no metadata found, send a new metadata + Task { + let serverUrl = metadataFolder.serverUrl + "/" + metadataFolder.fileName + let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, account: account) + if error != .success { + NCContentPresenter().showError(error: error) + } } } else { - completion(tableDirectory, metadatas, metadatasDifferentCount, metadatasModified, error) + NCContentPresenter().showError(error: NKError(errorCode: NCGlobal.shared.errorE2EEKeyDecodeMetadata, errorDescription: "_e2e_error_")) } + completion(metadatas, true, error) } - } else { - completion(tableDirectory, nil, 0, 0, NKError()) } } } func blinkCell(fileName: String?) { - if let fileName = fileName, let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", self.appDelegate.account, self.serverUrl, fileName)) { - let (indexPath, _) = self.dataSource.getIndexPathMetadata(ocId: metadata.ocId) + if let fileName = fileName, let metadata = database.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", session.account, self.serverUrl, fileName)) { + let indexPath = self.dataSource.getIndexPathMetadata(ocId: metadata.ocId) if let indexPath = indexPath { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { UIView.animate(withDuration: 0.3) { @@ -273,8 +305,8 @@ class NCFiles: NCCollectionViewCommon { } func openFile(fileName: String?) { - if let fileName = fileName, let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", self.appDelegate.account, self.serverUrl, fileName)) { - let (indexPath, _) = self.dataSource.getIndexPathMetadata(ocId: metadata.ocId) + if let fileName = fileName, let metadata = database.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", session.account, self.serverUrl, fileName)) { + let indexPath = self.dataSource.getIndexPathMetadata(ocId: metadata.ocId) if let indexPath = indexPath { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.collectionView(self.collectionView, didSelectItemAt: indexPath) @@ -285,11 +317,12 @@ class NCFiles: NCCollectionViewCommon { // MARK: - NCAccountSettingsModelDelegate - override func accountSettingsDidDismiss(tableAccount: tableAccount?) { - if NCManageDatabase.shared.getAllAccount().isEmpty { - appDelegate.openLogin(selector: NCGlobal.shared.introLogin, openLoginWeb: false) - } else if let account = tableAccount?.account, account != appDelegate.account { - appDelegate.changeAccount(account, userProfile: nil) { } + override func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { + let currentAccount = session.account + if database.getAllTableAccount().isEmpty { + appDelegate.openLogin(selector: NCGlobal.shared.introLogin) + } else if let account = tableAccount?.account, account != currentAccount { + NCAccount().changeAccount(account, userProfile: nil, controller: controller) { } } else if isRoot { titleCurrentFolder = getNavigationTitle() navigationItem.title = titleCurrentFolder diff --git a/iOSClient/GUI/ComponentView.swift b/iOSClient/GUI/ComponentView.swift index 93fad23da3..4e2bd766d1 100644 --- a/iOSClient/GUI/ComponentView.swift +++ b/iOSClient/GUI/ComponentView.swift @@ -24,7 +24,6 @@ import SwiftUI struct TextFieldClearButton: ViewModifier { - @Binding var text: String func body(content: Content) -> some View { @@ -44,15 +43,15 @@ struct TextFieldClearButton: ViewModifier { } struct ButtonRounded: ButtonStyle { - var disabled = false + var account = "" func makeBody(configuration: Configuration) -> some View { configuration.label .padding(.horizontal, 40) .padding(.vertical, 10) - .background(disabled ? Color(UIColor.placeholderText) : Color(NCBrandColor.shared.brandElement)) - .foregroundColor(disabled ? Color(UIColor.placeholderText) : Color(NCBrandColor.shared.brandText)) + .background(disabled ? Color(UIColor.placeholderText) : Color(NCBrandColor.shared.getElement(account: account))) + .foregroundColor(disabled ? Color(UIColor.placeholderText) : Color(NCBrandColor.shared.getText(account: account))) .clipShape(Capsule()) .opacity(configuration.isPressed ? 0.5 : 1.0) } diff --git a/iOSClient/GUI/HUDView.swift b/iOSClient/GUI/NCHUDView.swift similarity index 88% rename from iOSClient/GUI/HUDView.swift rename to iOSClient/GUI/NCHUDView.swift index 789c7711c1..051957a391 100644 --- a/iOSClient/GUI/HUDView.swift +++ b/iOSClient/GUI/NCHUDView.swift @@ -23,11 +23,11 @@ import SwiftUI -struct HUDView: View { - +struct NCHUDView: View { @Binding var showHUD: Bool @State var textLabel: String @State var image: String + @State var color: UIColor var body: some View { Button(action: { @@ -40,7 +40,7 @@ struct HUDView: View { .padding(.horizontal, 10) .padding(14) .background( - Blur(style: .regular) + Blur(style: .regular, color: color) .clipShape(Capsule()) .shadow(color: Color(.black).opacity(0.22), radius: 12, x: 0, y: 5) ) @@ -49,12 +49,12 @@ struct HUDView: View { } struct Blur: UIViewRepresentable { - var style: UIBlurEffect.Style + var color: UIColor func makeUIView(context: Context) -> UIVisualEffectView { let effectView = UIVisualEffectView(effect: UIBlurEffect(style: style)) - effectView.backgroundColor = NCBrandColor.shared.brandElement + effectView.backgroundColor = color return effectView } @@ -64,8 +64,8 @@ struct Blur: UIViewRepresentable { } struct ContentView: View { - @State private var showHUD = false + @State var color: UIColor @Namespace var hudAnimation func dismissHUDAfterTime() { @@ -83,14 +83,10 @@ struct ContentView: View { } .navigationTitle("Content View") } - HUDView(showHUD: $showHUD, textLabel: NSLocalizedString("_wait_", comment: ""), image: "doc.badge.arrow.up") + NCHUDView(showHUD: $showHUD, textLabel: NSLocalizedString("_wait_", comment: ""), image: "doc.badge.arrow.up", color: color) .offset(y: showHUD ? (geo.size.height / 2) : -200) .animation(.easeOut, value: showHUD) } } } } - -#Preview { - ContentView() -} diff --git a/iOSClient/GUI/NCHud.swift b/iOSClient/GUI/NCHud.swift new file mode 100644 index 0000000000..32a9f75cf5 --- /dev/null +++ b/iOSClient/GUI/NCHud.swift @@ -0,0 +1,131 @@ +// +// NCHud.swift +// Nextcloud +// +// Created by Marino Faggiana on 04/09/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Foundation +import UIKit +import JGProgressHUD + +class NCHud: NSObject { + private let hud = JGProgressHUD() + private var view: UIView? + + public init(_ view: UIView? = nil) { + if let view { + self.view = view + } + super.init() + } + + func initHud(view: UIView? = nil, text: String? = nil, detailText: String? = nil) { + DispatchQueue.main.async { + if let view { + self.view = view + } + + self.hud.indicatorView = JGProgressHUDIndicatorView() + + self.hud.textLabel.text = text + self.hud.textLabel.textColor = NCBrandColor.shared.iconImageColor + + self.hud.detailTextLabel.text = detailText + self.hud.detailTextLabel.textColor = NCBrandColor.shared.iconImageColor2 + + if let view = self.view { + self.hud.show(in: view) + } + } + } + + func initHudRing(view: UIView? = nil, text: String? = nil, detailText: String? = nil, tapToCancelDetailText: Bool = false, tapOperation: (() -> Void)? = nil) { + DispatchQueue.main.async { + self.hud.tapOnHUDViewBlock = { hud in + if let tapOperation { + tapOperation() + hud.dismiss() + } + } + + if let view { + self.view = view + } + + self.hud.indicatorView = JGProgressHUDRingIndicatorView() + self.hud.progress = 0.0 + + let indicatorView = self.hud.indicatorView as? JGProgressHUDRingIndicatorView + indicatorView?.ringWidth = 1.5 + indicatorView?.ringColor = NCBrandColor.shared.iconImageColor + + self.hud.textLabel.text = text + self.hud.textLabel.textColor = NCBrandColor.shared.iconImageColor + + if tapToCancelDetailText { + self.hud.detailTextLabel.text = NSLocalizedString("_tap_to_cancel_", comment: "") + } else { + self.hud.detailTextLabel.text = detailText + } + self.hud.detailTextLabel.textColor = NCBrandColor.shared.iconImageColor2 + + if let view = self.view { + self.hud.show(in: view) + } + } + } + + func dismiss() { + DispatchQueue.main.async { + self.hud.dismiss() + } + } + + func show() { + DispatchQueue.main.async { + if let view = self.view { + self.hud.show(in: view) + } + } + } + + func progress(num: Float, total: Float) { + DispatchQueue.main.async { + self.hud.progress = num / total + } + } + + func progress(_ progress: Double) { + DispatchQueue.main.async { + self.hud.progress = Float(progress) + } + } + + func success() { + DispatchQueue.main.async { + self.hud.indicatorView = JGProgressHUDSuccessIndicatorView() + self.hud.indicatorView?.tintColor = .green + self.hud.textLabel.text = NSLocalizedString("_success_", comment: "") + self.hud.detailTextLabel.text = nil + self.hud.dismiss(afterDelay: 1.0) + } + } + + func error(text: String?) { + DispatchQueue.main.async { + self.hud.indicatorView = JGProgressHUDErrorIndicatorView() + self.hud.indicatorView?.tintColor = .red + self.hud.textLabel.text = text + self.hud.dismiss(afterDelay: 2.0) + } + } + + func setText(text: String?, detailText: String? = nil) { + DispatchQueue.main.async { + self.hud.textLabel.text = text + self.hud.detailTextLabel.text = detailText + } + } +} diff --git a/iOSClient/Groupfolders/NCGroupfolders.swift b/iOSClient/Groupfolders/NCGroupfolders.swift index f8c0a49b2c..44ca7f8f29 100644 --- a/iOSClient/Groupfolders/NCGroupfolders.swift +++ b/iOSClient/Groupfolders/NCGroupfolders.swift @@ -23,6 +23,7 @@ import UIKit import NextcloudKit +import RealmSwift class NCGroupfolders: NCCollectionViewCommon { @@ -33,7 +34,7 @@ class NCGroupfolders: NCCollectionViewCommon { layoutKey = NCGlobal.shared.layoutViewGroupfolders enableSearchBar = false headerRichWorkspaceDisable = true - emptyImage = utility.loadImage(named: "folder_group", colors: [NCBrandColor.shared.brandElement]) + emptyImageName = "folder_group" emptyTitle = "_files_no_files_" emptyDescription = "_tutorial_groupfolders_view_" } @@ -43,58 +44,59 @@ class NCGroupfolders: NCCollectionViewCommon { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if dataSource.metadatas.isEmpty { - reloadDataSource() - } - reloadDataSourceNetwork() + reloadDataSource() } - // MARK: - DataSource + NC Endpoint + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + getServerData() + } - override func queryDB() { - super.queryDB() + // MARK: - DataSource - var metadatas: [tableMetadata] = [] + override func reloadDataSource() { + var results: Results? if self.serverUrl.isEmpty { - metadatas = NCManageDatabase.shared.getMetadatasFromGroupfolders(account: self.appDelegate.account, urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId) + results = database.getResultsMetadatasFromGroupfolders(session: session) } else { - metadatas = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", self.appDelegate.account, self.serverUrl)) + results = self.database.getResultsMetadatasPredicate(self.defaultPredicate, layoutForView: layoutForView) } - self.dataSource = NCDataSource(metadatas: metadatas, account: self.appDelegate.account, layoutForView: layoutForView, providers: self.providers, searchResults: self.searchResults) - } + self.dataSource = NCCollectionViewDataSource(results: results, layoutForView: layoutForView) - override func reloadDataSourceNetwork(withQueryDB: Bool = false) { - super.reloadDataSourceNetwork() + super.reloadDataSource() + } - let homeServerUrl = utilityFileSystem.getHomeServer(urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId) + override func getServerData() { + let homeServerUrl = utilityFileSystem.getHomeServer(session: session) - NextcloudKit.shared.getGroupfolders(account: self.appDelegate.account, options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { task in + NextcloudKit.shared.getGroupfolders(account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + if self.dataSource.isEmpty() { + self.collectionView.reloadData() + } } completion: { account, results, _, error in if error == .success, let groupfolders = results { - NCManageDatabase.shared.addGroupfolders(account: account, groupfolders: groupfolders) + self.database.addGroupfolders(account: account, groupfolders: groupfolders) Task { for groupfolder in groupfolders { let mountPoint = groupfolder.mountPoint.hasPrefix("/") ? groupfolder.mountPoint : "/" + groupfolder.mountPoint let serverUrlFileName = homeServerUrl + mountPoint - if NCManageDatabase.shared.getMetadataFromDirectory(account: self.appDelegate.account, serverUrl: serverUrlFileName) == nil { - let results = await NCNetworking.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", showHiddenFiles: NCKeychain().showHiddenFiles, account: account) - if results.error == .success, let file = results.files.first { - let isDirectoryE2EE = self.utilityFileSystem.isDirectoryE2EE(file: file) - let metadata = NCManageDatabase.shared.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE) - NCManageDatabase.shared.addMetadata(metadata) - NCManageDatabase.shared.addDirectory(e2eEncrypted: isDirectoryE2EE, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account) - } + let results = await NCNetworking.shared.readFileOrFolder(serverUrlFileName: serverUrlFileName, depth: "0", showHiddenFiles: NCKeychain().showHiddenFiles, account: account) + + if results.error == .success, let file = results.files?.first { + let isDirectoryE2EE = self.utilityFileSystem.isDirectoryE2EE(file: file) + let metadata = self.database.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE) + self.database.addMetadata(metadata) + self.database.addDirectory(e2eEncrypted: isDirectoryE2EE, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account) } } self.reloadDataSource() } - } else { - self.reloadDataSource(withQueryDB: withQueryDB) } + self.refreshControl.endRefreshing() } } } diff --git a/iOSClient/Login/NCLogin.storyboard b/iOSClient/Login/NCLogin.storyboard index 9841743f53..4d56ceb63f 100644 --- a/iOSClient/Login/NCLogin.storyboard +++ b/iOSClient/Login/NCLogin.storyboard @@ -1,10 +1,12 @@ - - + + - + + + @@ -13,18 +15,18 @@ - + - + - + @@ -33,7 +35,7 @@ - - - - - - - - + + - + @@ -90,27 +111,33 @@ + + - - + + + + + - + + + - @@ -123,7 +150,7 @@ - + @@ -134,4 +161,12 @@ + + + + + + + + diff --git a/iOSClient/Login/NCLogin.swift b/iOSClient/Login/NCLogin.swift index 1eb13cb43a..6c6db2d487 100644 --- a/iOSClient/Login/NCLogin.swift +++ b/iOSClient/Login/NCLogin.swift @@ -30,12 +30,13 @@ import SwiftUI class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { @IBOutlet weak var imageBrand: UIImageView! @IBOutlet weak var imageBrandConstraintY: NSLayoutConstraint! - @IBOutlet weak var baseUrl: UITextField! + @IBOutlet weak var baseUrlTextField: UITextField! @IBOutlet weak var loginAddressDetail: UILabel! @IBOutlet weak var loginButton: UIButton! - @IBOutlet weak var loginImage: UIImageView! @IBOutlet weak var qrCode: UIButton! @IBOutlet weak var certificate: UIButton! + @IBOutlet weak var enforceServersButton: UIButton! + @IBOutlet weak var enforceServersDropdownImage: UIImageView! private let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! private var textColor: UIColor = .white @@ -82,37 +83,31 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { imageBrand.image = UIImage(named: "logo") // Url - baseUrl.textColor = textColor - baseUrl.tintColor = textColor - baseUrl.layer.cornerRadius = 10 - baseUrl.layer.borderWidth = 1 - baseUrl.layer.borderColor = textColor.cgColor - baseUrl.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: baseUrl.frame.height)) - baseUrl.leftViewMode = .always - baseUrl.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 35, height: baseUrl.frame.height)) - baseUrl.rightViewMode = .always - baseUrl.attributedPlaceholder = NSAttributedString(string: NSLocalizedString("_login_url_", comment: ""), attributes: [NSAttributedString.Key.foregroundColor: textColor.withAlphaComponent(0.5)]) - baseUrl.delegate = self - - baseUrl.isEnabled = !NCBrandOptions.shared.disable_request_login_url + baseUrlTextField.textColor = textColor + baseUrlTextField.tintColor = textColor + baseUrlTextField.layer.cornerRadius = 10 + baseUrlTextField.layer.borderWidth = 1 + baseUrlTextField.layer.borderColor = textColor.cgColor + baseUrlTextField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: baseUrlTextField.frame.height)) + baseUrlTextField.leftViewMode = .always + baseUrlTextField.rightView = UIView(frame: CGRect(x: 0, y: 0, width: 35, height: baseUrlTextField.frame.height)) + baseUrlTextField.rightViewMode = .always + baseUrlTextField.attributedPlaceholder = NSAttributedString(string: NSLocalizedString("_login_url_", comment: ""), attributes: [NSAttributedString.Key.foregroundColor: textColor.withAlphaComponent(0.5)]) + baseUrlTextField.delegate = self + + baseUrlTextField.isEnabled = !NCBrandOptions.shared.disable_request_login_url // Login button loginAddressDetail.textColor = textColor loginAddressDetail.text = String.localizedStringWithFormat(NSLocalizedString("_login_address_detail_", comment: ""), NCBrandOptions.shared.brand) - // Login Image - loginImage.image = UIImage(named: "arrow.right")?.image(color: textColor, size: 100) - // brand if NCBrandOptions.shared.disable_request_login_url { - baseUrl.isEnabled = false - baseUrl.isUserInteractionEnabled = false - baseUrl.alpha = 0.5 + baseUrlTextField.isEnabled = false + baseUrlTextField.isUserInteractionEnabled = false + baseUrlTextField.alpha = 0.5 } - // qrcode - qrCode.setImage(UIImage(systemName: "qrcode.viewfinder")?.image(color: textColor, size: 100), for: .normal) - // certificate certificate.setImage(UIImage(named: "certificate")?.image(color: textColor, size: 100), for: .normal) certificate.isHidden = true @@ -129,7 +124,7 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { self.navigationController?.view.backgroundColor = NCBrandColor.shared.customer self.navigationController?.navigationBar.tintColor = textColor - if !NCManageDatabase.shared.getAllAccount().isEmpty { + if !NCManageDatabase.shared.getAllTableAccount().isEmpty { let navigationItemCancel = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(self.actionCancel)) navigationItemCancel.tintColor = textColor navigationItem.leftBarButtonItem = navigationItemCancel @@ -137,14 +132,14 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { if let dirGroupApps = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: NCBrandOptions.shared.capabilitiesGroupApps) { // Nextcloud update share accounts - if let error = appDelegate.updateShareAccounts() { + if let error = NCAccount().updateAppsShareAccounts() { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Create share accounts \(error.localizedDescription)") } // Nextcloud get share accounts if let shareAccounts = NKShareAccounts().getShareAccount(at: dirGroupApps, application: UIApplication.shared) { var accountTemp = [NKShareAccounts.DataAccounts]() for shareAccount in shareAccounts { - if NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "urlBase == %@ AND user == %@", shareAccount.url, shareAccount.user)) == nil { + if NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "urlBase == %@ AND user == %@", shareAccount.url, shareAccount.user)) == nil { accountTemp.append(shareAccount) } } @@ -165,14 +160,40 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) handleLoginWithAppConfig() - baseUrl.text = urlBase + baseUrlTextField.text = urlBase + + enforceServersButton.setTitle(NSLocalizedString("_select_server_", comment: ""), for: .normal) + + let enforceServers = NCBrandOptions.shared.enforce_servers + + if !enforceServers.isEmpty { + baseUrlTextField.isHidden = true + enforceServersDropdownImage.isHidden = false + enforceServersButton.isHidden = false + + let actions = enforceServers.map { server in + UIAction(title: server.name, handler: { [self] _ in + enforceServersButton.setTitle(server.name, for: .normal) + baseUrlTextField.text = server.url + }) + } + + enforceServersButton.layer.cornerRadius = 10 + enforceServersButton.menu = .init(title: NSLocalizedString("_servers_", comment: ""), children: actions) + enforceServersButton.showsMenuAsPrimaryAction = true + enforceServersButton.configuration?.titleTextAttributesTransformer = + UIConfigurationTextAttributesTransformer { incoming in + var outgoing = incoming + outgoing.font = UIFont.systemFont(ofSize: 13) + return outgoing + } + } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - appDelegate.timerErrorNetworkingDisabled = true - if self.shareAccounts != nil, let image = UIImage(systemName: "person.badge.plus")?.withTintColor(.white, renderingMode: .alwaysOriginal), let backgroundColor = NCBrandColor.shared.brandElement.lighter(by: 10) { + if self.shareAccounts != nil, let image = UIImage(systemName: "person.badge.plus")?.withTintColor(.white, renderingMode: .alwaysOriginal), let backgroundColor = NCBrandColor.shared.customer.lighter(by: 10) { let title = String(format: NSLocalizedString("_apps_nextcloud_detect_", comment: ""), NCBrandOptions.shared.brand) let description = String(format: NSLocalizedString("_add_existing_account_", comment: ""), NCBrandOptions.shared.brand) NCContentPresenter().alertAction(image: image, contentModeImage: .scaleAspectFit, sizeImage: CGSize(width: 45, height: 45), backgroundColor: backgroundColor, textColor: textColor, title: title, description: description, textCancelButton: "_cancel_", textOkButton: "_ok_", attributes: EKAttributes.topFloat) { identifier in @@ -183,11 +204,6 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { } } - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - appDelegate.timerErrorNetworkingDisabled = false - } - private func handleLoginWithAppConfig() { let accountCount = NCManageDatabase.shared.getAccounts()?.count ?? 0 @@ -297,20 +313,20 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { // MARK: - Login private func login() { - guard var url = baseUrl.text?.trimmingCharacters(in: .whitespacesAndNewlines) else { return } + guard var url = baseUrlTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) else { return } if url.hasSuffix("/") { url = String(url.dropLast()) } if url.isEmpty { return } // Check whether baseUrl contain protocol. If not add https:// by default. if url.hasPrefix("https") == false && url.hasPrefix("http") == false { url = "https://" + url } - self.baseUrl.text = url + self.baseUrlTextField.text = url isUrlValid(url: url) } func isUrlValid(url: String, user: String? = nil) { loginButton.isEnabled = false - NextcloudKit.shared.getServerStatus(serverUrl: url) { serverInfoResult in + NextcloudKit.shared.getServerStatus(serverUrl: url) { _, serverInfoResult in switch serverInfoResult { case .success(let serverInfo): if let host = URL(string: url)?.host { @@ -369,9 +385,9 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { let user = valueArray[0].replacingOccurrences(of: "user:", with: "") let password = valueArray[1].replacingOccurrences(of: "password:", with: "") let urlBase = valueArray[2].replacingOccurrences(of: "server:", with: "") - let serverUrl = urlBase + "/" + NextcloudKit.shared.nkCommonInstance.dav + let serverUrl = urlBase + "/remote.php/dav" loginButton.isEnabled = false - NextcloudKit.shared.checkServer(serverUrl: serverUrl) { error in + NextcloudKit.shared.checkServer(serverUrl: serverUrl) { _, error in self.loginButton.isEnabled = true if error == .success { self.createAccount(urlBase: urlBase, user: user, password: password) @@ -397,16 +413,19 @@ class NCLogin: UIViewController, UITextFieldDelegate, NCLoginQRCodeDelegate { } private func createAccount(urlBase: String, user: String, password: String) { + let controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController if let host = URL(string: urlBase)?.host { NCNetworking.shared.writeCertificate(host: host) } - self.appDelegate.createAccount(urlBase: urlBase, user: user, password: password) { error in + NCAccount().createAccount(urlBase: urlBase, user: user, password: password, controller: controller) { account, error in if error == .success { let window = UIApplication.shared.firstWindow - if window?.rootViewController is NCMainTabBarController { + if let controller = window?.rootViewController as? NCMainTabBarController { + controller.account = account self.dismiss(animated: true) } else { if let controller = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? NCMainTabBarController { + controller.account = account controller.modalPresentationStyle = .fullScreen controller.view.alpha = 0 window?.rootViewController = controller diff --git a/iOSClient/Login/NCLoginPoll.swift b/iOSClient/Login/NCLoginPoll.swift index d1d38d0d79..7030653740 100644 --- a/iOSClient/Login/NCLoginPoll.swift +++ b/iOSClient/Login/NCLoginPoll.swift @@ -67,17 +67,18 @@ struct NCLoginPoll: View { .onChange(of: loginManager.pollFinished) { value in if value { let window = UIApplication.shared.firstWindow - - if window?.rootViewController is NCMainTabBarController { - window?.rootViewController?.dismiss(animated: true, completion: nil) + if let controller = window?.rootViewController as? NCMainTabBarController { + controller.account = loginManager.account + controller.dismiss(animated: true, completion: nil) } else { - if let mainTabBarController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? NCMainTabBarController { - mainTabBarController.modalPresentationStyle = .fullScreen - mainTabBarController.view.alpha = 0 - window?.rootViewController = mainTabBarController + if let controller = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? NCMainTabBarController { + controller.account = loginManager.account + controller.modalPresentationStyle = .fullScreen + controller.view.alpha = 0 + window?.rootViewController = controller window?.makeKeyAndVisible() UIView.animate(withDuration: 0.5) { - mainTabBarController.view.alpha = 1 + controller.view.alpha = 1 } } } @@ -91,6 +92,9 @@ struct NCLoginPoll: View { loginManager.openLoginInBrowser() } } + .onDisappear { + loginManager.onDisappear() + } .interactiveDismissDisabled() } } @@ -100,40 +104,53 @@ struct NCLoginPoll: View { } private class LoginManager: ObservableObject { - private let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - var loginFlowV2Token = "" var loginFlowV2Endpoint = "" var loginFlowV2Login = "" @Published var pollFinished = false @Published var isLoading = false + @Published var account = "" - init() { - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) - } - - @objc func applicationDidBecomeActive(_ notification: NSNotification) { - poll() - } + var timer: DispatchSourceTimer? func configure(loginFlowV2Token: String, loginFlowV2Endpoint: String, loginFlowV2Login: String) { self.loginFlowV2Token = loginFlowV2Token self.loginFlowV2Endpoint = loginFlowV2Endpoint self.loginFlowV2Login = loginFlowV2Login + + poll() } func poll() { - NextcloudKit.shared.getLoginFlowV2Poll(token: self.loginFlowV2Token, endpoint: self.loginFlowV2Endpoint) { server, loginName, appPassword, _, error in - if error == .success, let urlBase = server, let user = loginName, let appPassword { - self.isLoading = true - self.appDelegate.createAccount(urlBase: urlBase, user: user, password: appPassword) { error in - if error == .success { - self.pollFinished = true + let queue = DispatchQueue.global(qos: .background) + timer = DispatchSource.makeTimerSource(queue: queue) + + guard let timer = timer else { return } + + timer.schedule(deadline: .now(), repeating: .seconds(1), leeway: .seconds(1)) + timer.setEventHandler(handler: { + DispatchQueue.main.async { + let controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController + NextcloudKit.shared.getLoginFlowV2Poll(token: self.loginFlowV2Token, endpoint: self.loginFlowV2Endpoint) { server, loginName, appPassword, _, error in + if error == .success, let urlBase = server, let user = loginName, let appPassword { + self.isLoading = true + NCAccount().createAccount(urlBase: urlBase, user: user, password: appPassword, controller: controller) { account, error in + if error == .success { + self.account = account + self.pollFinished = true + } + } } } } - } + }) + + timer.resume() + } + + func onDisappear() { + timer?.cancel() } func openLoginInBrowser() { diff --git a/iOSClient/Login/NCLoginProvider.swift b/iOSClient/Login/NCLoginProvider.swift index d43cc5dc04..add68eb699 100644 --- a/iOSClient/Login/NCLoginProvider.swift +++ b/iOSClient/Login/NCLoginProvider.swift @@ -22,13 +22,12 @@ // import UIKit -import WebKit +@preconcurrency import WebKit import NextcloudKit import FloatingPanel class NCLoginProvider: UIViewController { var webView: WKWebView? - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let utility = NCUtility() var titleView: String = "" var urlBase = "" @@ -58,24 +57,17 @@ class NCLoginProvider: UIViewController { if let host = URL(string: urlBase)?.host { titleView = host - if let account = NCManageDatabase.shared.getActiveAccount(), NCKeychain().getPassword(account: account.account).isEmpty { - titleView = NSLocalizedString("_user_", comment: "") + " " + account.userId + " " + NSLocalizedString("_in_", comment: "") + " " + host + if let activeTableAccount = NCManageDatabase.shared.getActiveTableAccount(), NCKeychain().getPassword(account: activeTableAccount.account).isEmpty { + titleView = NSLocalizedString("_user_", comment: "") + " " + activeTableAccount.userId + " " + NSLocalizedString("_in_", comment: "") + " " + host } } self.title = titleView } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - // Stop timer error network - appDelegate.timerErrorNetworkingDisabled = true - } - override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) NCActivityIndicator.shared.stop() - appDelegate.timerErrorNetworkingDisabled = false } func loadWebPage(webView: WKWebView, url: URL) { @@ -166,17 +158,27 @@ extension NCLoginProvider: WKNavigationDelegate { let account: String = "\(username) \(urlBase)" let user = username - NextcloudKit.shared.setup(account: account, user: user, userId: user, password: password, urlBase: urlBase) - NextcloudKit.shared.getUserProfile(account: account) { _, userProfile, _, error in + NextcloudKit.shared.getUserProfile(account: account) { account, userProfile, _, error in if error == .success, let userProfile { - NCManageDatabase.shared.deleteAccount(account) + NextcloudKit.shared.appendSession(account: account, + urlBase: urlBase, + user: user, + userId: user, + password: password, + userAgent: userAgent, + nextcloudVersion: NCCapabilities.shared.getCapabilities(account: account).capabilityServerVersionMajor, + groupIdentifier: NCBrandOptions.shared.capabilitiesGroup) + NCSession.shared.appendSession(account: account, urlBase: urlBase, user: user, userId: userProfile.userId) NCManageDatabase.shared.addAccount(account, urlBase: urlBase, user: user, userId: userProfile.userId, password: password) - self.appDelegate.changeAccount(account, userProfile: userProfile) { } + NCAccount().changeAccount(account, userProfile: userProfile, controller: nil) { } + let window = UIApplication.shared.firstWindow - if window?.rootViewController is NCMainTabBarController { + if let controller = window?.rootViewController as? NCMainTabBarController { + controller.account = account self.dismiss(animated: true) } else { if let controller = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? NCMainTabBarController { + controller.account = account controller.modalPresentationStyle = .fullScreen controller.view.alpha = 0 window?.rootViewController = controller diff --git a/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift b/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift index 8a315baa1d..b861d54f00 100644 --- a/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift +++ b/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift @@ -25,26 +25,32 @@ import UIKit protocol NCCellProtocol { var fileAvatarImageView: UIImageView? { get } - var fileObjectId: String? { get set } + var fileAccount: String? { get set } + var fileOcId: String? { get set } + var fileOcIdTransfer: String? { get set } var filePreviewImageView: UIImageView? { get set } - var filePreviewImageBottom: NSLayoutConstraint? { get set } var fileUser: String? { get set } var fileTitleLabel: UILabel? { get set } var fileInfoLabel: UILabel? { get set } var fileSubinfoLabel: UILabel? { get set } - var fileProgressView: UIProgressView? { get set } var fileStatusImage: UIImageView? { get set } var fileLocalImage: UIImageView? { get set } var fileFavoriteImage: UIImageView? { get set } var fileSharedImage: UIImageView? { get set } var fileMoreImage: UIImageView? { get set } var cellSeparatorView: UIView? { get set } - var indexPath: IndexPath { get set } func titleInfoTrailingDefault() func titleInfoTrailingFull() func writeInfoDateSize(date: NSDate, size: Int64) - func setButtonMore(named: String, image: UIImage) + func setButtonMore(image: UIImage) + func hideImageItem(_ status: Bool) + func hideImageFavorite(_ status: Bool) + func hideImageStatus(_ status: Bool) + func hideImageLocal(_ status: Bool) + func hideLabelTitle(_ status: Bool) + func hideLabelInfo(_ status: Bool) + func hideLabelSubinfo(_ status: Bool) func hideButtonShare(_ status: Bool) func hideButtonMore(_ status: Bool) func selected(_ status: Bool, isEditMode: Bool) @@ -54,19 +60,22 @@ protocol NCCellProtocol { } extension NCCellProtocol { - var fileAvatarImageView: UIImageView? { return nil } - var fileObjectId: String? { + var fileAccount: String? { get { return nil } set {} } - var filePreviewImageView: UIImageView? { + var fileOcId: String? { get { return nil } set {} } - var filePreviewImageBottom: NSLayoutConstraint? { + var fileOcIdTransfer: String? { + get { return nil } + set {} + } + var filePreviewImageView: UIImageView? { get { return nil } set {} } @@ -82,10 +91,6 @@ extension NCCellProtocol { get { return nil } set { } } - var fileProgressView: UIProgressView? { - get { return nil } - set {} - } var fileStatusImage: UIImageView? { get { return nil } set {} @@ -114,7 +119,14 @@ extension NCCellProtocol { func titleInfoTrailingDefault() {} func titleInfoTrailingFull() {} func writeInfoDateSize(date: NSDate, size: Int64) {} - func setButtonMore(named: String, image: UIImage) {} + func setButtonMore(image: UIImage) {} + func hideImageItem(_ status: Bool) {} + func hideImageFavorite(_ status: Bool) {} + func hideImageStatus(_ status: Bool) {} + func hideImageLocal(_ status: Bool) {} + func hideLabelTitle(_ status: Bool) {} + func hideLabelInfo(_ status: Bool) {} + func hideLabelSubinfo(_ status: Bool) {} func hideButtonShare(_ status: Bool) {} func hideButtonMore(_ status: Bool) {} func selected(_ status: Bool, isEditMode: Bool) {} diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index 2e262ed129..5c3393be06 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -35,16 +35,20 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto @IBOutlet weak var buttonMore: UIButton! @IBOutlet weak var imageVisualEffect: UIVisualEffectView! - var objectId = "" - var indexPath = IndexPath() - private var user = "" + var ocId = "" + var ocIdTransfer = "" + var account = "" + var user = "" weak var gridCellDelegate: NCGridCellDelegate? - var namedButtonMore = "" - var fileObjectId: String? { - get { return objectId } - set { objectId = newValue ?? "" } + var fileOcId: String? { + get { return ocId } + set { ocId = newValue ?? "" } + } + var fileOcIdTransfer: String? { + get { return ocIdTransfer } + set { ocIdTransfer = newValue ?? "" } } var filePreviewImageView: UIImageView? { get { return imageItem } @@ -95,25 +99,26 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto accessibilityValue = nil isAccessibilityElement = true + imageItem.image = nil imageItem.layer.cornerRadius = 6 imageItem.layer.masksToBounds = true - + imageSelect.isHidden = true + imageSelect.image = NCImageCache.shared.getImageCheckedYes() + imageStatus.image = nil + imageFavorite.image = nil + imageLocal.image = nil + labelTitle.text = "" + labelInfo.text = "" + labelSubinfo.text = "" imageVisualEffect.layer.cornerRadius = 6 imageVisualEffect.clipsToBounds = true imageVisualEffect.alpha = 0.5 - imageSelect.isHidden = true - imageSelect.image = NCImageCache.images.checkedYes - let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) longPressedGesture.minimumPressDuration = 0.5 longPressedGesture.delegate = self longPressedGesture.delaysTouchesBegan = true self.addGestureRecognizer(longPressedGesture) - - labelTitle.text = "" - labelInfo.text = "" - labelSubinfo.text = "" } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { @@ -121,30 +126,55 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } @IBAction func touchUpInsideMore(_ sender: Any) { - gridCellDelegate?.tapMoreGridItem(with: objectId, namedButtonMore: namedButtonMore, image: imageItem.image, indexPath: indexPath, sender: sender) + gridCellDelegate?.tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: imageItem.image, sender: sender) } @objc func longPress(gestureRecognizer: UILongPressGestureRecognizer) { - gridCellDelegate?.longPressGridItem(with: objectId, indexPath: indexPath, gestureRecognizer: gestureRecognizer) + gridCellDelegate?.longPressGridItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) } fileprivate func setA11yActions() { - let moreName = namedButtonMore == NCGlobal.shared.buttonMoreStop ? "_cancel_" : "_more_" - self.accessibilityCustomActions = [ UIAccessibilityCustomAction( - name: NSLocalizedString(moreName, comment: ""), + name: NSLocalizedString("_more_", comment: ""), target: self, selector: #selector(touchUpInsideMore)) ] } - func setButtonMore(named: String, image: UIImage) { - namedButtonMore = named + func setButtonMore(image: UIImage) { buttonMore.setImage(image, for: .normal) setA11yActions() } + func hideImageItem(_ status: Bool) { + imageItem.isHidden = status + } + + func hideImageFavorite(_ status: Bool) { + imageFavorite.isHidden = status + } + + func hideImageStatus(_ status: Bool) { + imageStatus.isHidden = status + } + + func hideImageLocal(_ status: Bool) { + imageLocal.isHidden = status + } + + func hideLabelTitle(_ status: Bool) { + labelTitle.isHidden = status + } + + func hideLabelInfo(_ status: Bool) { + labelInfo.isHidden = status + } + + func hideLabelSubinfo(_ status: Bool) { + labelSubinfo.isHidden = status + } + func hideButtonMore(_ status: Bool) { buttonMore.isHidden = status } @@ -159,6 +189,7 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } if status { imageSelect.isHidden = false + imageSelect.image = NCImageCache.shared.getImageCheckedYes() imageVisualEffect.isHidden = false } else { imageSelect.isHidden = true @@ -191,8 +222,8 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } protocol NCGridCellDelegate: AnyObject { - func tapMoreGridItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) - func longPressGridItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) + func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) } // MARK: - Grid Layout diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.xib b/iOSClient/Main/Collection Common/Cell/NCGridCell.xib index 8a2beec3a7..2466c28b44 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.xib @@ -4,14 +4,13 @@ - - + @@ -19,44 +18,44 @@ - + - + - - + + - + - - + + - + - - + + - + - - + + @@ -86,7 +85,7 @@ - - - + - + + + - + + - - - + - + - - - - - - + + + + + + @@ -150,13 +148,13 @@ - + - + - + diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 8f13107769..a10822b210 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -37,7 +37,6 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto @IBOutlet weak var buttonShared: UIButton! @IBOutlet weak var imageMore: UIImageView! @IBOutlet weak var buttonMore: UIButton! - @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var separator: UIView! @IBOutlet weak var tag0: UILabel! @IBOutlet weak var tag1: UILabel! @@ -47,19 +46,22 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto @IBOutlet weak var titleTrailingConstraint: NSLayoutConstraint! @IBOutlet weak var subInfoTrailingConstraint: NSLayoutConstraint! - private var objectId = "" - private var user = "" - var indexPath = IndexPath() + var ocId = "" + var ocIdTransfer = "" + var user = "" weak var listCellDelegate: NCListCellDelegate? - var namedButtonMore = "" var fileAvatarImageView: UIImageView? { return imageShared } - var fileObjectId: String? { - get { return objectId } - set { objectId = newValue ?? "" } + var fileOcId: String? { + get { return ocId } + set { ocId = newValue ?? "" } + } + var fileOcIdTransfer: String? { + get { return ocIdTransfer } + set { ocIdTransfer = newValue ?? "" } } var filePreviewImageView: UIImageView? { get { return imageItem } @@ -81,10 +83,6 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto get { return labelSubinfo } set { labelSubinfo = newValue } } - var fileProgressView: UIProgressView? { - get { return progressView } - set { progressView = newValue } - } var fileStatusImage: UIImageView? { get { return imageStatus } set { imageStatus = newValue } @@ -112,51 +110,42 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto override func awakeFromNib() { super.awakeFromNib() + initCell() + } - imageItem.layer.cornerRadius = 6 - imageItem.layer.masksToBounds = true + override func prepareForReuse() { + super.prepareForReuse() + initCell() + } - // use entire cell as accessibility element + func initCell() { accessibilityHint = nil accessibilityLabel = nil accessibilityValue = nil isAccessibilityElement = true - progressView.tintColor = NCBrandColor.shared.brandElement - progressView.transform = CGAffineTransform(scaleX: 1.0, y: 0.5) - progressView.trackTintColor = .clear + imageItem.image = nil + imageItem.layer.cornerRadius = 6 + imageItem.layer.masksToBounds = true + imageStatus.image = nil + imageFavorite.image = nil + imageFavoriteBackground.isHidden = true + imageLocal.image = nil + labelTitle.text = "" + labelInfo.text = "" + labelSubinfo.text = "" + imageShared.image = nil + imageMore.image = nil + separatorHeightConstraint.constant = 0.5 + tag0.text = "" + tag1.text = "" + titleInfoTrailingDefault() let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) longPressedGesture.minimumPressDuration = 0.5 longPressedGesture.delegate = self longPressedGesture.delaysTouchesBegan = true self.addGestureRecognizer(longPressedGesture) - - separator.backgroundColor = .separator - separatorHeightConstraint.constant = 0.5 - - labelTitle.text = "" - labelInfo.text = "" - labelSubinfo.text = "" - labelTitle.textColor = NCBrandColor.shared.textColor - labelInfo.textColor = NCBrandColor.shared.textColor2 - labelSubinfo.textColor = NCBrandColor.shared.textColor2 - - imageFavoriteBackground.isHidden = true - } - - override func prepareForReuse() { - super.prepareForReuse() - imageItem.backgroundColor = nil - if fileFavoriteImage?.image != nil { - imageFavoriteBackground.isHidden = false - } else { - imageFavoriteBackground.isHidden = true - } - - accessibilityHint = nil - accessibilityLabel = nil - accessibilityValue = nil } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { @@ -164,26 +153,25 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } @IBAction func touchUpInsideShare(_ sender: Any) { - listCellDelegate?.tapShareListItem(with: objectId, indexPath: indexPath, sender: sender) + listCellDelegate?.tapShareListItem(with: ocId, ocIdTransfer: ocIdTransfer, sender: sender) } @IBAction func touchUpInsideMore(_ sender: Any) { - listCellDelegate?.tapMoreListItem(with: objectId, namedButtonMore: namedButtonMore, image: imageItem.image, indexPath: indexPath, sender: sender) + listCellDelegate?.tapMoreListItem(with: ocId, ocIdTransfer: ocIdTransfer, image: imageItem.image, sender: sender) } @objc func longPress(gestureRecognizer: UILongPressGestureRecognizer) { - listCellDelegate?.longPressListItem(with: objectId, indexPath: indexPath, gestureRecognizer: gestureRecognizer) + listCellDelegate?.longPressListItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) } fileprivate func setA11yActions() { - let moreName = namedButtonMore == NCGlobal.shared.buttonMoreStop ? "_cancel_" : "_more_" self.accessibilityCustomActions = [ UIAccessibilityCustomAction( name: NSLocalizedString("_share_", comment: ""), target: self, selector: #selector(touchUpInsideShare)), UIAccessibilityCustomAction( - name: NSLocalizedString(moreName, comment: ""), + name: NSLocalizedString("_more_", comment: ""), target: self, selector: #selector(touchUpInsideMore)) ] @@ -199,10 +187,8 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto subInfoTrailingConstraint.constant = 90 } - func setButtonMore(named: String, image: UIImage) { - namedButtonMore = named + func setButtonMore(image: UIImage) { imageMore.image = image - setA11yActions() } @@ -216,10 +202,6 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto buttonShared.isHidden = status } - func hideSeparator(_ status: Bool) { - separator.isHidden = status - } - func selected(_ status: Bool, isEditMode: Bool) { if isEditMode { imageItemLeftConstraint.constant = 45 @@ -245,11 +227,11 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto blurEffectView?.backgroundColor = .lightGray blurEffectView?.frame = self.bounds blurEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] - imageSelect.image = NCImageCache.images.checkedYes + imageSelect.image = NCImageCache.shared.getImageCheckedYes() backgroundView = blurEffectView separator.isHidden = true } else { - imageSelect.image = NCImageCache.images.checkedNo + imageSelect.image = NCImageCache.shared.getImageCheckedNo() backgroundView = nil separator.isHidden = false } @@ -306,9 +288,9 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } protocol NCListCellDelegate: AnyObject { - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) - func tapMoreListItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) + func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) } // MARK: - List Layout diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.xib b/iOSClient/Main/Collection Common/Cell/NCListCell.xib index 8422b1cfae..3f9945744a 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.xib @@ -1,17 +1,16 @@ - + - - + - + @@ -39,6 +38,13 @@ + + + + + + + - - - - - - - + + - - - - + - - + + + - + + - - + + + + + + + + + diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index cc4403d047..664ed2765d 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -24,115 +24,107 @@ import Foundation import UIKit import NextcloudKit +import RealmSwift extension NCCollectionViewCommon: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { - return dataSource.numberOfSections() + return self.dataSource.numberOfSections() } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return dataSource.numberOfItemsInSection(section) + // get auto upload folder + self.autoUploadFileName = self.database.getAccountAutoUploadFileName() + self.autoUploadDirectory = self.database.getAccountAutoUploadDirectory(session: self.session) + // get layout for view + self.layoutForView = self.database.getLayoutForView(account: self.session.account, key: self.layoutKey, serverUrl: self.serverUrl) + + return self.dataSource.numberOfItemsInSection(section) } - func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - guard let metadata = dataSource.cellForItemAt(indexPath: indexPath), - let cell = (cell as? NCCellProtocol) else { return } - let existsIcon = utilityFileSystem.fileProviderStoragePreviewIconExists(metadata.ocId, etag: metadata.etag) - - func downloadAvatar(fileName: String, user: String, dispalyName: String?) { - if let image = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) { - cell.fileAvatarImageView?.contentMode = .scaleAspectFill - cell.fileAvatarImageView?.image = image - } else { - NCNetworking.shared.downloadAvatar(user: user, dispalyName: dispalyName, fileName: fileName, cell: cell, view: collectionView) + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + if !collectionView.indexPathsForVisibleItems.contains(indexPath) { + guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { return } + for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { + operation.cancel() } } - /// CONTENT MODE - cell.filePreviewImageView?.layer.borderWidth = 0 - if existsIcon { + } + + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return } + let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag) + let ext = global.getSizeExtension(column: self.numberOfColumns) + + if metadata.hasPreview, + !existsImagePreview, + NCNetworking.shared.downloadThumbnailQueue.operations.filter({ ($0 as? NCMediaDownloadThumbnail)?.metadata.ocId == metadata.ocId }).isEmpty { + NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, collectionView: collectionView, ext: ext)) + } + } + + private func photoCell(cell: NCPhotoCell, indexPath: IndexPath, metadata: tableMetadata, ext: String) -> NCPhotoCell { + let width = UIScreen.main.bounds.width / CGFloat(self.numberOfColumns) + + cell.ocId = metadata.ocId + cell.ocIdTransfer = metadata.ocIdTransfer + cell.hideButtonMore(true) + cell.hideImageStatus(true) + + /// Image + /// + if let image = NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + + cell.filePreviewImageView?.image = image cell.filePreviewImageView?.contentMode = .scaleAspectFill + } else { - cell.filePreviewImageView?.contentMode = .scaleAspectFit - } - cell.fileAvatarImageView?.contentMode = .center - /// THUMBNAIL - if !metadata.directory { - if metadata.hasPreviewBorder { - cell.filePreviewImageView?.layer.borderWidth = 0.2 - cell.filePreviewImageView?.layer.borderColor = UIColor.lightGray.cgColor + + if isPinchGestureActive || ext == global.previewExt512 || ext == global.previewExt1024 { + cell.filePreviewImageView?.image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) } - if metadata.name == NCGlobal.shared.appName { - if layoutForView?.layout == NCGlobal.shared.layoutPhotoRatio || layoutForView?.layout == NCGlobal.shared.layoutPhotoSquare { - if let image = NCImageCache.shared.getPreviewImageCache(ocId: metadata.ocId, etag: metadata.etag) { - cell.filePreviewImageView?.image = image - } else if let image = UIImage(contentsOfFile: self.utilityFileSystem.getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag)) { + + DispatchQueue.global(qos: .userInteractive).async { + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) + if let image { + self.imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext, cost: indexPath.row) + DispatchQueue.main.async { cell.filePreviewImageView?.image = image - NCImageCache.shared.addPreviewImageCache(metadata: metadata, image: image) + cell.filePreviewImageView?.contentMode = .scaleAspectFill } - } else { - if let image = NCImageCache.shared.getIconImageCache(ocId: metadata.ocId, etag: metadata.etag) { - cell.filePreviewImageView?.image = image - } else if metadata.hasPreview { - cell.filePreviewImageView?.image = utility.getIcon(metadata: metadata) - } - } - if cell.filePreviewImageView?.image == nil { - if metadata.iconName.isEmpty { - cell.filePreviewImageView?.image = NCImageCache.images.file - } else { - cell.filePreviewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true) - } - if metadata.hasPreview && metadata.status == NCGlobal.shared.metadataStatusNormal && !existsIcon { - for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { return } - NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, cell: cell, collectionView: collectionView)) - } - } - } else { - /// APP NAME - UNIFIED SEARCH - switch metadata.iconName { - case let str where str.contains("contacts"): - cell.filePreviewImageView?.image = NCImageCache.images.iconContacts - case let str where str.contains("conversation"): - cell.filePreviewImageView?.image = NCImageCache.images.iconTalk - case let str where str.contains("calendar"): - cell.filePreviewImageView?.image = NCImageCache.images.iconCalendar - case let str where str.contains("deck"): - cell.filePreviewImageView?.image = NCImageCache.images.iconDeck - case let str where str.contains("mail"): - cell.filePreviewImageView?.image = NCImageCache.images.iconMail - case let str where str.contains("talk"): - cell.filePreviewImageView?.image = NCImageCache.images.iconTalk - case let str where str.contains("confirm"): - cell.filePreviewImageView?.image = NCImageCache.images.iconConfirm - case let str where str.contains("pages"): - cell.filePreviewImageView?.image = NCImageCache.images.iconPages - default: - cell.filePreviewImageView?.image = NCImageCache.images.iconFile - } - if !metadata.iconUrl.isEmpty { - if let ownerId = getAvatarFromIconUrl(metadata: metadata) { - let fileName = metadata.userBaseUrl + "-" + ownerId + ".png" - downloadAvatar(fileName: fileName, user: ownerId, dispalyName: nil) + } else if !metadata.hasPreview { + DispatchQueue.main.async { + cell.filePreviewImageView?.contentMode = .scaleAspectFit + if metadata.iconName.isEmpty { + cell.filePreviewImageView?.image = NCImageCache.shared.getImageFile() + } else { + cell.filePreviewImageView?.image = self.utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + } } } } } - /// AVATAR - if !metadata.ownerId.isEmpty, - metadata.ownerId != appDelegate.userId, - appDelegate.account == metadata.account { - let fileName = metadata.userBaseUrl + "-" + metadata.ownerId + ".png" - downloadAvatar(fileName: fileName, user: metadata.ownerId, dispalyName: metadata.ownerDisplayName) + + /// Status + /// + if metadata.isLivePhoto { + cell.fileStatusImage?.image = utility.loadImage(named: "livephoto", colors: isLayoutPhoto ? [.white] : [NCBrandColor.shared.iconImageColor2]) + } else if metadata.isVideo { + cell.fileStatusImage?.image = utility.loadImage(named: "play.circle", colors: NCBrandColor.shared.iconImageMultiColors) } - } - func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - if !collectionView.indexPathsForVisibleItems.contains(indexPath) { - guard let metadata = dataSource.cellForItemAt(indexPath: indexPath) else { return } - for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { - operation.cancel() - } + /// Edit mode + if fileSelect.contains(metadata.ocId) { + cell.selected(true, isEditMode: isEditMode) + } else { + cell.selected(false, isEditMode: isEditMode) + } + + if width > 100 && cell.filePreviewImageView?.image != nil { + cell.hideButtonMore(false) + cell.hideImageStatus(false) } + + return cell } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -141,61 +133,74 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { var isShare = false var isMounted = false var a11yValues: [String] = [] + let metadata = self.dataSource.getMetadata(indexPath: indexPath) ?? tableMetadata() + let existsImagePreview = utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag) + let ext = global.getSizeExtension(column: self.numberOfColumns) + + defer { + if !metadata.isSharable() || NCCapabilities.shared.disableSharesView(account: metadata.account) { + cell.hideButtonShare(true) + } + } // LAYOUT PHOTO - if layoutForView?.layout == NCGlobal.shared.layoutPhotoRatio || layoutForView?.layout == NCGlobal.shared.layoutPhotoSquare { - guard let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as? NCPhotoCell else { return NCPhotoCell() } - photoCell.photoCellDelegate = self - cell = photoCell - } else if layoutForView?.layout == NCGlobal.shared.layoutGrid { - // LAYOUT GRID - guard let gridCell = collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell else { return NCGridCell() } + if isLayoutPhoto { + if metadata.isImageOrVideo { + let photoCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as? NCPhotoCell)! + photoCell.photoCellDelegate = self + cell = photoCell + return self.photoCell(cell: photoCell, indexPath: indexPath, metadata: metadata, ext: ext) + } else { + let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell)! + gridCell.gridCellDelegate = self + cell = gridCell + } + } else if isLayoutGrid { + // LAYOUT GRID + let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell)! gridCell.gridCellDelegate = self cell = gridCell } else { - // LAYOUT LIST - guard let listCell = collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell else { return NCListCell() } + // LAYOUT LIST + let listCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)! listCell.listCellDelegate = self cell = listCell } - guard let metadata = dataSource.cellForItemAt(indexPath: indexPath) else { return cell } - defer { - if NCGlobal.shared.disableSharesView || !metadata.isSharable() { - cell.hideButtonShare(true) - } + /// CONTENT MODE + cell.fileAvatarImageView?.contentMode = .center + cell.filePreviewImageView?.layer.borderWidth = 0 + + if existsImagePreview { + cell.filePreviewImageView?.contentMode = .scaleAspectFill + } else { + cell.filePreviewImageView?.contentMode = .scaleAspectFit } + guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { return cell } + if metadataFolder != nil { isShare = metadata.permissions.contains(permissions.permissionShared) && !metadataFolder!.permissions.contains(permissions.permissionShared) isMounted = metadata.permissions.contains(permissions.permissionMounted) && !metadataFolder!.permissions.contains(permissions.permissionMounted) } - cell.fileStatusImage?.image = nil - cell.fileLocalImage?.image = nil - cell.fileFavoriteImage?.image = nil - cell.fileSharedImage?.image = nil - cell.fileMoreImage?.image = nil - cell.filePreviewImageView?.image = nil - cell.filePreviewImageView?.backgroundColor = nil - cell.fileObjectId = metadata.ocId - cell.indexPath = indexPath + cell.fileAccount = metadata.account + cell.fileOcId = metadata.ocId + cell.fileOcIdTransfer = metadata.ocIdTransfer cell.fileUser = metadata.ownerId - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = 0.0 - cell.hideButtonShare(false) - cell.hideButtonMore(false) - cell.titleInfoTrailingDefault() if isSearchingMode { cell.fileTitleLabel?.text = metadata.fileName cell.fileTitleLabel?.lineBreakMode = .byTruncatingTail - if metadata.name == NCGlobal.shared.appName { + if metadata.name == global.appName { cell.fileInfoLabel?.text = NSLocalizedString("_in_", comment: "") + " " + utilityFileSystem.getPath(path: metadata.path, user: metadata.user) } else { cell.fileInfoLabel?.text = metadata.subline } cell.fileSubinfoLabel?.isHidden = true + } else if !metadata.sessionError.isEmpty, metadata.status != global.metadataStatusNormal { + cell.fileSubinfoLabel?.isHidden = false + cell.fileInfoLabel?.text = metadata.sessionError } else { cell.fileSubinfoLabel?.isHidden = false cell.fileTitleLabel?.text = metadata.fileNameView @@ -203,115 +208,188 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { cell.writeInfoDateSize(date: metadata.date, size: metadata.size) } - if metadata.status == NCGlobal.shared.metadataStatusDownloading || metadata.status == NCGlobal.shared.metadataStatusUploading { - cell.fileProgressView?.isHidden = false - } - - // Accessibility [shared] - if metadata.ownerId != appDelegate.userId, appDelegate.account == metadata.account { + // Accessibility [shared] if metadata.ownerId != appDelegate.userId, appDelegate.account == metadata.account { + if metadata.ownerId != metadata.userId { a11yValues.append(NSLocalizedString("_shared_with_you_by_", comment: "") + " " + metadata.ownerDisplayName) } if metadata.directory { - let tableDirectory = NCManageDatabase.shared.getTableDirectory(ocId: metadata.ocId) + let tableDirectory = database.getTableDirectory(ocId: metadata.ocId) if metadata.e2eEncrypted { - cell.filePreviewImageView?.image = NCImageCache.images.folderEncrypted + cell.filePreviewImageView?.image = imageCache.getFolderEncrypted(account: metadata.account) } else if isShare { - cell.filePreviewImageView?.image = NCImageCache.images.folderSharedWithMe + cell.filePreviewImageView?.image = imageCache.getFolderSharedWithMe(account: metadata.account) } else if !metadata.shareType.isEmpty { metadata.shareType.contains(3) ? - (cell.filePreviewImageView?.image = NCImageCache.images.folderPublic) : - (cell.filePreviewImageView?.image = NCImageCache.images.folderSharedWithMe) + (cell.filePreviewImageView?.image = imageCache.getFolderPublic(account: metadata.account)) : + (cell.filePreviewImageView?.image = imageCache.getFolderSharedWithMe(account: metadata.account)) } else if !metadata.shareType.isEmpty && metadata.shareType.contains(3) { - cell.filePreviewImageView?.image = NCImageCache.images.folderPublic + cell.filePreviewImageView?.image = imageCache.getFolderPublic(account: metadata.account) } else if metadata.mountType == "group" { - cell.filePreviewImageView?.image = NCImageCache.images.folderGroup + cell.filePreviewImageView?.image = imageCache.getFolderGroup(account: metadata.account) } else if isMounted { - cell.filePreviewImageView?.image = NCImageCache.images.folderExternal + cell.filePreviewImageView?.image = imageCache.getFolderExternal(account: metadata.account) } else if metadata.fileName == autoUploadFileName && metadata.serverUrl == autoUploadDirectory { - cell.filePreviewImageView?.image = NCImageCache.images.folderAutomaticUpload + cell.filePreviewImageView?.image = imageCache.getFolderAutomaticUpload(account: metadata.account) } else { - cell.filePreviewImageView?.image = NCImageCache.images.folder + cell.filePreviewImageView?.image = imageCache.getFolder(account: metadata.account) } // Local image: offline if let tableDirectory, tableDirectory.offline { - cell.fileLocalImage?.image = NCImageCache.images.offlineFlag + cell.fileLocalImage?.image = imageCache.getImageOfflineFlag() } // color folder cell.filePreviewImageView?.image = cell.filePreviewImageView?.image?.colorizeFolder(metadata: metadata, tableDirectory: tableDirectory) + } else { - let tableLocalFile = NCManageDatabase.shared.getResultsTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId))?.first + + if metadata.hasPreviewBorder { + cell.filePreviewImageView?.layer.borderWidth = 0.2 + cell.filePreviewImageView?.layer.borderColor = UIColor.lightGray.cgColor + } + + if metadata.name == global.appName { + if let image = NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + cell.filePreviewImageView?.image = image + } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + cell.filePreviewImageView?.image = image + } + + if cell.filePreviewImageView?.image == nil { + if metadata.iconName.isEmpty { + cell.filePreviewImageView?.image = NCImageCache.shared.getImageFile() + } else { + cell.filePreviewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + } + } + } else { + /// APP NAME - UNIFIED SEARCH + switch metadata.iconName { + case let str where str.contains("contacts"): + cell.filePreviewImageView?.image = utility.loadImage(named: "person.crop.rectangle.stack", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("conversation"): + cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) + case let str where str.contains("calendar"): + cell.filePreviewImageView?.image = utility.loadImage(named: "calendar", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("deck"): + cell.filePreviewImageView?.image = utility.loadImage(named: "square.stack.fill", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("mail"): + cell.filePreviewImageView?.image = utility.loadImage(named: "mail", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("talk"): + cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) + case let str where str.contains("confirm"): + cell.filePreviewImageView?.image = utility.loadImage(named: "arrow.right", colors: [NCBrandColor.shared.iconImageColor]) + case let str where str.contains("pages"): + cell.filePreviewImageView?.image = utility.loadImage(named: "doc.richtext", colors: [NCBrandColor.shared.iconImageColor]) + default: + cell.filePreviewImageView?.image = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor]) + } + if !metadata.iconUrl.isEmpty { + if let ownerId = getAvatarFromIconUrl(metadata: metadata) { + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: ownerId) + let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) + if results.image == nil { + cell.filePreviewImageView?.image = utility.loadUserImage(for: ownerId, displayName: nil, urlBase: metadata.urlBase) + } else { + cell.filePreviewImageView?.image = results.image + } + if !(results.tableAvatar?.loaded ?? false), + NCNetworking.shared.downloadAvatarQueue.operations.filter({ ($0 as? NCOperationDownloadAvatar)?.fileName == fileName }).isEmpty { + NCNetworking.shared.downloadAvatarQueue.addOperation(NCOperationDownloadAvatar(user: ownerId, fileName: fileName, account: metadata.account, view: collectionView, isPreviewImageView: true)) + } + } + } + } + + let tableLocalFile = database.getResultsTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId))?.first // image local if let tableLocalFile, tableLocalFile.offline { a11yValues.append(NSLocalizedString("_offline_", comment: "")) - cell.fileLocalImage?.image = NCImageCache.images.offlineFlag + cell.fileLocalImage?.image = imageCache.getImageOfflineFlag() } else if utilityFileSystem.fileProviderStorageExists(metadata) { - cell.fileLocalImage?.image = NCImageCache.images.local + cell.fileLocalImage?.image = imageCache.getImageLocal() } } // image Favorite if metadata.favorite { - cell.fileFavoriteImage?.image = NCImageCache.images.favorite + cell.fileFavoriteImage?.image = imageCache.getImageFavorite() a11yValues.append(NSLocalizedString("_favorite_short_", comment: "")) } // Share image if isShare { - cell.fileSharedImage?.image = NCImageCache.images.shared + cell.fileSharedImage?.image = imageCache.getImageShared() } else if !metadata.shareType.isEmpty { metadata.shareType.contains(3) ? - (cell.fileSharedImage?.image = NCImageCache.images.shareByLink) : - (cell.fileSharedImage?.image = NCImageCache.images.shared) + (cell.fileSharedImage?.image = imageCache.getImageShareByLink()) : + (cell.fileSharedImage?.image = imageCache.getImageShared()) } else { - cell.fileSharedImage?.image = NCImageCache.images.canShare - } - if appDelegate.account != metadata.account { - cell.fileSharedImage?.image = NCImageCache.images.shared + cell.fileSharedImage?.image = imageCache.getImageCanShare() } // Button More - if metadata.isInTransfer || metadata.isWaitingTransfer { - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - } else if metadata.lock == true { - cell.setButtonMore(named: NCGlobal.shared.buttonMoreLock, image: NCImageCache.images.buttonMoreLock) + if metadata.lock == true { + cell.setButtonMore(image: imageCache.getImageButtonMoreLock()) a11yValues.append(String(format: NSLocalizedString("_locked_by_", comment: ""), metadata.lockOwnerDisplayName)) } else { - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + cell.setButtonMore(image: imageCache.getImageButtonMore()) } - // Write status on Label Info + // Staus + if metadata.isLivePhoto { + cell.fileStatusImage?.image = utility.loadImage(named: "livephoto", colors: isLayoutPhoto ? [.white] : [NCBrandColor.shared.iconImageColor2]) + a11yValues.append(NSLocalizedString("_upload_mov_livephoto_", comment: "")) + } else if metadata.isVideo { + cell.fileStatusImage?.image = utility.loadImage(named: "play.circle", colors: NCBrandColor.shared.iconImageMultiColors) + } switch metadata.status { + case NCGlobal.shared.metadataStatusWaitCreateFolder: + cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) + cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_create_folder_", comment: "") + case NCGlobal.shared.metadataStatusWaitFavorite: + cell.fileStatusImage?.image = utility.loadImage(named: "star.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_favorite_", comment: "") + case NCGlobal.shared.metadataStatusWaitCopy: + cell.fileStatusImage?.image = utility.loadImage(named: "c.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_copy_", comment: "") + case NCGlobal.shared.metadataStatusWaitMove: + cell.fileStatusImage?.image = utility.loadImage(named: "m.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_move_", comment: "") + case NCGlobal.shared.metadataStatusWaitRename: + cell.fileStatusImage?.image = utility.loadImage(named: "a.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_rename_", comment: "") case NCGlobal.shared.metadataStatusWaitDownload: - cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) - cell.fileSubinfoLabel?.text = infoLabelsSeparator + NSLocalizedString("_status_wait_download_", comment: "") + cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) case NCGlobal.shared.metadataStatusDownloading: - cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) - cell.fileSubinfoLabel?.text = infoLabelsSeparator + "↓ …" - case NCGlobal.shared.metadataStatusWaitUpload: - cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) - cell.fileSubinfoLabel?.text = infoLabelsSeparator + NSLocalizedString("_status_wait_upload_", comment: "") - cell.fileLocalImage?.image = nil - case NCGlobal.shared.metadataStatusUploading: - cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) - cell.fileSubinfoLabel?.text = infoLabelsSeparator + "↑ …" - cell.fileLocalImage?.image = nil - case NCGlobal.shared.metadataStatusUploadError: - if metadata.sessionError.isEmpty { - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_upload_", comment: "") - } else { - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_upload_", comment: "") + " " + metadata.sessionError + if #available(iOS 17.0, *) { + cell.fileStatusImage?.image = utility.loadImage(named: "arrowshape.down.circle", colors: NCBrandColor.shared.iconImageMultiColors) } + case NCGlobal.shared.metadataStatusDownloadError, NCGlobal.shared.metadataStatusUploadError: + cell.fileStatusImage?.image = utility.loadImage(named: "exclamationmark.circle", colors: NCBrandColor.shared.iconImageMultiColors) default: break } - // Live Photo - if metadata.isLivePhoto { - cell.fileStatusImage?.image = NCImageCache.images.livePhoto - a11yValues.append(NSLocalizedString("_upload_mov_livephoto_", comment: "")) + // AVATAR + if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { + cell.fileAvatarImageView?.contentMode = .scaleAspectFill + + let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) + let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) + + if results.image == nil { + cell.fileAvatarImageView?.image = utility.loadUserImage(for: metadata.ownerId, displayName: metadata.ownerDisplayName, urlBase: metadata.urlBase) + } else { + cell.fileAvatarImageView?.image = results.image + } + + if !(results.tableAvatar?.loaded ?? false), + NCNetworking.shared.downloadAvatarQueue.operations.filter({ ($0 as? NCOperationDownloadAvatar)?.fileName == fileName }).isEmpty { + NCNetworking.shared.downloadAvatarQueue.addOperation(NCOperationDownloadAvatar(user: metadata.ownerId, fileName: fileName, account: metadata.account, view: collectionView)) + } } // URL @@ -332,7 +410,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } // Edit mode - if selectOcId.contains(metadata.ocId) { + if fileSelect.contains(metadata.ocId) { cell.selected(true, isEditMode: isEditMode) a11yValues.append(NSLocalizedString("_selected_", comment: "")) } else { @@ -353,22 +431,38 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { cell.fileTitleLabel?.attributedText = attributedString } + // TAGS + cell.setTags(tags: Array(metadata.tags)) + // Layout photo - if layoutForView?.layout == NCGlobal.shared.layoutPhotoRatio || layoutForView?.layout == NCGlobal.shared.layoutPhotoSquare { - if metadata.directory { - cell.filePreviewImageBottom?.constant = 10 - cell.fileTitleLabel?.text = metadata.fileNameView - } else { - cell.filePreviewImageBottom?.constant = 0 - cell.fileTitleLabel?.text = "" + if isLayoutPhoto { + let width = UIScreen.main.bounds.width / CGFloat(self.numberOfColumns) + + cell.hideImageFavorite(false) + cell.hideImageLocal(false) + cell.hideImageItem(false) + cell.hideButtonMore(false) + cell.hideLabelInfo(false) + cell.hideLabelSubinfo(false) + cell.hideImageStatus(false) + cell.fileTitleLabel?.font = UIFont.systemFont(ofSize: 15) + + if width < 120 { + cell.hideImageFavorite(true) + cell.hideImageLocal(true) + cell.fileTitleLabel?.font = UIFont.systemFont(ofSize: 10) + if width < 100 { + cell.hideImageItem(true) + cell.hideButtonMore(true) + cell.hideLabelInfo(true) + cell.hideLabelSubinfo(true) + cell.hideImageStatus(true) + } } } - // TAGS - cell.setTags(tags: Array(metadata.tags)) - // Hide buttons - if metadata.name != NCGlobal.shared.appName { + if metadata.name != global.appName { cell.titleInfoTrailingFull() cell.hideButtonShare(true) cell.hideButtonMore(true) @@ -379,24 +473,20 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - if kind == UICollectionView.elementKindSectionHeader || kind == mediaSectionHeader { - - if dataSource.getMetadataSourceForAllSections().isEmpty { - + if self.dataSource.isEmpty() { guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFirstHeaderEmptyData", for: indexPath) as? NCSectionFirstHeaderEmptyData else { return NCSectionFirstHeaderEmptyData() } self.sectionFirstHeaderEmptyData = header header.delegate = self - if !isSearchingMode, headerMenuTransferView, let ocId = NCNetworking.shared.transferInForegorund?.ocId { - let text = String(format: NSLocalizedString("_upload_foreground_msg_", comment: ""), NCBrandOptions.shared.brand) - header.setViewTransfer(isHidden: false, ocId: ocId, text: text, progress: NCNetworking.shared.transferInForegorund?.progress) + if !isSearchingMode, headerMenuTransferView, isHeaderMenuTransferViewEnabled() != nil { + header.setViewTransfer(isHidden: false) } else { header.setViewTransfer(isHidden: true) } if isSearchingMode { - header.emptyImage.image = utility.loadImage(named: "magnifyingglass", colors: [NCBrandColor.shared.brandElement]) + header.emptyImage.image = utility.loadImage(named: "magnifyingglass", colors: [NCBrandColor.shared.getElement(account: session.account)]) if self.dataSourceTask?.state == .running { header.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "") } else { @@ -404,33 +494,38 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } header.emptyDescription.text = NSLocalizedString("_search_instruction_", comment: "") } else if self.dataSourceTask?.state == .running { - header.emptyImage.image = utility.loadImage(named: "wifi", colors: [NCBrandColor.shared.brandElement]) + header.emptyImage.image = utility.loadImage(named: "wifi", colors: [NCBrandColor.shared.getElement(account: session.account)]) header.emptyTitle.text = NSLocalizedString("_request_in_progress_", comment: "") header.emptyDescription.text = "" } else { if serverUrl.isEmpty { - header.emptyImage.image = emptyImage + if let emptyImageName { + header.emptyImage.image = utility.loadImage(named: emptyImageName, colors: emptyImageColors != nil ? emptyImageColors : [NCBrandColor.shared.getElement(account: session.account)]) + } else { + header.emptyImage.image = imageCache.getFolder(account: session.account) + } header.emptyTitle.text = NSLocalizedString(emptyTitle, comment: "") header.emptyDescription.text = NSLocalizedString(emptyDescription, comment: "") + } else if metadataFolder?.status == global.metadataStatusWaitCreateFolder { + header.emptyImage.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: [NCBrandColor.shared.getElement(account: session.account)]) + header.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") + header.emptyDescription.text = NSLocalizedString("_folder_offline_desc_", comment: "") } else { - header.emptyImage.image = NCImageCache.images.folder + header.emptyImage.image = imageCache.getFolder(account: session.account) header.emptyTitle.text = NSLocalizedString("_files_no_files_", comment: "") header.emptyDescription.text = NSLocalizedString("_no_file_pull_down_", comment: "") } } - return header } else if indexPath.section == 0 { - guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFirstHeader", for: indexPath) as? NCSectionFirstHeader else { return NCSectionFirstHeader() } let (_, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: indexPath.section) self.sectionFirstHeader = header header.delegate = self - if !isSearchingMode, headerMenuTransferView, let ocId = NCNetworking.shared.transferInForegorund?.ocId { - let text = String(format: NSLocalizedString("_upload_foreground_msg_", comment: ""), NCBrandOptions.shared.brand) - header.setViewTransfer(isHidden: false, ocId: ocId, text: text, progress: NCNetworking.shared.transferInForegorund?.progress) + if !isSearchingMode, headerMenuTransferView, isHeaderMenuTransferViewEnabled() != nil { + header.setViewTransfer(isHidden: false) } else { header.setViewTransfer(isHidden: true) } @@ -447,9 +542,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { header.labelSection.textColor = NCBrandColor.shared.textColor return header - } else { - guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionHeader", for: indexPath) as? NCSectionHeader else { return NCSectionHeader() } header.labelSection.text = self.dataSource.getSectionValueLocalization(indexPath: indexPath) @@ -457,11 +550,9 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { return header } - } else { - guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFooter", for: indexPath) as? NCSectionFooter else { return NCSectionFooter() } - let sections = dataSource.numberOfSections() + let sections = self.dataSource.numberOfSections() let section = indexPath.section let metadataForSection = self.dataSource.getMetadataForSection(indexPath.section) let isPaginated = metadataForSection?.lastSearchResult?.isPaginated ?? false @@ -496,13 +587,12 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } } else { if sections == 1 || section == sections - 1 { - let info = dataSource.getFooterInformationAllMetadatas() + let info = self.dataSource.getFooterInformation() footer.setTitleLabel(directories: info.directories, files: info.files, size: info.size) } else { footer.separatorIsHidden(false) } } - return footer } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift new file mode 100644 index 0000000000..f3a87bb39e --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift @@ -0,0 +1,49 @@ +// +// NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift +// Nextcloud +// +// Created by Marino Faggiana on 16/09/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import UIKit + +extension NCCollectionViewCommon: UICollectionViewDataSourcePrefetching { + func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { + let ext = global.getSizeExtension(column: self.numberOfColumns) + guard !(self is NCTransfers), + !isSearchingMode, + imageCache.allowExtensions(ext: ext) + else { return } + + let cost = indexPaths.first?.row ?? 0 + + DispatchQueue.global().async { + for indexPath in indexPaths { + if let metadata = self.dataSource.getMetadata(indexPath: indexPath), + metadata.isImageOrVideo, + self.imageCache.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) == nil, + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { + + self.imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext, cost: cost) + } + } + } + } +} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 81539b7c19..9eb00f7326 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -27,29 +27,32 @@ import NextcloudKit extension NCCollectionViewCommon: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let metadata = dataSource.cellForItemAt(indexPath: indexPath), !metadata.isInvalidated else { return } + guard let metadata = self.dataSource.getMetadata(indexPath: indexPath), + !metadata.isInvalidated, + (metadata.name == global.appName || metadata.name == NCGlobal.shared.talkName) + else { return } if isEditMode { - if let index = selectOcId.firstIndex(of: metadata.ocId) { - selectOcId.remove(at: index) + if let index = fileSelect.firstIndex(of: metadata.ocId) { + fileSelect.remove(at: index) } else { - selectOcId.append(metadata.ocId) + fileSelect.append(metadata.ocId) } collectionView.reloadItems(at: [indexPath]) - tabBarSelect.update(selectOcId: selectOcId, metadatas: getSelectedMetadatas(), userId: appDelegate.userId) + tabBarSelect.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: metadata.userId) return } if metadata.e2eEncrypted { - if NCGlobal.shared.capabilityE2EEEnabled { - if !NCKeychain().isEndToEndEnabled(account: appDelegate.account) { + if NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityE2EEEnabled { + if !NCKeychain().isEndToEndEnabled(account: metadata.account) { let e2ee = NCEndToEndInitialize() e2ee.delegate = self - e2ee.initEndToEndEncryption(viewController: self.tabBarController, metadata: metadata) + e2ee.initEndToEndEncryption(controller: self.controller, metadata: metadata) return } } else { - NCContentPresenter().showInfo(error: NKError(errorCode: NCGlobal.shared.errorE2EENotEnabled, errorDescription: "_e2e_server_disabled_")) + NCContentPresenter().showInfo(error: NKError(errorCode: global.errorE2EENotEnabled, errorDescription: "_e2e_server_disabled_")) return } } @@ -57,43 +60,51 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { if metadata.directory { pushMetadata(metadata) } else { - let imageIcon = UIImage(contentsOfFile: utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)) - if !metadata.isDirectoryE2EE && (metadata.isImage || metadata.isAudioOrVideo) { - var metadatas: [tableMetadata] = [] - for metadata in dataSource.getMetadataSourceForAllSections() { - if metadata.isImage || metadata.isAudioOrVideo { - metadatas.append(metadata) - } - } - return NCViewer().view(viewController: self, metadata: metadata, metadatas: metadatas, imageIcon: imageIcon) - } else if metadata.isAvailableEditorView || utilityFileSystem.fileProviderStorageExists(metadata) { - NCViewer().view(viewController: self, metadata: metadata, metadatas: [metadata], imageIcon: imageIcon) + let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) + if !metadata.isDirectoryE2EE, (metadata.isImage || metadata.isAudioOrVideo) { + let metadatas = self.dataSource.getMetadatas() + let ocIds = metadatas.filter { $0.classFile == NKCommon.TypeClassFile.image.rawValue || + $0.classFile == NKCommon.TypeClassFile.video.rawValue || + $0.classFile == NKCommon.TypeClassFile.audio.rawValue }.map(\.ocId) + + return NCViewer().view(viewController: self, metadata: metadata, ocIds: ocIds, image: image) + + } else if metadata.isAvailableEditorView || + utilityFileSystem.fileProviderStorageExists(metadata) || + metadata.name == NCGlobal.shared.talkName { + + NCViewer().view(viewController: self, metadata: metadata, image: image) + } else if NextcloudKit.shared.isNetworkReachable(), - let metadata = NCManageDatabase.shared.setMetadatasSessionInWaitDownload(metadatas: [metadata], - session: NextcloudKit.shared.nkCommonInstance.sessionIdentifierDownload, - selector: NCGlobal.shared.selectorLoadFileView, - sceneIdentifier: (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier) { + let metadata = database.setMetadatasSessionInWaitDownload(metadatas: [metadata], + session: NCNetworking.shared.sessionDownload, + selector: global.selectorLoadFileView, + sceneIdentifier: self.controller?.sceneIdentifier) { + NCNetworking.shared.download(metadata: metadata, withNotificationProgressTask: true) } else { - let error = NKError(errorCode: NCGlobal.shared.errorOffline, errorDescription: "_go_online_") + let error = NKError(errorCode: global.errorOffline, errorDescription: "_go_online_") + NCContentPresenter().showInfo(error: error) } } } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - guard let metadata = dataSource.cellForItemAt(indexPath: indexPath) else { return nil } + guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { return nil } if isEditMode || metadata.classFile == NKCommon.TypeClassFile.url.rawValue { return nil } let identifier = indexPath as NSCopying - var image: UIImage? - let cell = collectionView.cellForItem(at: indexPath) + var image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: global.previewExt1024) - if cell is NCListCell { - image = (cell as? NCListCell)?.imageItem.image - } else if cell is NCGridCell { - image = (cell as? NCGridCell)?.imageItem.image - } else if cell is NCPhotoCell { - image = (cell as? NCPhotoCell)?.imageItem.image + if image == nil { + let cell = collectionView.cellForItem(at: indexPath) + if cell is NCListCell { + image = (cell as? NCListCell)?.imageItem.image + } else if cell is NCGridCell { + image = (cell as? NCGridCell)?.imageItem.image + } else if cell is NCPhotoCell { + image = (cell as? NCPhotoCell)?.imageItem.image + } } return UIContextMenuConfiguration(identifier: identifier, previewProvider: { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift index 9b02c08b48..b0e22f12c5 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+DragDrop.swift @@ -22,16 +22,16 @@ // import Foundation +import UIKit import NextcloudKit -import JGProgressHUD // MARK: - Drag extension NCCollectionViewCommon: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { if isEditMode { - return NCDragDrop().performDrag(selectOcId: selectOcId) - } else if let metadata = dataSource.cellForItemAt(indexPath: indexPath) { + return NCDragDrop().performDrag(fileSelect: fileSelect) + } else if let metadata = self.dataSource.getMetadata(indexPath: indexPath) { return NCDragDrop().performDrag(metadata: metadata) } return [] @@ -40,7 +40,7 @@ extension NCCollectionViewCommon: UICollectionViewDragDelegate { func collectionView(_ collectionView: UICollectionView, dragPreviewParametersForItemAt indexPath: IndexPath) -> UIDragPreviewParameters? { let previewParameters = UIDragPreviewParameters() - if layoutForView?.layout == NCGlobal.shared.layoutList, + if isLayoutList, let cell = collectionView.cellForItem(at: indexPath) as? NCListCell { let width = (collectionView.frame.width / 3) * 2 previewParameters.visiblePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: width, height: cell.frame.height), cornerRadius: 10) @@ -66,8 +66,8 @@ extension NCCollectionViewCommon: UICollectionViewDropDelegate { func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { var destinationMetadata: tableMetadata? - if let destinationIndexPath { - destinationMetadata = dataSource.cellForItemAt(indexPath: destinationIndexPath) + if let destinationIndexPath, let metadata = self.dataSource.getMetadata(indexPath: destinationIndexPath) { + destinationMetadata = metadata } DragDropHover.shared.destinationMetadata = destinationMetadata @@ -81,7 +81,7 @@ extension NCCollectionViewCommon: UICollectionViewDropDelegate { return UICollectionViewDropProposal(operation: .forbidden) } } else { - if serverUrl.isEmpty || NCUtilityFileSystem().isDirectoryE2EE(account: appDelegate.account, urlBase: appDelegate.urlBase, userId: appDelegate.userId, serverUrl: serverUrl) { + if serverUrl.isEmpty || NCUtilityFileSystem().isDirectoryE2EE(serverUrl: serverUrl, account: self.session.account) { DragDropHover.shared.cleanPushDragDropHover() return UICollectionViewDropProposal(operation: .forbidden) } @@ -97,7 +97,7 @@ extension NCCollectionViewCommon: UICollectionViewDropDelegate { if let destinationIndexPath, DragDropHover.shared.pushIndexPath == destinationIndexPath, DragDropHover.shared.pushCollectionView == collectionView, - let metadata = self.dataSource.cellForItemAt(indexPath: destinationIndexPath), + let metadata = self.dataSource.getMetadata(indexPath: destinationIndexPath), metadata.directory { DragDropHover.shared.cleanPushDragDropHover() self.pushMetadata(metadata) @@ -111,7 +111,12 @@ extension NCCollectionViewCommon: UICollectionViewDropDelegate { DragDropHover.shared.cleanPushDragDropHover() DragDropHover.shared.sourceMetadatas = nil - if let metadatas = NCDragDrop().performDrop(collectionView, performDropWith: coordinator, serverUrl: self.serverUrl, isImageVideo: false) { + if let metadatas = NCDragDrop().performDrop(collectionView, performDropWith: coordinator, serverUrl: self.serverUrl, isImageVideo: false, controller: self.controller) { + // TODO: NOT POSSIBLE DRAG DROP DIFFERENT ACCOUNT + if let metadata = metadatas.first, + metadata.account != self.session.account { + return + } DragDropHover.shared.sourceMetadatas = metadatas openMenu(collectionView: collectionView, location: coordinator.session.location(in: collectionView)) } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+EasyTipView.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+EasyTipView.swift index 562d2f40e5..96a04ad106 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+EasyTipView.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+EasyTipView.swift @@ -27,13 +27,13 @@ import EasyTipView extension NCCollectionViewCommon: EasyTipViewDelegate { func showTip() { - guard !appDelegate.account.isEmpty, + guard !session.account.isEmpty, self is NCFiles, self.view.window != nil, !NCBrandOptions.shared.disable_multiaccount, - self.serverUrl == utilityFileSystem.getHomeServer(urlBase: appDelegate.urlBase, userId: appDelegate.userId), + self.serverUrl == utilityFileSystem.getHomeServer(session: session), let view = self.navigationItem.leftBarButtonItem?.customView, - !NCManageDatabase.shared.tipExists(NCGlobal.shared.tipNCCollectionViewCommonAccountRequest) else { return } + !database.tipExists(global.tipNCCollectionViewCommonAccountRequest) else { return } var preferences = EasyTipView.Preferences() preferences.drawing.foregroundColor = .white @@ -55,14 +55,14 @@ extension NCCollectionViewCommon: EasyTipViewDelegate { } func easyTipViewDidTap(_ tipView: EasyTipView) { - NCManageDatabase.shared.addTip(NCGlobal.shared.tipNCCollectionViewCommonAccountRequest) + database.addTip(global.tipNCCollectionViewCommonAccountRequest) } func easyTipViewDidDismiss(_ tipView: EasyTipView) { } func dismissTip() { - if !NCManageDatabase.shared.tipExists(NCGlobal.shared.tipNCCollectionViewCommonAccountRequest) { - NCManageDatabase.shared.addTip(NCGlobal.shared.tipNCCollectionViewCommonAccountRequest) + if !database.tipExists(global.tipNCCollectionViewCommonAccountRequest) { + database.addTip(global.tipNCCollectionViewCommonAccountRequest) } appDelegate.tipView?.dismiss() appDelegate.tipView = nil diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift index f5c042f1ef..ac95c0893e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+MediaLayout.swift @@ -27,11 +27,15 @@ import NextcloudKit extension NCCollectionViewCommon: NCMediaLayoutDelegate { func getColumnCount() -> Int { - if let column = self.layoutForView?.columnPhoto, column > 0 { - return column + if self.numberOfColumns == 0, + let layoutForView = database.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewFiles, serverUrl: self.serverUrl) { + if layoutForView.columnPhoto > 0 { + self.numberOfColumns = layoutForView.columnPhoto + } else { + self.numberOfColumns = 3 + } } - self.layoutForView?.columnPhoto = 3 - return 3 + return self.numberOfColumns } func getLayout() -> String? { @@ -62,17 +66,17 @@ extension NCCollectionViewCommon: NCMediaLayoutDelegate { return 1.0 } - func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath, columnCount: Int, typeLayout: String) -> CGSize { - let size = CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount)) - if typeLayout == NCGlobal.shared.layoutPhotoRatio { - let metadata = self.dataSource.metadatas[indexPath.row] + func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath, columnCount: Int, typeLayout: String) -> CGSize { + if typeLayout == NCGlobal.shared.layoutPhotoSquare { + return CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount)) + } else { + guard let metadata = dataSource.getMetadata(indexPath: indexPath) else { return .zero } if metadata.imageSize != CGSize.zero { return metadata.imageSize - } else if let size = NCImageCache.shared.getPreviewSizeCache(ocId: metadata.ocId, etag: metadata.etag) { - return size + } else { + return CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount)) } } - return size } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift index 39e0dcebcc..019c3ce4fc 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift @@ -27,13 +27,13 @@ import NextcloudKit extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func selectAll() { - if !selectOcId.isEmpty, dataSource.getMetadataSourceForAllSections().count == selectOcId.count { - selectOcId = [] + if !fileSelect.isEmpty, self.dataSource.getMetadatas().count == fileSelect.count { + fileSelect = [] } else { - selectOcId = dataSource.getMetadataSourceForAllSections().compactMap({ $0.ocId }) + fileSelect = self.dataSource.getMetadatas().compactMap({ $0.ocId }) } - tabBarSelect.update(selectOcId: selectOcId, metadatas: getSelectedMetadatas(), userId: appDelegate.userId) - collectionView.reloadData() + tabBarSelect.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: session.userId) + self.reloadDataSource() } func delete() { @@ -44,19 +44,9 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { let canDeleteServer = metadatas.allSatisfy { !$0.lock } if canDeleteServer { - let copyMetadatas = metadatas alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .destructive) { _ in - Task { - var error = NKError() - var ocId: [String] = [] - for metadata in copyMetadatas where error == .success { - error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: false) - if error == .success { - ocId.append(metadata.ocId) - } - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "onlyLocalCache": false, "error": error]) - } + NCNetworking.shared.deleteMetadatas(metadatas, sceneIdentifier: self.controller?.sceneIdentifier) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) self.setEditMode(false) }) } @@ -68,17 +58,14 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { var error = NKError() var ocId: [String] = [] for metadata in copyMetadatas where error == .success { - error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: true) + error = await NCNetworking.shared.deleteCache(metadata, sceneIdentifier: self.controller?.sceneIdentifier) if error == .success { ocId.append(metadata.ocId) } } - if error != .success { - NCContentPresenter().showError(error: error) - } - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "onlyLocalCache": true, "error": error]) - self.setEditMode(false) + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "error": error]) } + self.setEditMode(false) }) alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) { (_: UIAlertAction) in }) @@ -88,13 +75,13 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func move() { let metadatas = getSelectedMetadatas() - NCActionCenter.shared.openSelectView(items: metadatas, controller: self.tabBarController as? NCMainTabBarController) + NCActionCenter.shared.openSelectView(items: metadatas, controller: self.controller) setEditMode(false) } func share() { let metadatas = getSelectedMetadatas() - NCActionCenter.shared.openActivityViewController(selectedMetadata: metadatas, controller: self.tabBarController as? NCMainTabBarController) + NCActionCenter.shared.openActivityViewController(selectedMetadata: metadatas, controller: self.controller) setEditMode(false) } @@ -127,8 +114,8 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func getSelectedMetadatas() -> [tableMetadata] { var selectedMetadatas: [tableMetadata] = [] - for ocId in selectOcId { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { continue } + for ocId in fileSelect { + guard let metadata = database.getMetadataFromOcId(ocId) else { continue } selectedMetadatas.append(metadata) } return selectedMetadatas @@ -136,7 +123,7 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { func setEditMode(_ editMode: Bool) { isEditMode = editMode - selectOcId.removeAll() + fileSelect.removeAll() if editMode { navigationItem.leftBarButtonItems = nil @@ -148,6 +135,6 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { navigationController?.interactivePopGestureRecognizer?.isEnabled = !editMode navigationItem.hidesBackButton = editMode searchController(enabled: !editMode) - collectionView.reloadData() + self.reloadDataSource() } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift deleted file mode 100644 index a9b5b77b97..0000000000 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift -// Nextcloud -// -// Created by Milen on 01.03.24. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation -import SwipeCellKit - -extension NCCollectionViewCommon: SwipeCollectionViewCellDelegate { - func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeCellKit.SwipeActionsOrientation) -> [SwipeCellKit.SwipeAction]? { - guard orientation == .right, let metadata = self.dataSource.cellForItemAt(indexPath: indexPath) else { return nil } - - let scaleTransition = ScaleTransition(duration: 0.3, initialScale: 0.8, threshold: 0.8) - - // wait a fix for truncate the text .. ? .. - let favoriteAction = SwipeAction(style: .default, title: NSLocalizedString(metadata.favorite ? "_favorite_short_" : "_favorite_short_", comment: "") ) { _, _ in - NCNetworking.shared.favoriteMetadata(metadata) { error in - if error != .success { - NCContentPresenter().showError(error: error) - } - } - } - favoriteAction.backgroundColor = NCBrandColor.shared.yellowFavorite - favoriteAction.image = .init(systemName: metadata.favorite ? "star.slash.fill" : "star.fill") - favoriteAction.transitionDelegate = scaleTransition - favoriteAction.hidesWhenSelected = true - - var actions = [favoriteAction] - - let shareAction = SwipeAction(style: .default, title: NSLocalizedString("_share_", comment: "")) { _, _ in - NCActionCenter.shared.openActivityViewController(selectedMetadata: [metadata]) - } - shareAction.backgroundColor = .blue - shareAction.image = .init(systemName: "square.and.arrow.up") - shareAction.transitionDelegate = scaleTransition - shareAction.hidesWhenSelected = true - - let deleteAction = SwipeAction(style: .destructive, title: NSLocalizedString("_delete_", comment: "")) { _, _ in - let titleDelete: String - - if metadata.directory { - titleDelete = NSLocalizedString("_delete_folder_", comment: "") - } else { - titleDelete = NSLocalizedString("_delete_file_", comment: "") - } - - let message = NSLocalizedString("_want_delete_", comment: "") + "\n - " + metadata.fileNameView - - let alertController = UIAlertController.deleteFileOrFolder(titleString: titleDelete + "?", message: message, canDeleteServer: !metadata.lock, selectedMetadatas: [metadata], indexPaths: self.selectIndexPaths) { _ in } - - self.viewController.present(alertController, animated: true, completion: nil) - } - deleteAction.image = .init(systemName: "trash") - deleteAction.style = .destructive - deleteAction.transitionDelegate = scaleTransition - deleteAction.hidesWhenSelected = true - - if !NCManageDatabase.shared.isMetadataShareOrMounted(metadata: metadata, metadataFolder: metadataFolder) { - actions.insert(deleteAction, at: 0) - } - - if metadata.canShare { - actions.append(shareAction) - } - - return actions - } - - func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { - var options = SwipeOptions() - options.expansionStyle = .selection - options.transitionStyle = .border - options.backgroundColor = .clear - return options - } -} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 11bd2566ee..02c0fb3ca9 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -26,26 +26,29 @@ import SwiftUI import RealmSwift import NextcloudKit import EasyTipView -import JGProgressHUD class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! + let database = NCManageDatabase.shared + let global = NCGlobal.shared + let utility = NCUtility() + let utilityFileSystem = NCUtilityFileSystem() + let imageCache = NCImageCache.shared + var dataSource = NCCollectionViewDataSource() + let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + var autoUploadFileName = "" var autoUploadDirectory = "" - var isTransitioning: Bool = false - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! - let utilityFileSystem = NCUtilityFileSystem() - let utility = NCUtility() let refreshControl = UIRefreshControl() var searchController: UISearchController? var backgroundImageView = UIImageView() var serverUrl: String = "" var isEditMode = false - var selectOcId: [String] = [] + var fileSelect: [String] = [] var metadataFolder: tableMetadata? - var dataSource = NCDataSource() var richWorkspaceText: String? var sectionFirstHeader: NCSectionFirstHeader? var sectionFirstHeaderEmptyData: NCSectionFirstHeaderEmptyData? @@ -60,12 +63,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var layoutType = NCGlobal.shared.layoutList var literalSearch: String? var tabBarSelect: NCCollectionViewCommonSelectTabBar! - var timerNotificationCenter: Timer? - var notificationReloadDataSource: Int = 0 - var notificationReloadDataSourceNetwork: Int = 0 var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] - let maxImageGrid: CGFloat = 7 // DECLARE var layoutKey = "" @@ -74,18 +73,78 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var enableSearchBar: Bool = false var headerMenuTransferView = false var headerRichWorkspaceDisable: Bool = false - var emptyImage: UIImage? + + var emptyImageName: String? + var emptyImageColors: [UIColor]? var emptyTitle: String = "" + var emptyDescription: String = "" var emptyDataPortaitOffset: CGFloat = 0 var emptyDataLandscapeOffset: CGFloat = -20 + var lastScale: CGFloat = 1.0 + var currentScale: CGFloat = 1.0 + var maxColumns: Int { + let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) + let column = Int(screenWidth / 44) + + return column + } + var transitionColumns = false + var numberOfColumns: Int = 0 + var lastNumberOfColumns: Int = 0 + + var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } + + var isLayoutPhoto: Bool { + layoutForView?.layout == global.layoutPhotoRatio || layoutForView?.layout == global.layoutPhotoSquare + } + + var isLayoutGrid: Bool { + layoutForView?.layout == global.layoutGrid + } + + var isLayoutList: Bool { + layoutForView?.layout == global.layoutList + } + var showDescription: Bool { !headerRichWorkspaceDisable && NCKeychain().showDescription } var infoLabelsSeparator: String { - layoutForView?.layout == NCGlobal.shared.layoutList ? " - " : "" + layoutForView?.layout == global.layoutList ? " - " : "" + } + + var controller: NCMainTabBarController? { + self.tabBarController as? NCMainTabBarController + } + + var defaultPredicate: NSPredicate { + let predicate = NSPredicate(format: "account == %@ AND serverUrl == %@ AND NOT (status IN %@) AND NOT (livePhotoFile != '' AND classFile == %@)", session.account, self.serverUrl, NCGlobal.shared.metadataStatusHideInView, NKCommon.TypeClassFile.video.rawValue) + return predicate + } + + var isNumberOfItemsInAllSectionsNull: Bool { + var totalItems = 0 + for section in 0.. 0 { - print("notificationReloadDataSource: \(notificationReloadDataSource)") - reloadDataSource() - notificationReloadDataSource = 0 - } - } - @objc func applicationWillResignActive(_ notification: NSNotification) { self.refreshControl.endRefreshing() } @@ -314,107 +364,139 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } guard let userInfo = notification.userInfo as NSDictionary?, let error = userInfo["error"] as? NKError, - error.errorCode != NCGlobal.shared.errorNotModified else { return } + error.errorCode != global.errorNotModified else { return } setNavigationLeftItems() } - @objc func changeTheming() { + @objc func changeTheming(_ notification: NSNotification) { + self.reloadDataSource() + } + + @objc func changeLayout(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let account = userInfo["account"] as? String, + let serverUrl = userInfo["serverUrl"] as? String, + let layoutForView = userInfo["layoutForView"] as? NCDBLayoutForView, + account == session.account, + serverUrl == self.serverUrl + else { return } + + if self.layoutForView?.layout == layoutForView.layout { + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + self.reloadDataSource() + return + } + + self.layoutForView = self.database.setLayoutForView(layoutForView: layoutForView) + layoutForView.layout = layoutForView.layout + self.layoutType = layoutForView.layout + collectionView.reloadData() + + switch layoutForView.layout { + case global.layoutList: + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) + case global.layoutGrid: + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) + case global.layoutPhotoSquare, global.layoutPhotoRatio: + self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) + default: + break + } + + self.collectionView.collectionViewLayout.invalidateLayout() + + self.setNavigationRightItems() } @objc func reloadDataSource(_ notification: NSNotification) { - notificationReloadDataSource += 1 - } + if let userInfo = notification.userInfo as? NSDictionary { + if let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } + } + + if let clearDataSource = userInfo["clearDataSource"] as? Bool, clearDataSource { + self.dataSource.removeAll() + } + } - @objc func reloadDataSourceNetwork(_ notification: NSNotification) { - var withQueryDB = false + reloadDataSource() + } + @objc func getServerData(_ notification: NSNotification) { if let userInfo = notification.userInfo as NSDictionary?, - let reload = userInfo["withQueryDB"] as? Bool { - withQueryDB = reload + let serverUrl = userInfo["serverUrl"] as? String { + if serverUrl != self.serverUrl { + return + } } - if !isSearchingMode { - reloadDataSourceNetwork(withQueryDB: withQueryDB) - } + getServerData() } @objc func changeStatusFolderE2EE(_ notification: NSNotification) { - notificationReloadDataSource += 1 + reloadDataSource() } @objc func closeRichWorkspaceWebView() { - reloadDataSourceNetwork() + reloadDataSource() } @objc func deleteFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let error = userInfo["error"] as? NKError else { return } - if error == .success { - reloadDataSource() - } else { + if error != .success { NCContentPresenter().showError(error: error) } - } - - @objc func moveFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } - if error == .success { - if !isSearchingMode, let dragDrop = userInfo["dragdrop"] as? Bool, dragDrop { - setEditMode(false) - reloadDataSourceNetwork(withQueryDB: true) - } else { - reloadDataSource() - } - } else { - NCContentPresenter().showError(error: error) - } + reloadDataSource() } - @objc func copyFile(_ notification: NSNotification) { + @objc func copyMoveFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, - let error = userInfo["error"] as? NKError else { return } + let serverUrl = userInfo["serverUrl"] as? String, + let account = userInfo["account"] as? String, + account == session.account, + serverUrl == self.serverUrl else { return } - if error == .success { - if !isSearchingMode, let dragDrop = userInfo["dragdrop"] as? Bool, dragDrop { - setEditMode(false) - reloadDataSourceNetwork(withQueryDB: true) - } else { - reloadDataSource() - } - } else { - NCContentPresenter().showError(error: error) - } + reloadDataSource() } @objc func renameFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let account = userInfo["account"] as? String, - account == appDelegate.account + let serverUrl = userInfo["serverUrl"] as? String, + let error = userInfo["error"] as? NKError, + account == session.account, + serverUrl == self.serverUrl else { return } + if error != .success { + NCContentPresenter().showError(error: error) + } + reloadDataSource() } @objc func createFolder(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl, let account = userInfo["account"] as? String, - account == appDelegate.account, - let withPush = userInfo["withPush"] as? Bool + account == session.account, + let withPush = userInfo["withPush"] as? Bool, + let metadata = database.getMetadataFromOcId(ocId) else { return } - notificationReloadDataSource += 1 - - if withPush, let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + if metadata.serverUrl + "/" + metadata.fileName == self.serverUrl { + reloadDataSource() + } else if withPush, metadata.serverUrl == self.serverUrl { + reloadDataSource() if let sceneIdentifier = userInfo["sceneIdentifier"] as? String { - if sceneIdentifier == (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier { + if sceneIdentifier == controller?.sceneIdentifier { pushMetadata(metadata) } } else { @@ -425,7 +507,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS @objc func favoriteFile(_ notification: NSNotification) { if self is NCFavorite { - return notificationReloadDataSource += 1 + return reloadDataSource() } guard let userInfo = notification.userInfo as NSDictionary?, @@ -439,103 +521,82 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS @objc func downloadStartFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl || self.serverUrl.isEmpty, - let account = userInfo["account"] as? String, - account == appDelegate.account + let account = userInfo["account"] as? String else { return } - self.notificationReloadDataSource += 1 + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } } @objc func downloadedFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl || self.serverUrl.isEmpty, - let account = userInfo["account"] as? String, - account == appDelegate.account, - let error = userInfo["error"] as? NKError + let account = userInfo["account"] as? String else { return } - if error != .success { - NCContentPresenter().showError(error: error) + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() } - - notificationReloadDataSource += 1 } @objc func downloadCancelFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl || self.serverUrl.isEmpty, - let account = userInfo["account"] as? String, - account == appDelegate.account + let account = userInfo["account"] as? String else { return } - reloadDataSource() + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } } @objc func uploadStartFile(_ notification: NSNotification) { - guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, - let serverUrl = userInfo["serverUrl"] as? String, - let account = userInfo["account"] as? String, - !isSearchingMode, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) - else { return } - - // Header view trasfer - if metadata.isTransferInForeground { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: 0) - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if serverUrl == self.serverUrl, account == appDelegate.account { - notificationReloadDataSource += 1 - } + collectionView?.reloadData() } @objc func uploadedFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, - let ocIdTemp = userInfo["ocIdTemp"] as? String, let serverUrl = userInfo["serverUrl"] as? String, let account = userInfo["account"] as? String else { return } - if ocIdTemp == NCNetworking.shared.transferInForegorund?.ocId { - NCNetworking.shared.transferInForegorund = nil - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == appDelegate.account, serverUrl == self.serverUrl { - notificationReloadDataSource += 1 + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() } } @objc func uploadedLivePhoto(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, let serverUrl = userInfo["serverUrl"] as? String, - serverUrl == self.serverUrl, - let account = userInfo["account"] as? String, - account == appDelegate.account + let account = userInfo["account"] as? String else { return } - notificationReloadDataSource += 1 + if account == self.session.account, serverUrl == self.serverUrl { + reloadDataSource() + } else { + collectionView?.reloadData() + } } @objc func uploadCancelFile(_ notification: NSNotification) { guard let userInfo = notification.userInfo as NSDictionary?, - let ocId = userInfo["ocId"] as? String, let serverUrl = userInfo["serverUrl"] as? String, let account = userInfo["account"] as? String else { return } - if ocId == NCNetworking.shared.transferInForegorund?.ocId { - NCNetworking.shared.transferInForegorund = nil - DispatchQueue.main.async { self.collectionView?.reloadData() } - } - - if account == appDelegate.account, serverUrl == self.serverUrl { + if account == self.session.account, serverUrl == self.serverUrl { reloadDataSource() + } else { + collectionView?.reloadData() } } @@ -544,65 +605,32 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let progressNumber = userInfo["progress"] as? NSNumber, let totalBytes = userInfo["totalBytes"] as? Int64, let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, - let ocId = userInfo["ocId"] as? String + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String else { return } + let chunk: Int = userInfo["chunk"] as? Int ?? 0 let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false - DispatchQueue.main.async { - if self.headerMenuTransferView && (chunk > 0 || e2eEncrypted) { - if NCNetworking.shared.transferInForegorund?.ocId == ocId { - NCNetworking.shared.transferInForegorund?.progress = progressNumber.floatValue - } else { - NCNetworking.shared.transferInForegorund = NCNetworking.TransferInForegorund(ocId: ocId, progress: progressNumber.floatValue) - self.collectionView.reloadData() - } - self.sectionFirstHeader?.progressTransfer.progress = progressNumber.floatValue - self.sectionFirstHeaderEmptyData?.progressTransfer.progress = progressNumber.floatValue - } else { - guard let indexPath = self.dataSource.getIndexPathMetadata(ocId: ocId).indexPath, - let cell = self.collectionView?.cellForItem(at: indexPath), - let cell = cell as? NCCellProtocol else { return } - if progressNumber.floatValue == 1 && !(cell is NCTransferCell) { - cell.fileProgressView?.isHidden = true - cell.fileProgressView?.progress = .zero - cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - cell.writeInfoDateSize(date: metadata.date, size: metadata.size) - } else { - cell.fileInfoLabel?.text = "" - cell.fileSubinfoLabel?.text = "" - } - } else { - cell.fileProgressView?.isHidden = false - cell.fileProgressView?.progress = progressNumber.floatValue - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - let status = userInfo["status"] as? Int ?? NCGlobal.shared.metadataStatusNormal - if status == NCGlobal.shared.metadataStatusDownloading { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes) - } else if status == NCGlobal.shared.metadataStatusUploading { - if totalBytes > 0 { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes) - } else { - cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) - cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …" - } - } - } - } + let transfer = NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + // HEADER + if self.headerMenuTransferView, transfer.session.contains("upload") { + self.sectionFirstHeader?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) + self.sectionFirstHeaderEmptyData?.setViewTransfer(isHidden: false, progress: transfer.progressNumber.floatValue) } } // MARK: - Layout func setNavigationLeftItems() { - guard layoutKey == NCGlobal.shared.layoutViewFiles else { return } - let activeAccount = NCManageDatabase.shared.getActiveAccount() - let image = utility.loadUserImage(for: appDelegate.user, displayName: activeAccount?.displayName, userBaseUrl: appDelegate) + guard layoutKey == global.layoutViewFiles, + let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) else { + return } + let image = utility.loadUserImage(for: tableAccount.user, displayName: tableAccount.displayName, urlBase: tableAccount.urlBase) let accountButton = AccountSwitcherButton(type: .custom) - let accounts = NCManageDatabase.shared.getAllAccountOrderAlias() + let accounts = database.getAllAccountOrderAlias() var childrenAccountSubmenu: [UIMenuElement] = [] accountButton.setImage(image, for: .normal) @@ -612,7 +640,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS if !accounts.isEmpty { let accountActions: [UIAction] = accounts.map { account in - let image = utility.loadUserImage(for: account.user, displayName: account.displayName, userBaseUrl: account) + let image = utility.loadUserImage(for: account.user, displayName: account.displayName, urlBase: account.urlBase) var name: String = "" var url: String = "" @@ -625,7 +653,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let action = UIAction(title: name, image: image, state: account.active ? .on : .off) { _ in if !account.active { - self.appDelegate.changeAccount(account.account, userProfile: nil) { } + NCAccount().changeAccount(account.account, userProfile: nil, controller: self.controller) { } self.setEditMode(false) } } @@ -635,11 +663,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } let addAccountAction = UIAction(title: NSLocalizedString("_add_account_", comment: ""), image: utility.loadImage(named: "person.crop.circle.badge.plus", colors: NCBrandColor.shared.iconImageMultiColors)) { _ in - self.appDelegate.openLogin(selector: NCGlobal.shared.introLogin, openLoginWeb: false) + self.appDelegate.openLogin(selector: self.global.introLogin) } let settingsAccountAction = UIAction(title: NSLocalizedString("_account_settings_", comment: ""), image: utility.loadImage(named: "gear", colors: [NCBrandColor.shared.iconImageColor])) { _ in - let accountSettingsModel = NCAccountSettingsModel(controller: self.tabBarController as? NCMainTabBarController, delegate: self) + let accountSettingsModel = NCAccountSettingsModel(controller: self.controller, delegate: self) let accountSettingsView = NCAccountSettingsView(model: accountSettingsModel) let accountSettingsController = UIHostingController(rootView: accountSettingsView) self.present(accountSettingsController, animated: true, completion: nil) @@ -672,111 +700,64 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } func setNavigationRightItems() { - guard layoutKey != NCGlobal.shared.layoutViewTransfers else { return } + guard layoutKey != global.layoutViewTransfers else { return } let isTabBarHidden = self.tabBarController?.tabBar.isHidden ?? true let isTabBarSelectHidden = tabBarSelect.isHidden() func createMenuActions() -> [UIMenuElement] { - guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: appDelegate.account, key: layoutKey, serverUrl: serverUrl) else { return [] } - let columnPhoto = self.layoutForView?.columnPhoto ?? 3 - - func saveLayout(_ layoutForView: NCDBLayoutForView) { - NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) - setNavigationRightItems() - } + guard let layoutForView = database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl) else { return [] } - if layoutForView.layout != NCGlobal.shared.layoutPhotoSquare && layoutForView.layout != NCGlobal.shared.layoutPhotoRatio { - self.attributesZoomIn = .disabled - self.attributesZoomOut = .disabled - } else if CGFloat(columnPhoto) >= maxImageGrid - 1 { - self.attributesZoomIn = [] - self.attributesZoomOut = .disabled - } else if columnPhoto <= 1 { - self.attributesZoomIn = .disabled - self.attributesZoomOut = [] - } else { - self.attributesZoomIn = [] - self.attributesZoomOut = [] - } - - let select = UIAction(title: NSLocalizedString("_select_", comment: ""), image: utility.loadImage(named: "checkmark.circle"), attributes: self.dataSource.getMetadataSourceForAllSections().isEmpty ? .disabled : []) { _ in + let select = UIAction(title: NSLocalizedString("_select_", comment: ""), + image: utility.loadImage(named: "checkmark.circle"), + attributes: (self.dataSource.isEmpty() || NCNetworking.shared.isOffline) ? .disabled : []) { _ in self.setEditMode(true) } - let list = UIAction(title: NSLocalizedString("_list_", comment: ""), image: utility.loadImage(named: "list.bullet"), state: layoutForView.layout == NCGlobal.shared.layoutList ? .on : .off) { _ in - layoutForView.layout = NCGlobal.shared.layoutList - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.layoutType = NCGlobal.shared.layoutList + let list = UIAction(title: NSLocalizedString("_list_", comment: ""), image: utility.loadImage(named: "list.bullet"), state: layoutForView.layout == global.layoutList ? .on : .off) { _ in - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) {_ in self.isTransitioning = false } + layoutForView.layout = self.global.layoutList - self.setNavigationRightItems() + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) } - let grid = UIAction(title: NSLocalizedString("_icons_", comment: ""), image: utility.loadImage(named: "square.grid.2x2"), state: layoutForView.layout == NCGlobal.shared.layoutGrid ? .on : .off) { _ in - layoutForView.layout = NCGlobal.shared.layoutGrid - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.layoutType = NCGlobal.shared.layoutGrid + let grid = UIAction(title: NSLocalizedString("_icons_", comment: ""), image: utility.loadImage(named: "square.grid.2x2"), state: layoutForView.layout == global.layoutGrid ? .on : .off) { _ in - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) {_ in self.isTransitioning = false } + layoutForView.layout = self.global.layoutGrid - self.setNavigationRightItems() + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) } - let menuPhoto = UIMenu(title: "", options: .displayInline, children: [ - UIAction(title: NSLocalizedString("_media_square_", comment: ""), image: utility.loadImage(named: "square.grid.3x3"), state: layoutForView.layout == NCGlobal.shared.layoutPhotoSquare ? .on : .off) { _ in - layoutForView.layout = NCGlobal.shared.layoutPhotoSquare - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.layoutType = NCGlobal.shared.layoutPhotoSquare + let mediaSquare = UIAction(title: NSLocalizedString("_media_square_", comment: ""), image: utility.loadImage(named: "square.grid.3x3"), state: layoutForView.layout == global.layoutPhotoSquare ? .on : .off) { _ in - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) {_ in self.isTransitioning = false } + layoutForView.layout = self.global.layoutPhotoSquare - self.reloadDataSource() - self.setNavigationRightItems() - }, - UIAction(title: NSLocalizedString("_media_ratio_", comment: ""), image: utility.loadImage(named: "rectangle.grid.3x2"), state: layoutForView.layout == NCGlobal.shared.layoutPhotoRatio ? .on : .off) { _ in - layoutForView.layout = NCGlobal.shared.layoutPhotoRatio - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - self.layoutType = NCGlobal.shared.layoutPhotoRatio + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) + } - self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() - self.collectionView.setCollectionViewLayout(self.mediaLayout, animated: true) {_ in self.isTransitioning = false } + let mediaRatio = UIAction(title: NSLocalizedString("_media_ratio_", comment: ""), image: utility.loadImage(named: "rectangle.grid.3x2"), state: layoutForView.layout == self.global.layoutPhotoRatio ? .on : .off) { _ in - self.reloadDataSource() - self.setNavigationRightItems() - } - ]) - - let menuZoom = UIMenu(title: "", options: .displayInline, children: [ - UIAction(title: NSLocalizedString("_zoom_out_", comment: ""), image: utility.loadImage(named: "minus.magnifyingglass"), attributes: self.attributesZoomOut) { _ in - UIView.animate(withDuration: 0.0, animations: { - layoutForView.columnPhoto = columnPhoto + 1 - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - - self.collectionView.reloadData() - self.setNavigationRightItems() - }) - }, - UIAction(title: NSLocalizedString("_zoom_in_", comment: ""), image: utility.loadImage(named: "plus.magnifyingglass"), attributes: self.attributesZoomIn) { _ in - UIView.animate(withDuration: 0.0, animations: { - layoutForView.columnPhoto = columnPhoto - 1 - self.layoutForView = NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) - - self.collectionView.reloadData() - self.setNavigationRightItems() - }) - } - ]) + layoutForView.layout = self.global.layoutPhotoRatio + + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) + } - let viewStyleSubmenu = UIMenu(title: "", options: .displayInline, children: [list, grid, UIMenu(title: NSLocalizedString("_additional_view_options_", comment: ""), children: [menuPhoto, menuZoom])]) + let viewStyleSubmenu = UIMenu(title: "", options: .displayInline, children: [list, grid, mediaSquare, mediaRatio]) let ascending = layoutForView.ascending let ascendingChevronImage = utility.loadImage(named: ascending ? "chevron.up" : "chevron.down") @@ -785,55 +766,84 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS let isSize = layoutForView.sort == "size" let byName = UIAction(title: NSLocalizedString("_name_", comment: ""), image: isName ? ascendingChevronImage : nil, state: isName ? .on : .off) { _ in + if isName { // repeated press layoutForView.ascending = !layoutForView.ascending } layoutForView.sort = "fileName" - saveLayout(layoutForView) + + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) } let byNewest = UIAction(title: NSLocalizedString("_date_", comment: ""), image: isDate ? ascendingChevronImage : nil, state: isDate ? .on : .off) { _ in + if isDate { // repeated press layoutForView.ascending = !layoutForView.ascending } layoutForView.sort = "date" - saveLayout(layoutForView) + + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) } let byLargest = UIAction(title: NSLocalizedString("_size_", comment: ""), image: isSize ? ascendingChevronImage : nil, state: isSize ? .on : .off) { _ in + if isSize { // repeated press layoutForView.ascending = !layoutForView.ascending } layoutForView.sort = "size" - saveLayout(layoutForView) + + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) } let sortSubmenu = UIMenu(title: NSLocalizedString("_order_by_", comment: ""), options: .displayInline, children: [byName, byNewest, byLargest]) let foldersOnTop = UIAction(title: NSLocalizedString("_directory_on_top_no_", comment: ""), image: utility.loadImage(named: "folder"), state: layoutForView.directoryOnTop ? .on : .off) { _ in + layoutForView.directoryOnTop = !layoutForView.directoryOnTop - saveLayout(layoutForView) + + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterChangeLayout, + object: nil, + userInfo: ["account": self.session.account, + "serverUrl": self.serverUrl, + "layoutForView": layoutForView]) } - let personalFilesOnly = NCKeychain().getPersonalFilesOnly(account: appDelegate.account) + let personalFilesOnly = NCKeychain().getPersonalFilesOnly(account: session.account) let personalFilesOnlyAction = UIAction(title: NSLocalizedString("_personal_files_only_", comment: ""), image: utility.loadImage(named: "folder.badge.person.crop", colors: NCBrandColor.shared.iconImageMultiColors), state: personalFilesOnly ? .on : .off) { _ in - NCKeychain().setPersonalFilesOnly(account: self.appDelegate.account, value: !personalFilesOnly) + + NCKeychain().setPersonalFilesOnly(account: self.session.account, value: !personalFilesOnly) + self.reloadDataSource() + self.setNavigationRightItems() } let showDescriptionKeychain = NCKeychain().showDescription let showDescription = UIAction(title: NSLocalizedString("_show_description_", comment: ""), image: utility.loadImage(named: "list.dash.header.rectangle"), attributes: richWorkspaceText == nil ? .disabled : [], state: showDescriptionKeychain && richWorkspaceText != nil ? .on : .off) { _ in + NCKeychain().showDescription = !showDescriptionKeychain - self.collectionView.reloadData() + + self.reloadDataSource() self.setNavigationRightItems() } showDescription.subtitle = richWorkspaceText == nil ? NSLocalizedString("_no_description_available_", comment: "") : "" - if layoutKey == NCGlobal.shared.layoutViewRecent { + if layoutKey == global.layoutViewRecent { return [select] } else { var additionalSubmenu = UIMenu() - if layoutKey == NCGlobal.shared.layoutViewFiles { + if layoutKey == global.layoutViewFiles { additionalSubmenu = UIMenu(title: "", options: .displayInline, children: [foldersOnTop, personalFilesOnlyAction, showDescription]) } else { additionalSubmenu = UIMenu(title: "", options: .displayInline, children: [foldersOnTop, showDescription]) @@ -843,7 +853,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } if isEditMode { - tabBarSelect.update(selectOcId: selectOcId, metadatas: getSelectedMetadatas(), userId: appDelegate.userId) + tabBarSelect.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: session.userId) tabBarSelect.show() let select = UIBarButtonItem(title: NSLocalizedString("_cancel_", comment: ""), style: .done) { self.setEditMode(false) @@ -853,9 +863,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS tabBarSelect.hide() let menuButton = UIBarButtonItem(image: utility.loadImage(named: "ellipsis.circle"), menu: UIMenu(children: createMenuActions())) menuButton.tintColor = NCBrandColor.shared.iconImageColor - if layoutKey == NCGlobal.shared.layoutViewFiles { + if layoutKey == global.layoutViewFiles { let notification = UIBarButtonItem(image: utility.loadImage(named: "bell"), style: .plain) { if let viewController = UIStoryboard(name: "NCNotification", bundle: nil).instantiateInitialViewController() as? NCNotification { + viewController.session = self.session self.navigationController?.pushViewController(viewController, animated: true) } } @@ -874,14 +885,15 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } func getNavigationTitle() -> String { - let activeAccount = NCManageDatabase.shared.getActiveAccount() - guard let userAlias = activeAccount?.alias, !userAlias.isEmpty else { - return NCBrandOptions.shared.brand + let tableAccount = self.database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) + if let tableAccount, + !tableAccount.alias.isEmpty { + return tableAccount.alias } - return userAlias + return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tableAccount: tableAccount?) { } + func accountSettingsDidDismiss(tableAccount: tableAccount?, controller: NCMainTabBarController?) { } // MARK: - SEARCH @@ -903,152 +915,46 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { isSearchingMode = true self.providers?.removeAll() - self.dataSource.clearDataSource() - self.collectionView.reloadData() + self.dataSource.removeAll() + self.reloadDataSource() // TIP dismissTip() } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { - reloadDataSourceNetwork() + networkSearch() } } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - DispatchQueue.global().async { - NCNetworking.shared.cancelUnifiedSearchFiles() - self.isSearchingMode = false - self.literalSearch = "" - self.providers?.removeAll() - self.dataSource.clearDataSource() - self.reloadDataSource() - } + NCNetworking.shared.cancelUnifiedSearchFiles() + self.isSearchingMode = false + self.literalSearch = "" + self.providers?.removeAll() + self.dataSource.removeAll() + self.reloadDataSource() } // MARK: - TAP EVENT - // sessionIdentifierDownload: String = "com.nextcloud.nextcloudkit.session.download" - // sessionIdentifierUpload: String = "com.nextcloud.nextcloudkit.session.upload" - - // sessionUploadBackground: String = "com.nextcloud.session.upload.background" - // sessionUploadBackgroundWWan: String = "com.nextcloud.session.upload.backgroundWWan" - // sessionUploadBackgroundExtension: String = "com.nextcloud.session.upload.extension" - - func cancelSession(metadata: tableMetadata) async { - let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) - - utilityFileSystem.removeFile(atPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId)) - - // No session found - if metadata.session.isEmpty { - NCNetworking.shared.uploadRequest.removeValue(forKey: fileNameLocalPath) - NCNetworking.shared.downloadRequest.removeValue(forKey: fileNameLocalPath) - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) - return - } - - // DOWNLOAD FOREGROUND - if metadata.session == NextcloudKit.shared.nkCommonInstance.sessionIdentifierDownload { - if let request = NCNetworking.shared.downloadRequest[fileNameLocalPath] { - request.cancel() - } else if let metadata = NCManageDatabase.shared.getMetadataFromOcId(metadata.ocId) { - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - session: "", - sessionError: "", - selector: "", - status: NCGlobal.shared.metadataStatusNormal) - NotificationCenter.default.post(name: Notification.Name(rawValue: NCGlobal.shared.notificationCenterDownloadCancelFile), - object: nil, - userInfo: ["ocId": metadata.ocId, - "serverUrl": metadata.serverUrl, - "account": metadata.account]) - } - return - } - - // DOWNLOAD BACKGROUND - if metadata.session == NCNetworking.shared.sessionDownloadBackground { - let session: URLSession? = NCNetworking.shared.sessionManagerDownloadBackground - if let tasks = await session?.tasks { - for task in tasks.2 { // ([URLSessionDataTask], [URLSessionUploadTask], [URLSessionDownloadTask]) - if task.taskIdentifier == metadata.sessionTaskIdentifier { - task.cancel() - } - } - } - NCManageDatabase.shared.setMetadataSession(ocId: metadata.ocId, - session: "", - sessionError: "", - selector: "", - status: NCGlobal.shared.metadataStatusNormal) - NotificationCenter.default.post(name: Notification.Name(rawValue: NCGlobal.shared.notificationCenterDownloadCancelFile), - object: nil, - userInfo: ["ocId": metadata.ocId, - "serverUrl": metadata.serverUrl, - "account": metadata.account]) - } - - // UPLOAD FOREGROUND - if metadata.session == NextcloudKit.shared.nkCommonInstance.sessionIdentifierUpload { - if let request = NCNetworking.shared.uploadRequest[fileNameLocalPath] { - request.cancel() - NCNetworking.shared.uploadRequest.removeValue(forKey: fileNameLocalPath) - } - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) - NotificationCenter.default.post(name: Notification.Name(rawValue: NCGlobal.shared.notificationCenterUploadCancelFile), - object: nil, - userInfo: ["ocId": metadata.ocId, - "serverUrl": metadata.serverUrl, - "account": metadata.account]) - return - } - - // UPLOAD BACKGROUND - var session: URLSession? - if metadata.session == NCNetworking.shared.sessionUploadBackground { - session = NCNetworking.shared.sessionManagerUploadBackground - } else if metadata.session == NCNetworking.shared.sessionUploadBackgroundWWan { - session = NCNetworking.shared.sessionManagerUploadBackgroundWWan - } else if metadata.session == NCNetworking.shared.sessionUploadBackgroundExtension { - session = NCNetworking.shared.sessionManagerUploadBackgroundExtension - } - if let tasks = await session?.tasks { - for task in tasks.1 { // ([URLSessionDataTask], [URLSessionUploadTask], [URLSessionDownloadTask]) - if task.taskIdentifier == metadata.sessionTaskIdentifier { - task.cancel() - } - } - NCManageDatabase.shared.deleteMetadata(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) - NotificationCenter.default.post(name: Notification.Name(rawValue: NCGlobal.shared.notificationCenterUploadCancelFile), - object: nil, - userInfo: ["ocId": metadata.ocId, - "serverUrl": metadata.serverUrl, - "account": metadata.account]) - } + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } - func tapMoreListItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) { - tapMoreGridItem(with: objectId, namedButtonMore: namedButtonMore, image: image, indexPath: indexPath, sender: sender) + func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) } - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(objectId) else { return } + func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } NCActionCenter.shared.openShare(viewController: self, metadata: metadata, page: .sharing) } - func tapMoreGridItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(objectId) else { return } - - if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock { - toggleMenu(metadata: metadata, indexPath: indexPath, imageIcon: image) - } else if namedButtonMore == NCGlobal.shared.buttonMoreStop { - Task { - await cancelSession(metadata: metadata) - } - } + func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } + toggleMenu(metadata: metadata, image: image) } func tapRichWorkspace(_ sender: Any) { @@ -1068,26 +974,15 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS unifiedSearchMore(metadataForSection: metadataForSection) } - func tapButtonTransfer(_ sender: Any) { - if let ocId = NCNetworking.shared.transferInForegorund?.ocId, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - Task { - await cancelSession(metadata: metadata) - } - } - } + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } + func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressGridItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreListItem(with objectId: String, namedButtonMore: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } + func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreGridItem(with objectId: String, namedButtonMore: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - } + func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) @@ -1137,72 +1032,69 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } @objc func pasteFilesMenu() { - NCActionCenter.shared.pastePasteboard(serverUrl: serverUrl, account: appDelegate.account, hudView: tabBarController?.view) + NCActionCenter.shared.pastePasteboard(serverUrl: serverUrl, account: session.account, controller: self.controller) } - // MARK: - DataSource + NC Endpoint - - func queryDB() { } + // MARK: - DataSource - @objc func reloadDataSource(withQueryDB: Bool = true) { - guard !appDelegate.account.isEmpty, !self.isSearchingMode else { return } + @objc func reloadDataSource() { + guard !session.account.isEmpty, !self.isSearchingMode else { return } - // get auto upload folder - autoUploadFileName = NCManageDatabase.shared.getAccountAutoUploadFileName() - autoUploadDirectory = NCManageDatabase.shared.getAccountAutoUploadDirectory(urlBase: appDelegate.urlBase, userId: appDelegate.userId, account: appDelegate.account) - // get layout for view - layoutForView = NCManageDatabase.shared.getLayoutForView(account: appDelegate.account, key: layoutKey, serverUrl: serverUrl) - - DispatchQueue.global(qos: .userInteractive).async { - if withQueryDB { self.queryDB() } - DispatchQueue.main.async { - self.refreshControl.endRefreshing() - self.collectionView.reloadData() - self.setNavigationRightItems() - } + DispatchQueue.main.async { + UIView.transition(with: self.collectionView, + duration: 0.20, + options: .transitionCrossDissolve, + animations: { self.collectionView.reloadData() }, + completion: nil) + + self.setNavigationRightItems() + self.refreshControl.endRefreshing() } } - @objc func reloadDataSourceNetwork(withQueryDB: Bool = false) { - DispatchQueue.main.async { - self.collectionView?.reloadData() - } + func getServerData() { } @objc func networkSearch() { - guard !appDelegate.account.isEmpty, let literalSearch = literalSearch, !literalSearch.isEmpty - else { return self.refreshControl.endRefreshing() } + guard !session.account.isEmpty, + let literalSearch = literalSearch, + !literalSearch.isEmpty else { + return self.refreshControl.endRefreshing() + } - self.dataSource.clearDataSource() + self.dataSource.removeAll() self.refreshControl.beginRefreshing() - self.collectionView.reloadData() + self.reloadDataSource() - if NCGlobal.shared.capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion20 { - NCNetworking.shared.unifiedSearchFiles(userBaseUrl: appDelegate, literal: literalSearch) { task in + if NCCapabilities.shared.getCapabilities(account: session.account).capabilityServerVersionMajor >= global.nextcloudVersion20 { + NCNetworking.shared.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + self.reloadDataSource() } providers: { _, searchProviders in self.providers = searchProviders self.searchResults = [] - self.dataSource = NCDataSource(metadatas: [], account: self.appDelegate.account, layoutForView: self.layoutForView, providers: self.providers, searchResults: self.searchResults) + self.dataSource = NCCollectionViewDataSource(results: nil, layoutForView: self.layoutForView, providers: self.providers, searchResults: self.searchResults) } update: { _, _, searchResult, metadatas in guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } NCNetworking.shared.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) } completion: { _, _ in self.refreshControl.endRefreshing() - self.collectionView.reloadData() + self.reloadDataSource() } } else { - NCNetworking.shared.searchFiles(urlBase: appDelegate, literal: literalSearch, account: appDelegate.account) { task in + NCNetworking.shared.searchFiles(literal: literalSearch, account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + self.reloadDataSource() } completion: { metadatas, error in DispatchQueue.main.async { self.refreshControl.endRefreshing() - self.collectionView.reloadData() + self.reloadDataSource() } - guard let metadatas = metadatas, error == .success, self.isSearchingMode else { return } - self.dataSource = NCDataSource(metadatas: metadatas, account: self.appDelegate.account, layoutForView: self.layoutForView, providers: self.providers, searchResults: self.searchResults) + guard let metadatas, error == .success, self.isSearchingMode else { return } + let ocId = metadatas.map { $0.ocId } + let results = self.database.getResultsMetadatasPredicate(NSPredicate(format: "ocId IN %@", ocId), layoutForView: self.layoutForView) + + self.dataSource = NCCollectionViewDataSource(results: results, layoutForView: self.layoutForView, providers: self.providers, searchResults: self.searchResults) } } } @@ -1213,9 +1105,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS metadataForSection.unifiedSearchInProgress = true self.collectionView?.reloadData() - NCNetworking.shared.unifiedSearchFilesProvider(userBaseUrl: appDelegate, id: lastSearchResult.id, term: term, limit: 5, cursor: cursor) { task in + NCNetworking.shared.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + self.reloadDataSource() } completion: { _, searchResult, metadatas, error in if error != .success { NCContentPresenter().showError(error: error) @@ -1234,7 +1126,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - Push metadata func pushMetadata(_ metadata: tableMetadata) { - guard let navigationCollectionViewCommon = (tabBarController as? NCMainTabBarController)?.navigationCollectionViewCommon else { return } + guard let navigationCollectionViewCommon = self.controller?.navigationCollectionViewCommon else { return } let serverUrlPush = utilityFileSystem.stringAppendServerUrl(metadata.serverUrl, addFileName: metadata.fileName) if let viewController = navigationCollectionViewCommon.first(where: { $0.navigationController == self.navigationController && $0.serverUrl == serverUrlPush})?.viewController, viewController.isViewLoaded { @@ -1255,11 +1147,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - Header size - func isHeaderMenuTransferViewEnabled() -> Bool { - if headerMenuTransferView { - return NCManageDatabase.shared.getResultMetadataFromOcId(NCNetworking.shared.transferInForegorund?.ocId)?.isTransferInForeground ?? false + func isHeaderMenuTransferViewEnabled() -> [tableMetadata]? { + if headerMenuTransferView, + NCNetworking.shared.isOnline, + let results = database.getResultsMetadatas(predicate: NSPredicate(format: "status IN %@", [global.metadataStatusWaitUpload, global.metadataStatusUploading])), + !results.isEmpty { + return Array(results) } - return false + return nil } func getHeaderHeight(section: Int) -> (heightHeaderCommands: CGFloat, heightHeaderRichWorkspace: CGFloat, heightHeaderSection: CGFloat) { @@ -1268,12 +1163,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS func getHeaderHeight() -> CGFloat { var size: CGFloat = 0 - if isHeaderMenuTransferViewEnabled() { + if isHeaderMenuTransferViewEnabled() != nil { if !isSearchingMode { - size += NCGlobal.shared.heightHeaderTransfer + size += global.heightHeaderTransfer } - } else { - NCNetworking.shared.transferInForegorund = nil } return size } @@ -1285,11 +1178,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - if isSearchingMode || layoutForView?.groupBy != "none" || dataSource.numberOfSections() > 1 { + if isSearchingMode || layoutForView?.groupBy != "none" || self.dataSource.numberOfSections() > 1 { if section == 0 { - return (getHeaderHeight(), headerRichWorkspace, NCGlobal.shared.heightSection) + return (getHeaderHeight(), headerRichWorkspace, global.heightSection) } else { - return (0, 0, NCGlobal.shared.heightSection) + return (0, 0, global.heightSection) } } else { return (getHeaderHeight(), headerRichWorkspace, 0) @@ -1301,8 +1194,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS if isEditMode { return CGSize.zero - } else if dataSource.getMetadataSourceForAllSections().isEmpty { - height = NCGlobal.shared.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled()) + } else if self.dataSource.isEmpty() { + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: emptyDataPortaitOffset, landscapeOffset: emptyDataLandscapeOffset, isHeaderMenuTransferViewEnabled: isHeaderMenuTransferViewEnabled() != nil) } else { let (heightHeaderCommands, heightHeaderRichWorkspace, heightHeaderSection) = getHeaderHeight(section: section) height = heightHeaderCommands + heightHeaderRichWorkspace + heightHeaderSection @@ -1320,13 +1213,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS var size = CGSize(width: collectionView.frame.width, height: 0) if section == sections - 1 { - size.height += NCGlobal.shared.endHeightFooter + size.height += global.endHeightFooter } else { - size.height += NCGlobal.shared.heightFooter + size.height += global.heightFooter } if isSearchingMode && isPaginated && metadatasCount > 0 { - size.height += NCGlobal.shared.heightFooterButton + size.height += global.heightFooterButton } return size } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift new file mode 100644 index 0000000000..456acadafa --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonPinchGesture.swift @@ -0,0 +1,94 @@ +// +// NCCollectionViewCommonPinchGesture.swift +// Nextcloud +// +// Created by Marino Faggiana on 23/09/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +import Foundation +import UIKit + +extension NCCollectionViewCommon { + @objc func handlePinchGesture(_ gestureRecognizer: UIPinchGestureRecognizer) { + guard isLayoutPhoto else { return } + + func updateNumberOfColumns() { + let originalColumns = numberOfColumns + transitionColumns = true + + if currentScale < 1 && numberOfColumns < maxColumns { + numberOfColumns += 1 + } else if currentScale > 1 && numberOfColumns > 1 { + numberOfColumns -= 1 + } + + if originalColumns != numberOfColumns { + + self.collectionView.transform = .identity + self.currentScale = 1.0 + + UIView.transition(with: self.collectionView, duration: 0.20, options: .transitionCrossDissolve) { + + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + + } completion: { _ in + + if let layoutForView = self.database.getLayoutForView(account: self.session.account, key: NCGlobal.shared.layoutViewFiles, serverUrl: self.serverUrl) { + layoutForView.columnPhoto = self.numberOfColumns + self.database.setLayoutForView(layoutForView: layoutForView) + } + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.transitionColumns = false + } + } + + switch gestureRecognizer.state { + case .began: + lastScale = gestureRecognizer.scale + lastNumberOfColumns = numberOfColumns + case .changed: + guard !transitionColumns else { + return + } + let scale = gestureRecognizer.scale + let scaleChange = scale / lastScale + + currentScale *= scaleChange + currentScale = max(0.5, min(currentScale, 2.0)) + + updateNumberOfColumns() + + if numberOfColumns > 1 && numberOfColumns < maxColumns { + collectionView.transform = CGAffineTransform(scaleX: currentScale, y: currentScale) + } + + lastScale = scale + case .ended: + UIView.animate(withDuration: 0.30) { + self.currentScale = 1.0 + self.collectionView.transform = .identity + } + default: + break + } + } +} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift index 02a2759c89..1f6522b4b3 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift @@ -22,6 +22,7 @@ // import Foundation +import UIKit import SwiftUI protocol NCCollectionViewCommonSelectTabBarDelegate: AnyObject { @@ -89,9 +90,8 @@ class NCCollectionViewCommonSelectTabBar: ObservableObject { return hostingController.view.isHidden } - func update(selectOcId: [String], metadatas: [tableMetadata]? = nil, userId: String? = nil) { + func update(fileSelect: [String], metadatas: [tableMetadata]? = nil, userId: String? = nil) { if let metadatas { - isAnyOffline = false canSetAsOffline = true isAnyDirectory = false @@ -120,15 +120,17 @@ class NCCollectionViewCommonSelectTabBar: ObservableObject { guard !isAnyOffline else { continue } if metadata.directory, - let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl + "/" + metadata.fileName)) { + let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", + metadata.account, + metadata.serverUrl + "/" + metadata.fileName)) { isAnyOffline = directory.offline } else if let localFile = NCManageDatabase.shared.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { isAnyOffline = localFile.offline } // else: file is not offline, continue } - enableLock = !isAnyDirectory && canUnlock && !NCGlobal.shared.capabilityFilesLockVersion.isEmpty + enableLock = !isAnyDirectory && canUnlock && !NCCapabilities.shared.getCapabilities(account: controller?.account).capabilityFilesLockVersion.isEmpty } - isSelectedEmpty = selectOcId.isEmpty + isSelectedEmpty = fileSelect.isEmpty } } diff --git a/iOSClient/Data/NCDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift similarity index 63% rename from iOSClient/Data/NCDataSource.swift rename to iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift index 5575aca838..fc17fc68df 100644 --- a/iOSClient/Data/NCDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift @@ -1,5 +1,5 @@ // -// NCDataSource.swift +// NCCollectionViewDataSource.swift // Nextcloud // // Created by Marino Faggiana on 06/09/2020. @@ -23,79 +23,60 @@ import UIKit import NextcloudKit +import RealmSwift -class NCDataSource: NSObject { - var metadatas: [tableMetadata] = [] - var metadatasForSection: [NCMetadataForSection] = [] - var directory: tableDirectory? - var groupBy: String? - var layout: String? - +class NCCollectionViewDataSource: NSObject { private let utilityFileSystem = NCUtilityFileSystem() + private let utility = NCUtility() + private let global = NCGlobal.shared private var sectionsValue: [String] = [] private var providers: [NKSearchProvider]? private var searchResults: [NKSearchResult]? + private var results: Results? + private var metadatas: [tableMetadata] = [] + private var metadatasForSection: [NCMetadataForSection] = [] + private var layoutForView: NCDBLayoutForView? + private var metadataIndexPath = ThreadSafeDictionary() - private var ascending: Bool = true - private var sort: String = "" - private var directoryOnTop: Bool = true - private var favoriteOnTop: Bool = true + override init() { super.init() } - override init() { - super.init() - } - - init(metadatas: [tableMetadata], - account: String, - directory: tableDirectory? = nil, - layoutForView: NCDBLayoutForView?, - favoriteOnTop: Bool = true, + init(results: Results?, + layoutForView: NCDBLayoutForView? = nil, providers: [NKSearchProvider]? = nil, searchResults: [NKSearchResult]? = nil) { super.init() + removeAll() - self.metadatas = metadatas.filter({ - !(NCGlobal.shared.includeHiddenFiles.contains($0.fileNameView) || $0.isTransferInForeground) - }) - self.directory = directory - self.sort = layoutForView?.sort ?? "none" - self.ascending = layoutForView?.ascending ?? false - self.directoryOnTop = layoutForView?.directoryOnTop ?? true - self.favoriteOnTop = favoriteOnTop - self.groupBy = layoutForView?.groupBy ?? "none" - self.layout = layoutForView?.layout - // unified search + self.results = results + if let results { + self.metadatas = Array(results.freeze()) + } else { + self.metadatas = [] + } + + self.layoutForView = layoutForView + /// unified search self.providers = providers self.searchResults = searchResults - createSections() + if let providers, !providers.isEmpty || (layoutForView?.groupBy != "none") { + createSections() + } } // MARK: - - func clearDataSource() { + func removeAll() { self.metadatas.removeAll() + self.metadataIndexPath.removeAll() + self.results = nil + self.metadatasForSection.removeAll() - self.directory = nil self.sectionsValue.removeAll() self.providers = nil self.searchResults = nil } - func clearDirectory() { - self.directory = nil - } - - func changeGroupByField(_ groupBy: String) { - self.groupBy = groupBy - print("DATASOURCE: set group by filed " + groupBy) - self.metadatasForSection.removeAll() - self.sectionsValue.removeAll() - print("DATASOURCE: remove all sections") - - createSections() - } - func addSection(metadatas: [tableMetadata], searchResult: NKSearchResult?) { self.metadatas.append(contentsOf: metadatas) @@ -107,25 +88,17 @@ class NCDataSource: NSObject { } internal func createSections() { - // get all Section for metadata in self.metadatas { - // skipped livePhoto VIDEO part - if metadata.isLivePhoto && metadata.classFile == NKCommon.TypeClassFile.video.rawValue && metadata.status <= NCGlobal.shared.metadataStatusNormal { + /// skipped livePhoto VIDEO part + if metadata.isLivePhoto, metadata.classFile == NKCommon.TypeClassFile.video.rawValue { continue } let section = NSLocalizedString(self.getSectionValue(metadata: metadata), comment: "") if !self.sectionsValue.contains(section) { self.sectionsValue.append(section) } - // image Cache - if (layout == NCGlobal.shared.layoutPhotoRatio || layout == NCGlobal.shared.layoutPhotoSquare), - (metadata.isVideo || metadata.isImage), - NCImageCache.shared.getPreviewImageCache(ocId: metadata.ocId, etag: metadata.etag) == nil, - let image = UIImage(contentsOfFile: self.utilityFileSystem.getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag)) { - NCImageCache.shared.addPreviewImageCache(metadata: metadata, image: image) - } } - // Unified search + /// Unified search if let providers = self.providers, !providers.isEmpty { let sectionsDictionary = ThreadSafeDictionary() for section in self.sectionsValue { @@ -136,22 +109,27 @@ class NCDataSource: NSObject { self.sectionsValue.removeAll() let sectionsDictionarySorted = sectionsDictionary.sorted(by: {$0.value < $1.value }) for section in sectionsDictionarySorted { - if section.key == NCGlobal.shared.appName { + if section.key == global.appName { self.sectionsValue.insert(section.key, at: 0) } else { self.sectionsValue.append(section.key) } } } else { - // normal + /// normal let directory = NSLocalizedString("directory", comment: "").lowercased().firstUppercased self.sectionsValue = self.sectionsValue.sorted { - if directoryOnTop && $0 == directory { + if let directoryOnTop = layoutForView?.directoryOnTop, + directoryOnTop, + $0 == directory { return true - } else if directoryOnTop && $1 == directory { + } else if let directoryOnTop = layoutForView?.directoryOnTop, + directoryOnTop, + $1 == directory { return false } - if self.ascending { + if let ascending = layoutForView?.ascending, + ascending { return $0 < $1 } else { return $0 > $1 @@ -176,22 +154,10 @@ class NCDataSource: NSObject { let metadataForSection = NCMetadataForSection(sectionValue: sectionValue, metadatas: metadatas, lastSearchResult: searchResult, - sort: self.sort, - ascending: self.ascending, - directoryOnTop: self.directoryOnTop, - favoriteOnTop: self.favoriteOnTop) + layoutForView: self.layoutForView) metadatasForSection.append(metadataForSection) } - func getMetadataSourceForAllSections() -> [tableMetadata] { - var metadatas: [tableMetadata] = [] - - for section in metadatasForSection { - metadatas.append(contentsOf: section.metadatas) - } - return metadatas - } - // MARK: - func appendMetadatasToSection(_ metadatas: [tableMetadata], metadataForSection: NCMetadataForSection, lastSearchResult: NKSearchResult) { @@ -212,47 +178,137 @@ class NCDataSource: NSObject { // MARK: - - func getIndexPathMetadata(ocId: String) -> (indexPath: IndexPath?, metadataForSection: NCMetadataForSection?) { - guard let metadata = self.metadatas.filter({ $0.ocId == ocId}).first else { return (nil, nil) } - let sectionValue = getSectionValue(metadata: metadata) - guard let sectionIndex = getSectionIndex(sectionValue), let metadataForSection = getMetadataForSection(sectionValue), let rowIndex = metadataForSection.metadatas.firstIndex(where: {$0.ocId == ocId}) else { return (nil, nil) } - return (IndexPath(row: rowIndex, section: sectionIndex), metadataForSection) + func getMetadatas() -> [tableMetadata] { + return self.metadatas + } + + func isEmpty() -> Bool { + return self.metadatas.isEmpty + } + + func getIndexPathMetadata(ocId: String) -> IndexPath? { + guard self.sectionsValue.isEmpty else { return nil } + let validMetadatas = self.metadatas.filter { !$0.isInvalidated } + + if let rowIndex = validMetadatas.firstIndex(where: {$0.ocId == ocId}) { + return IndexPath(row: rowIndex, section: 0) + } + + return nil } func numberOfSections() -> Int { guard !self.sectionsValue.isEmpty else { return 1 } + return self.sectionsValue.count } func numberOfItemsInSection(_ section: Int) -> Int { - guard !self.sectionsValue.isEmpty && !self.metadatas.isEmpty, let metadataForSection = getMetadataForSection(section) else { return 0} - return metadataForSection.metadatas.count - } + if self.sectionsValue.isEmpty { + let validMetadatas = metadatas.filter { !$0.isInvalidated } + return validMetadatas.count + } + guard !self.metadatas.isEmpty, + let metadataForSection = getMetadataForSection(section) + else { return 0} - func cellForItemAt(indexPath: IndexPath) -> tableMetadata? { - guard !metadatasForSection.isEmpty && indexPath.section < metadatasForSection.count, let metadataForSection = getMetadataForSection(indexPath.section), indexPath.row < metadataForSection.metadatas.count else { return nil } - return metadataForSection.metadatas[indexPath.row] + return metadataForSection.metadatas.count } func getSectionValueLocalization(indexPath: IndexPath) -> String { guard !metadatasForSection.isEmpty, let metadataForSection = self.getMetadataForSection(indexPath.section) else { return ""} + if let searchResults = self.searchResults, let searchResult = searchResults.filter({ $0.id == metadataForSection.sectionValue}).first { return searchResult.name } + return metadataForSection.sectionValue } - func getFooterInformationAllMetadatas() -> (directories: Int, files: Int, size: Int64) { - var directories: Int = 0 - var files: Int = 0 + func getFooterInformation() -> (directories: Int, files: Int, size: Int64) { + let validMetadatas = metadatas.filter { !$0.isInvalidated } + let directories = validMetadatas.filter({ $0.directory == true}) + let files = validMetadatas.filter({ $0.directory == false}) var size: Int64 = 0 - for metadataForSection in metadatasForSection { - directories += metadataForSection.numDirectory - files += metadataForSection.numFile - size += metadataForSection.totalSize + files.forEach { metadata in + size += metadata.size + } + + return (directories.count, files.count, size) + } + + func getResultMetadata(indexPath: IndexPath) -> tableMetadata? { + let validMetadatas = metadatas.filter { !$0.isInvalidated } + + if indexPath.row < validMetadatas.count { + return validMetadatas[indexPath.row] + } + + return nil + } + + func getMetadata(indexPath: IndexPath) -> tableMetadata? { + if !metadatasForSection.isEmpty, indexPath.section < metadatasForSection.count { + if let metadataForSection = getMetadataForSection(indexPath.section), + indexPath.row < metadataForSection.metadatas.count, + !metadataForSection.metadatas[indexPath.row].isInvalidated { + return tableMetadata(value: metadataForSection.metadatas[indexPath.row]) + } + } else if indexPath.row < self.metadatas.count { + if let metadata = metadataIndexPath[indexPath] { + return metadata + } else { + let validMetadatas = self.metadatas.filter { !$0.isInvalidated } + let metadata = tableMetadata(value: validMetadatas[indexPath.row]) + metadataIndexPath[indexPath] = metadata + return metadata + } + } + + return nil + } + + func caching(metadatas: [tableMetadata], dataSourceMetadatas: [tableMetadata], completion: @escaping (_ update: Bool) -> Void) { + var counter: Int = 0 + var updated: Bool = dataSourceMetadatas.isEmpty + + DispatchQueue.global().async { + for metadata in metadatas { + let indexPath = IndexPath(row: counter, section: 0) + if indexPath.row < dataSourceMetadatas.count { + if !metadata.isEqual(dataSourceMetadatas[indexPath.row]) { + updated = true + } + } else { + updated = true + } + + self.metadataIndexPath[indexPath] = tableMetadata(value: metadata) + + /// caching preview + /// + if metadata.isImageOrVideo, + NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: self.global.previewExt256) == nil, + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: self.global.previewExt256) { + NCImageCache.shared.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: self.global.previewExt256, cost: counter) + } + + counter += 1 + } + + DispatchQueue.main.async { + return completion(updated) + } + } + } + + func removeImageCache() { + DispatchQueue.global().async { + for metadata in self.metadatas { + NCImageCache.shared.removeImageCache(ocIdPlusEtag: metadata.ocId + metadata.etag) + } } - return (directories, files, size) } // MARK: - @@ -263,7 +319,7 @@ class NCDataSource: NSObject { } internal func getSectionValue(metadata: tableMetadata) -> String { - switch self.groupBy { + switch self.layoutForView?.groupBy { case "name", "none": return NSLocalizedString(metadata.name, comment: "") case "classFile": @@ -303,11 +359,7 @@ class NCMetadataForSection: NSObject { var metadatas: [tableMetadata] var lastSearchResult: NKSearchResult? var unifiedSearchInProgress: Bool = false - - private var sort: String - private var ascending: Bool - private var directoryOnTop: Bool - private var favoriteOnTop: Bool + var layoutForView: NCDBLayoutForView? private var metadatasSorted: [tableMetadata] = [] private var metadatasFavoriteDirectory: [tableMetadata] = [] @@ -319,15 +371,11 @@ class NCMetadataForSection: NSObject { public var numFile: Int = 0 public var totalSize: Int64 = 0 - init(sectionValue: String, metadatas: [tableMetadata], lastSearchResult: NKSearchResult?, sort: String, ascending: Bool, directoryOnTop: Bool, favoriteOnTop: Bool) { - + init(sectionValue: String, metadatas: [tableMetadata], lastSearchResult: NKSearchResult?, layoutForView: NCDBLayoutForView?) { self.sectionValue = sectionValue self.metadatas = metadatas self.lastSearchResult = lastSearchResult - self.sort = sort - self.ascending = ascending - self.directoryOnTop = directoryOnTop - self.favoriteOnTop = favoriteOnTop + self.layoutForView = layoutForView super.init() @@ -352,23 +400,23 @@ class NCMetadataForSection: NSObject { // Metadata order // - if sort != "none" && !sort.isEmpty { + if let layoutForView = self.layoutForView, layoutForView.sort != "none" && !layoutForView.sort.isEmpty { metadatasSorted = metadatas.sorted { - switch sort { + switch layoutForView.sort { case "date": - if ascending { + if layoutForView.ascending { return ($0.date as Date) < ($1.date as Date) } else { return ($0.date as Date) > ($1.date as Date) } case "size": - if ascending { + if layoutForView.ascending { return $0.size < $1.size } else { return $0.size > $1.size } default: - if ascending { + if layoutForView.ascending { return $0.fileNameView.lowercased() < $1.fileNameView.lowercased() } else { return $0.fileNameView.lowercased() > $1.fileNameView.lowercased() @@ -388,8 +436,9 @@ class NCMetadataForSection: NSObject { continue } - // skipped livePhoto - if metadata.isLivePhoto && metadata.classFile == NKCommon.TypeClassFile.video.rawValue && metadata.status <= NCGlobal.shared.metadataStatusNormal { + // skipped livePhoto VIDEO part + if metadata.isLivePhoto, + metadata.classFile == NKCommon.TypeClassFile.video.rawValue { continue } @@ -404,13 +453,13 @@ class NCMetadataForSection: NSObject { } // Organized the metadata - if metadata.favorite && favoriteOnTop { + if metadata.favorite { if metadata.directory { metadatasFavoriteDirectory.append(metadata) } else { metadatasFavoriteFile.append(metadata) } - } else if metadata.directory && directoryOnTop { + } else if metadata.directory && layoutForView?.directoryOnTop ?? true { metadatasDirectory.append(metadata) } else { metadatasFile.append(metadata) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift index 33d3e0dbe0..627070cc57 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift @@ -27,56 +27,61 @@ import Queuer import NextcloudKit import RealmSwift -class NCCollectionViewDownloadThumbnail: ConcurrentOperation { +class NCCollectionViewDownloadThumbnail: ConcurrentOperation, @unchecked Sendable { var metadata: tableMetadata - var cell: NCCellProtocol? var collectionView: UICollectionView? - var fileNamePreviewLocalPath: String - var fileNameIconLocalPath: String + var ext = "" let utilityFileSystem = NCUtilityFileSystem() + let utility = NCUtility() - init(metadata: tableMetadata, cell: NCCellProtocol?, collectionView: UICollectionView?) { + init(metadata: tableMetadata, collectionView: UICollectionView?, ext: String) { self.metadata = tableMetadata.init(value: metadata) - self.cell = cell self.collectionView = collectionView - self.fileNamePreviewLocalPath = utilityFileSystem.getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag) - self.fileNameIconLocalPath = utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag) + self.ext = ext } override func start() { guard !isCancelled else { return self.finish() } var etagResource: String? - let sizePreview = NCUtility().getSizePreview(width: metadata.width, height: metadata.height) - if FileManager.default.fileExists(atPath: fileNameIconLocalPath) && FileManager.default.fileExists(atPath: fileNamePreviewLocalPath) { + if utilityFileSystem.fileProviderStorageImageExists(metadata.ocId, etag: metadata.etag) { etagResource = metadata.etagResource } NextcloudKit.shared.downloadPreview(fileId: metadata.fileId, - fileNamePreviewLocalPath: fileNamePreviewLocalPath, - fileNameIconLocalPath: fileNameIconLocalPath, - widthPreview: Int(sizePreview.width), - heightPreview: Int(sizePreview.height), - sizeIcon: NCGlobal.shared.sizeIcon, etag: etagResource, account: self.metadata.account, - options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { _, _, imageIcon, _, etag, error in - if error == .success, let imageIcon { + options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { _, _, _, etag, responseData, error in + + if error == .success, let data = responseData?.data, let collectionView = self.collectionView { + NCManageDatabase.shared.setMetadataEtagResource(ocId: self.metadata.ocId, etagResource: etag) + NCUtility().createImageFileFrom(data: data, metadata: self.metadata) + let image = self.utility.getImage(ocId: self.metadata.ocId, etag: self.metadata.etag, ext: self.ext) + DispatchQueue.main.async { - if self.metadata.ocId == self.cell?.fileObjectId, let filePreviewImageView = self.cell?.filePreviewImageView { - self.cell?.filePreviewImageView?.contentMode = .scaleAspectFill - if self.metadata.hasPreviewBorder { - self.cell?.filePreviewImageView?.layer.borderWidth = 0.2 - self.cell?.filePreviewImageView?.layer.borderColor = UIColor.systemGray3.cgColor + for case let cell as NCCellProtocol in collectionView.visibleCells where cell.fileOcId == self.metadata.ocId { + if let filePreviewImageView = cell.filePreviewImageView { + filePreviewImageView.contentMode = .scaleAspectFill + + if self.metadata.hasPreviewBorder { + filePreviewImageView.layer.borderWidth = 0.2 + filePreviewImageView.layer.borderColor = UIColor.systemGray3.cgColor + } + + if let photoCell = (cell as? NCPhotoCell), + photoCell.bounds.size.width > 100 { + cell.hideButtonMore(false) + cell.hideImageStatus(false) + } + + UIView.transition(with: filePreviewImageView, + duration: 0.75, + options: .transitionCrossDissolve, + animations: { filePreviewImageView.image = image }, + completion: nil) + break } - UIView.transition(with: filePreviewImageView, - duration: 0.75, - options: .transitionCrossDissolve, - animations: { filePreviewImageView.image = imageIcon }, - completion: nil) - } else { - self.collectionView?.reloadData() } } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift b/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift index a60177e494..5fd8de1e52 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift @@ -22,11 +22,12 @@ // import Foundation +import UIKit import Queuer import NextcloudKit import RealmSwift -class NCCollectionViewUnifiedSearch: ConcurrentOperation { +class NCCollectionViewUnifiedSearch: ConcurrentOperation, @unchecked Sendable { var collectionViewCommon: NCCollectionViewCommon var metadatas: [tableMetadata] var searchResult: NKSearchResult diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift index e71baa5fae..7a25864684 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift @@ -25,14 +25,12 @@ import UIKit import MarkdownKit protocol NCSectionFirstHeaderDelegate: AnyObject { - func tapButtonTransfer(_ sender: Any) func tapRichWorkspace(_ sender: Any) } class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegate { - @IBOutlet weak var buttonTransfer: UIButton! - @IBOutlet weak var imageButtonTransfer: UIImageView! + @IBOutlet weak var imageTransfer: UIImageView! @IBOutlet weak var labelTransfer: UILabel! @IBOutlet weak var progressTransfer: UIProgressView! @IBOutlet weak var transferSeparatorBottom: UIView! @@ -78,16 +76,13 @@ class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegat labelSection.text = "" viewSectionHeightConstraint.constant = 0 - buttonTransfer.backgroundColor = .clear - buttonTransfer.setImage(nil, for: .normal) - buttonTransfer.layer.cornerRadius = 6 - buttonTransfer.layer.masksToBounds = true - imageButtonTransfer.image = NCUtility().loadImage(named: "stop.circle") - imageButtonTransfer.tintColor = .white - labelTransfer.text = "" + imageTransfer.tintColor = NCBrandColor.shared.iconImageColor + imageTransfer.image = NCUtility().loadImage(named: "icloud.and.arrow.up") + progressTransfer.progress = 0 - progressTransfer.tintColor = NCBrandColor.shared.brandElement - progressTransfer.trackTintColor = NCBrandColor.shared.brandElement.withAlphaComponent(0.2) + progressTransfer.tintColor = NCBrandColor.shared.iconImageColor + progressTransfer.trackTintColor = NCBrandColor.shared.customer.withAlphaComponent(0.2) + transferSeparatorBottom.backgroundColor = .separator transferSeparatorBottomHeightConstraint.constant = 0.5 } @@ -135,29 +130,28 @@ class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegat // MARK: - Transfer - func setViewTransfer(isHidden: Bool, ocId: String? = nil, text: String? = nil, progress: Float? = nil) { - labelTransfer.text = text + func setViewTransfer(isHidden: Bool, progress: Float? = nil) { viewTransfer.isHidden = isHidden - progressTransfer.progress = 0 if isHidden { viewTransferHeightConstraint.constant = 0 + progressTransfer.progress = 0 } else { - var image: UIImage? - if let ocId, - let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { - image = utility.getIcon(metadata: metadata)?.darken() - if image == nil { - image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true) - buttonTransfer.backgroundColor = .lightGray + viewTransferHeightConstraint.constant = NCGlobal.shared.heightHeaderTransfer + if NCTransferProgress.shared.haveUploadInForeground() { + labelTransfer.text = String(format: NSLocalizedString("_upload_foreground_msg_", comment: ""), NCBrandOptions.shared.brand) + if let progress { + progressTransfer.progress = progress + } else if let progress = NCTransferProgress.shared.getLastTransferProgressInForeground() { + progressTransfer.progress = progress } else { - buttonTransfer.backgroundColor = .clear + progressTransfer.progress = 0.0 } + } else { + labelTransfer.text = NSLocalizedString("_upload_background_msg_", comment: "") + progressTransfer.progress = 0.0 } - viewTransferHeightConstraint.constant = NCGlobal.shared.heightHeaderTransfer - if let progress { - progressTransfer.progress = progress - } + } } @@ -175,10 +169,6 @@ class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegat // MARK: - Action - @IBAction func touchUpTransfer(_ sender: Any) { - delegate?.tapButtonTransfer(sender) - } - @objc func touchUpInsideViewRichWorkspace(_ sender: Any) { delegate?.tapRichWorkspace(sender) } diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.xib b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.xib index 9568658d6b..a75c025870 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.xib +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.xib @@ -45,25 +45,15 @@ - - - + + - diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib index adda67762c..ced40b7812 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib @@ -1,10 +1,9 @@ - + - - + @@ -16,7 +15,7 @@ - diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index 152a818cf8..a6fa46927c 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -37,18 +37,18 @@ class NCShareHeader: UIView { func setupUI(with metadata: tableMetadata) { let utilityFileSystem = NCUtilityFileSystem() - if FileManager.default.fileExists(atPath: utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)) { - fullWidthImageView.image = NCUtility().getImageMetadata(metadata, for: frame.height) + if let image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) { + fullWidthImageView.image = image fullWidthImageView.contentMode = .scaleAspectFill imageView.image = fullWidthImageView.image imageView.isHidden = true } else { if metadata.directory { - imageView.image = metadata.e2eEncrypted ? NCImageCache.images.folderEncrypted : NCImageCache.images.folder + imageView.image = metadata.e2eEncrypted ? NCImageCache.shared.getFolderEncrypted(account: metadata.account) : NCImageCache.shared.getFolder(account: metadata.account) } else if !metadata.iconName.isEmpty { - imageView.image = NCUtility().loadImage(named: metadata.iconName, useTypeIconFile: true) + imageView.image = NCUtility().loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } else { - imageView.image = NCImageCache.images.file + imageView.image = NCImageCache.shared.getImageFile() } fileNameTopConstraint.constant -= 45 diff --git a/iOSClient/Share/NCShareLinkCell.swift b/iOSClient/Share/NCShareLinkCell.swift index e2dcaea08d..9d18291873 100644 --- a/iOSClient/Share/NCShareLinkCell.swift +++ b/iOSClient/Share/NCShareLinkCell.swift @@ -65,7 +65,7 @@ class NCShareLinkCell: UITableViewCell { menuButton.accessibilityLabel = NSLocalizedString("_add_", comment: "") } - imageItem.image = NCUtility().loadImage(named: "link.circle.fill", colors: [NCBrandColor.shared.brandElement]) + imageItem.image = NCUtility().loadImage(named: "link.circle.fill", colors: [NCBrandColor.shared.getElement(account: tableShare?.account)]) menuButton.setImage(NCUtility().loadImage(named: menuImageName, colors: [NCBrandColor.shared.iconImageColor]), for: .normal) } diff --git a/iOSClient/Share/NCShareLinkCell.xib b/iOSClient/Share/NCShareLinkCell.xib index a102ccf497..69ea77ce79 100755 --- a/iOSClient/Share/NCShareLinkCell.xib +++ b/iOSClient/Share/NCShareLinkCell.xib @@ -1,9 +1,9 @@ - + - + @@ -37,7 +37,7 @@ - + @@ -111,7 +111,7 @@ - + diff --git a/iOSClient/Share/NCShareNetworking.swift b/iOSClient/Share/NCShareNetworking.swift index a11cc68350..a327e2eed5 100644 --- a/iOSClient/Share/NCShareNetworking.swift +++ b/iOSClient/Share/NCShareNetworking.swift @@ -24,40 +24,40 @@ import UIKit import NextcloudKit class NCShareNetworking: NSObject { - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let utilityFileSystem = NCUtilityFileSystem() + let database = NCManageDatabase.shared weak var delegate: NCShareNetworkingDelegate? var view: UIView var metadata: tableMetadata + var session: NCSession.Session - init(metadata: tableMetadata, view: UIView, delegate: NCShareNetworkingDelegate?) { + init(metadata: tableMetadata, view: UIView, delegate: NCShareNetworkingDelegate?, session: NCSession.Session) { self.metadata = metadata self.view = view self.delegate = delegate + self.session = session super.init() } func readShare(showLoadingIndicator: Bool) { - if showLoadingIndicator { NCActivityIndicator.shared.start(backgroundView: view) } - - let filenamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId) + let filenamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) let parameter = NKShareParameter(path: filenamePath) NextcloudKit.shared.readShares(parameters: parameter, account: metadata.account) { account, shares, _, error in if error == .success, let shares = shares { - NCManageDatabase.shared.deleteTableShare(account: account, path: "/" + filenamePath) - let home = self.utilityFileSystem.getHomeServer(urlBase: self.metadata.urlBase, userId: self.metadata.userId) - NCManageDatabase.shared.addShare(account: self.metadata.account, home: home, shares: shares) + self.database.deleteTableShare(account: account, path: "/" + filenamePath) + let home = self.utilityFileSystem.getHomeServer(session: self.session) + self.database.addShare(account: self.metadata.account, home: home, shares: shares) NextcloudKit.shared.getGroupfolders(account: account) { account, results, _, error in if showLoadingIndicator { NCActivityIndicator.shared.stop() } if error == .success, let groupfolders = results { - NCManageDatabase.shared.addGroupfolders(account: account, groupfolders: groupfolders) + self.database.addGroupfolders(account: account, groupfolders: groupfolders) } self.delegate?.readShareCompleted() } @@ -80,14 +80,14 @@ class NCShareNetworking: NSObject { // https://github.com/nextcloud/ios-communication-library/pull/104 NCActivityIndicator.shared.start(backgroundView: view) - let filenamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId) + let filenamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) NextcloudKit.shared.createShare(path: filenamePath, shareType: option.shareType, shareWith: option.shareWith, password: option.password, note: option.note, permissions: option.permissions, attributes: option.attributes, account: metadata.account) { _, share, _, error in NCActivityIndicator.shared.stop() if error == .success, let share = share { option.idShare = share.idShare - let home = self.utilityFileSystem.getHomeServer(urlBase: self.metadata.urlBase, userId: self.metadata.userId) - NCManageDatabase.shared.addShare(account: self.metadata.account, home: home, shares: [share]) + let home = self.utilityFileSystem.getHomeServer(session: self.session) + self.database.addShare(account: self.metadata.account, home: home, shares: [share]) if option.hasChanges(comparedTo: share) { self.updateShare(option: option) } @@ -100,10 +100,10 @@ class NCShareNetworking: NSObject { func unShare(idShare: Int) { NCActivityIndicator.shared.start(backgroundView: view) - NextcloudKit.shared.deleteShare(idShare: idShare, account: metadata.account) { account, error in + NextcloudKit.shared.deleteShare(idShare: idShare, account: metadata.account) { account, _, error in NCActivityIndicator.shared.stop() if error == .success { - NCManageDatabase.shared.deleteTableShare(account: account, idShare: idShare) + self.database.deleteTableShare(account: account, idShare: idShare) self.delegate?.unShareCompleted() } else { NCContentPresenter().showError(error: error) @@ -116,8 +116,8 @@ class NCShareNetworking: NSObject { NextcloudKit.shared.updateShare(idShare: option.idShare, password: option.password, expireDate: option.expDateString, permissions: option.permissions, note: option.note, label: option.label, hideDownload: option.hideDownload, attributes: option.attributes, account: metadata.account) { _, share, _, error in NCActivityIndicator.shared.stop() if error == .success, let share = share { - let home = self.utilityFileSystem.getHomeServer(urlBase: self.metadata.urlBase, userId: self.metadata.userId) - NCManageDatabase.shared.addShare(account: self.metadata.account, home: home, shares: [share]) + let home = self.utilityFileSystem.getHomeServer(session: self.session) + self.database.addShare(account: self.metadata.account, home: home, shares: [share]) self.delegate?.readShareCompleted() } else { NCContentPresenter().showError(error: error) diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 269c2ec5c8..b7dc569a9c 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -33,7 +33,6 @@ protocol NCSharePagingContent { } class NCSharePaging: UIViewController { - private let pagingViewController = NCShareHeaderViewController() private weak var appDelegate = UIApplication.shared.delegate as? AppDelegate private var currentVC: NCSharePagingContent? @@ -63,9 +62,9 @@ class NCSharePaging: UIViewController { pagingViewController.backgroundColor = .systemBackground pagingViewController.menuBackgroundColor = .systemBackground pagingViewController.selectedBackgroundColor = .systemBackground - pagingViewController.indicatorColor = NCBrandColor.shared.brandElement + pagingViewController.indicatorColor = NCBrandColor.shared.getElement(account: metadata.account) pagingViewController.textColor = NCBrandColor.shared.textColor - pagingViewController.selectedTextColor = NCBrandColor.shared.brandElement + pagingViewController.selectedTextColor = NCBrandColor.shared.getElement(account: metadata.account) // Pagination addChild(pagingViewController) @@ -110,7 +109,7 @@ class NCSharePaging: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if NCGlobal.shared.disableSharesView { + if NCCapabilities.shared.disableSharesView(account: metadata.account) { self.dismiss(animated: false, completion: nil) } @@ -121,7 +120,7 @@ class NCSharePaging: UIViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource, userInfo: ["serverUrl": metadata.serverUrl]) } deinit { @@ -193,6 +192,7 @@ extension NCSharePaging: PagingViewControllerDataSource { viewController.didSelectItemEnable = false viewController.metadata = metadata viewController.objectType = "files" + viewController.account = metadata.account return viewController } else if pages[index] == .sharing { guard let viewController = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "sharing") as? NCShare else { diff --git a/iOSClient/Share/NCShareUserCell.swift b/iOSClient/Share/NCShareUserCell.swift index fe96a866e0..576964010a 100644 --- a/iOSClient/Share/NCShareUserCell.swift +++ b/iOSClient/Share/NCShareUserCell.swift @@ -152,7 +152,7 @@ class NCSearchUserDropDownCell: DropDownCell, NCCellProtocol { set { user = newValue ?? "" } } - func setupCell(sharee: NKSharee, userBaseUrl: NCUserBaseUrl) { + func setupCell(sharee: NKSharee, session: NCSession.Session) { let utility = NCUtility() imageItem.image = NCShareCommon().getImageShareType(shareType: sharee.shareType) imageShareeType.image = NCShareCommon().getImageShareType(shareType: sharee.shareType) @@ -170,23 +170,20 @@ class NCSearchUserDropDownCell: DropDownCell, NCCellProtocol { centerTitle.constant = 0 } - imageItem.image = utility.loadUserImage( - for: sharee.shareWith, - displayName: nil, - userBaseUrl: userBaseUrl) + imageItem.image = utility.loadUserImage(for: sharee.shareWith, displayName: nil, urlBase: session.urlBase) - let fileName = userBaseUrl.userBaseUrl + "-" + sharee.shareWith + ".png" - if NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) == nil { - let fileNameLocalPath = NCUtilityFileSystem().directoryUserData + "/" + fileName + let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: sharee.shareWith) + let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) + + if results.image == nil { let etag = NCManageDatabase.shared.getTableAvatar(fileName: fileName)?.etag NextcloudKit.shared.downloadAvatar( user: sharee.shareWith, - fileNameLocalPath: fileNameLocalPath, + fileNameLocalPath: NCUtilityFileSystem().directoryUserData + "/" + fileName, sizeImage: NCGlobal.shared.avatarSize, avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etag: etag, account: userBaseUrl.account) { _, imageAvatar, _, etag, error in - + etag: etag, account: session.account) { _, imageAvatar, _, etag, _, error in if error == .success, let etag = etag, let imageAvatar = imageAvatar { NCManageDatabase.shared.addAvatar(fileName: fileName, etag: etag) self.imageItem.image = imageAvatar diff --git a/iOSClient/Shares/NCShares.swift b/iOSClient/Shares/NCShares.swift index 7f45bf1579..0997e3b353 100644 --- a/iOSClient/Shares/NCShares.swift +++ b/iOSClient/Shares/NCShares.swift @@ -33,7 +33,7 @@ class NCShares: NCCollectionViewCommon { layoutKey = NCGlobal.shared.layoutViewShares enableSearchBar = false headerRichWorkspaceDisable = true - emptyImage = utility.loadImage(named: "person.fill.badge.plus", colors: [NCBrandColor.shared.brandElement]) + emptyImageName = "person.fill.badge.plus" emptyTitle = "_list_shares_no_files_" emptyDescription = "_tutorial_list_shares_view_" } @@ -42,70 +42,68 @@ class NCShares: NCCollectionViewCommon { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if dataSource.metadatas.isEmpty { - reloadDataSource() - } - reloadDataSourceNetwork() + + reloadDataSource() } - // MARK: - DataSource + NC Endpoint + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) - override func queryDB() { - super.queryDB() + getServerData() + } - var metadatas: [tableMetadata] = [] + // MARK: - DataSource - func reload() { - self.dataSource = NCDataSource(metadatas: metadatas, account: appDelegate.account, layoutForView: layoutForView, providers: providers, searchResults: searchResults) - DispatchQueue.main.async { - self.refreshControl.endRefreshing() - self.collectionView.reloadData() - } - } + override func reloadDataSource() { + var ocId: [String] = [] + let sharess = self.database.getTableShares(account: session.account) - let sharess = NCManageDatabase.shared.getTableShares(account: appDelegate.account) for share in sharess { - if let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", appDelegate.account, share.serverUrl, share.fileName)) { - if !(metadatas.contains { $0.ocId == metadata.ocId }) { - metadatas.append(metadata) + if let result = self.database.getResultMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", session.account, share.serverUrl, share.fileName)) { + if !(ocId.contains { $0 == result.ocId }) { + ocId.append(result.ocId) } } else { let serverUrlFileName = share.serverUrl + "/" + share.fileName - NCNetworking.shared.readFile(serverUrlFileName: serverUrlFileName, account: appDelegate.account) { task in + NCNetworking.shared.readFile(serverUrlFileName: serverUrlFileName, account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + if self.dataSource.isEmpty() { + self.collectionView.reloadData() + } } completion: { _, metadata, _ in if let metadata { - NCManageDatabase.shared.addMetadata(metadata) - if !(metadatas.contains { $0.ocId == metadata.ocId }) { - metadatas.append(metadata) - reload() + self.database.addMetadata(metadata) + if !(ocId.contains { $0 == metadata.ocId }) { + ocId.append(metadata.ocId) } } } } } - reload() - } + let results = self.database.getResultsMetadatasPredicate(NSPredicate(format: "ocId IN %@", ocId), layoutForView: layoutForView) + + self.dataSource = NCCollectionViewDataSource(results: results, layoutForView: layoutForView) - override func reloadDataSourceNetwork(withQueryDB: Bool = false) { - super.reloadDataSourceNetwork() + super.reloadDataSource() + } - NextcloudKit.shared.readShares(parameters: NKShareParameter(), account: appDelegate.account) { task in + override func getServerData() { + NextcloudKit.shared.readShares(parameters: NKShareParameter(), account: session.account) { task in self.dataSourceTask = task - self.collectionView.reloadData() + if self.dataSource.isEmpty() { + self.collectionView.reloadData() + } } completion: { account, shares, _, error in if error == .success { - NCManageDatabase.shared.deleteTableShare(account: account) + self.database.deleteTableShare(account: account) if let shares = shares, !shares.isEmpty { - let home = self.utilityFileSystem.getHomeServer(urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId) - NCManageDatabase.shared.addShare(account: self.appDelegate.account, home: home, shares: shares) + let home = self.utilityFileSystem.getHomeServer(session: self.session) + self.database.addShare(account: account, home: home, shares: shares) } self.reloadDataSource() - } else { - self.reloadDataSource(withQueryDB: withQueryDB) } + self.refreshControl.endRefreshing() } } } diff --git a/iOSClient/Supporting Files/af.lproj/Localizable.strings b/iOSClient/Supporting Files/af.lproj/Localizable.strings index cb69929d96dc23a45f4418344dda333f4460cddf..15575872ada36c2974de6f8512489660c3b27cd1 100644 GIT binary patch delta 2159 zcmbtVZERCz6uzfx?z*n)Za*$t=~`~L0VBd-fPxHUBh&ctV>KEhi3xP;IxAZ{Tebxf z9T0WVAL2ZQW0p9-exO2hX3;7T*bgvZU}0(yk%*8$q9Q>ek;KG!&fAVof0>w@`_cD3 z?{l8>oacSJ|3$}@_Z{QgSKyD!Yf2MZOdHVB*q$;eYPZ?(+2t)IF)fZuQzl;Pag=S- z(ptZk(JUPK#W&Hn;#LLJQq#k*uKWS5mlta5fB|i>Vf)h6c>uK|+pwj_27_{L}@Z*{~CoX%zj;C+xbjSlvx|RcX zKKj1r>v1ydphIqOd$uweON(>L9=^9~U3Ae60To!1;a`yeL~ciy=A$z-vK~yj%Ioup zsmUTDwl#Rc!jC3{vjTo^E(Vk^2*qCi2f%Bt;(SFuNiP!{$kUleKes~zoxa6oBn&ts z669EdDIH(>+)HPV!d$%m$y~BrU`Okq1F|?`1ZXS`)wpbA4(>SVugexH?Bp6P7TMBd zBqJ&K*(euAHV5}EuLh(uAAo@mhQ0WfUQNb6Fl-B~T*E$Sn_HgX=rp&9F)-pXspc5? zY4jMZ<-V&KYW)IyxvR$@<;Ul&0dr7$QcEgvH));x9pkWWYz+rr4Li|&w{D8=R&k1h zQylP&6(Pe1VT_*Far}Ue$1MXdS=HFH(P?HmoSVZ}O54PUgM4qn$dTZlZg56s&q}Fk zRwXHL!KFhcWTt@>Y{Z!U{PhCs<4wMQVf{Hk0SxmLb5sNy)VIywng63US7<$#qSP=d|j+c zA)0}b@=XxL#c3D~}qf%JDl}Bz?ZCs;0!F9~n zTG(jvC3jT0LJ&oO8L@kmB@0sUc#`L{1wr%Kr6p3Q^frs0X5UZ|vz0M>c&1>p4^W6gZz^-McVHO3gSr_)S0 z$=_L;)**!Fr&rVBy?kCAj)SwR2-3k&eQdI{EWyb4K6<5@PYFgVD-J(B?W1pWK2&B< zaP$s?CZB?OjGof*x@oV=F!4OWK)LLa1UH3KljkJ(@UPVlWzW#P$~Iz( zL7tU1%AZXBtV?0=+#;Gh1O6eAshjx;`|S)q!~Yz6|2H3n?6%#11O0z2jQ{`u delta 414 zcmYL_F-SsT5XbLcBo9gR*-IjWz8Y*15t69LD2cY_=2uESdgVhi$w4Xzs-fi@z9kxq zBEopYYlx->TOy)4qM<1zhoULECt}mx{qDH?|L^@&KHrne#bIkAR`XcJgxSp>!*1() zK2+o#eZKZQ7+?rx3WqrJ^$2qked4x`a$18dEJ7M|Sceoa2r&k8puq;^YZR9oRHMp( z46MRZgX^$DJVN;lNuNn#7&|Z+MJ>)2VoG=I*xi6B>(4md^4ypy5YfYzBZf>05<1Thlj!x&=-VU%_sg?`anwv;GL zgg^|;!7IKRCBZ}u8bhX2YltR%7!nk47=oCn8OSe8NPvXk4}yYqfN z&-?PfwNqr!%-{kDKWa< zlQ0cO^L`xCYv}NH_BCnqIcUO*yTh2Tx8dj+ZHCF@rFkVpkvWg^k~;;_tDdcb|LDX` z2$WAZfc*d!kARLhf-XF4ZB(7%yc>G?;t^QjcXV-BjwixtFk4X9o&|lXA(E_aS+p|RI5xlR#!$vX%LhRBnYB(*}k1f~W_-)#h+J1W*PkeMRJVv}#Y=n717x0&2) zS}Cy#pJ_<(K+Gz0T!eR|m&7MWY2t-L?q|l+h<1)ovtA$UKWA z$KB^-v>QVBliw%f>&6Y(KH{dL1^hAv&U@5v*}`=1-v?eg-vdrG&RTKX6s;-DnB_?1 zZ{;SgvPQf2!E^}Yxs^ex`4L<}$(y`Eq=b^nKcxh#qUe(%1nS086zIe<@YPB(9;5aG z_$-~G6`OB1;nF)geSHQr+!NJ;Ppg^BiS{uQQuy*qP`RJ7`1|EKwEiM^w&!>#-OOp0 zowjny@hbe*#UW`m(gKf9f|!t=^z7mo*|f=nG{j2Ik+?6ztg1XVaQ1}Q#2!*$X^zRV zSCx`h?y110G4)g?Az6}lsXAv$IT8b_5bu3Fz0PDVWvN#BYd!=hw2Qwl`f2_wty*fO zOIyK3JNEHo-Q%EHI3Gq1c&#}FqdfE{3XP8!4ZPc} z+Lh$+mC1XOE`#@_(pzzL)ORpEX?FS8J-7k5HtLlBaesN$emG# zTa)d|PNB>T1}bg?P9P6?W~xi9(O3AIfih2gp_I|Vf&<7v7EU1z8q8=KY=I7Cs*mZ- zIaH@E4<;1g(BWA)A|9c7gOn}W{`(mym53LKnxvejt^{+y8$De1YLNP$=ebXu^y$v{ z84Z#_h}iGX)m1e3!Uc};WB@(9?nRGSS5d(s7KEFl>^>P3fiTL~!g|D5*uC#~-sB5= zw7J{FAn!CWHbPiTKVdZ}T5otG^M0J7zjR6Xs*8SB*;e?sOsE(_iPyW>7RnD^POPLU xQflZeOQ*k^mwPNkG(6 zDM4TcL5Bjz;qy@?%tWP*EyxYI+CfO@*mh{FfoW@_MCgOED`O0 zot;hY{T}D>`<>U0UQhp$!hjTTRiHsl_(rMHPHF;`S-J-!dESkULf!c2#@B=3X; z_qshebkK{V?|X5)z=ey0#m3t8pglb%8pIqC!IAfayr+!?!ve?McFj_n?Zn~TLOxIe zIa*EiHj6JrXIn*7s7jnPRfyX}LXNBlACB&HY3FM%0=7pA(;rm2l*g1F>|5(cN2Jg> zgKkuaYLO6?_=~z=Kvl|VeJUvb_#}9>k0Zh%$49`fjo@Y>UvPjEhC!DpU zrG_+Dh=?|_ywB?l882~K2+Kp)AgFD4^hd5+R^`@zLY~&RO#t>!ccoX0db$wOI<^Nh z3}i3tc4*t48FI1?sFhiFLPUOK0S8vSl8e85t^v<@9X?fw5hGE-Hq4;=QBkcqpI>7M z@?#Ax)QT8wCEsSyb4crZvEIX|moS^tC$?k65TmFnQTkgZ;>5#D{OoW5umAjBCkx<} z3R?UA>rZ6W6V|5KiB;{=>sRbB9sjLu!iNT5H=xD+$8$2u=wvP3n}&xT58!>r8*MT2 zv>JbUJaE(Ak>l%cdaE4zUh0(ph6FaAtf(Ss)!N%b>ELDGVxmn6tBJjjeo_10H z3uwQ4yVPDD5={m&NsNe)3IBM7FE&xUe0HixYdgKejia+%*j(z+iqBq655`I0OxjnK zGCHR!j8&To84MQUQkPx!dY}M%&*sTe4}`IC$5d?36tewQ z3j7~?ASbO_)X9B!fnTo91dnlZhja27FHSw;Mf+MW-q~%Qs5~AGinSt z2p)`N<>Q}+Jo4l*u*=yI$iw~*9F3}C5J36vB4ApK1PBrH9ES;m!O0NUDEz@KOA7Q8 zxo}=&-Eu`Y08w8;b7eNb1Zvb5A@;GE6fJ=@RgIB&#K$vgcY}Y%dz+;@!*0g3Z zQ~#?9zfQ~GP-FF#^2uZ1(}$-f3gXFIWQlV$o`GsCkDv1SV9jZN9qVLY;KIWa!C z3>s+5rC(hJ1-9}EGB=qR-%I@5HzxW_t)g(r4FrF?Z46wCfUfHm+PZs_mr zf+oAP?}v-})&0=ya3>?$DDd)*mtYzs^c63`e}t@g1@6~R48X@0+4dTE@!lL44zDb> zwQw@vr}^dj3$MY&bYf>B_Vj&6;0I~nj3kE!s;tsrxFW?47bLUGCiN$a7S&yhbmz=+c^?5cif5LUR*+i%AOI~}f@3bK%Ij1U0P5xP|urSi1p?;x@`t2uiU zggqDiI7VDPIWz*zsY7MA9HOI5Q=jnZ-9FMY8T`3U<#~A(yV9 z!s04YarQl~KpPpr^8|BxBid zN;{8PG4h263I5B;5~Al2?S+h|R3i;jX<*MAX+LIkF}_i4+)gI4`DS)J9!$8Bl6g2X zE0Ec1pkbP_oD7MS7BW zY+U?&uJoRVTw8(!#ANMuYP`eS3Ek=Ivt{8j@ZiM)4-RJL$^J`3Qt`uu+`Idll}^IF zT$xQBi=X%#F(#rCF>g}8t0u5iWv-|rvrKo(DJEJCaLqJfXzB?Nb9`taJD7$Tor#N- z^m+n?ETlc6YY_=e5FV;xj_Ax+vawDh(JUf43-K_{82v9p3^lUvB;@0`lP24>h4TD- z$ix)E*bvNNF`3JiZc3?MO5z@6qte3yeRr2Cs`0st{+24=F1d`fHIR*S zNF$>$!-!8q10Ec!#@>zsIcpV6-9Ku_j{^3-Dw4(Y@qdmXMvUh;6a0mg{cWOyw1ddcyVbnUr0G>3>} zfvNc6G%YuhvQ{3v21OPci}qZ~mFH$s#D3kFYs6x>n#A5i_skI^m99K%v%FM-LzGvz;6zDGC8>~SW7@C? z#N07|EE_vCm;~gK>kt~c4#OZvzk~+&BNW?R)IPXXl}fk_zV83K9YPL$1qc2M?G>4b delta 2833 zcmai04Qy0Z7Jg@bZs+ICP&+gIv6Q}cR;L|1Go?&Rr})=0ojyuwXX=_I8&+GlMHdm< zE)b2Y7A2Sz1lFs2#MN35Q3}z9SNe=$8xVr*#$-#f3GNbIFk%SHV$ooX*1G4uX=`;6 zGReGq?>j%=Ip;g)zE9Ei!9>PTzsugjqAbAXvu5oz*HNprfkjxY_K@eF7Ffbh7W=dn zxra1a@#zu zDWsZm6Rd$`ZYEf%83ze+!zccds~wMA0^YXNf|E~!taVi|huzO&6ln{q;`d(hY1?nz zW7hia9J6NJ#6ql`)$$Gg984|;H-6=UZ0s$7Y;7pP=HMTG3o=jKt!U@(esj(=QfHX_ z3WOAz_FCtYnfW0)xMd1VkYGeun5mSf#Z2L6*X3yH`bT8@j1aG_^zl#DWow^3H4pHE zad0?8Yynv-5f*vvxje0WqsdNXXy4}3)MrPWGoI@y22lV?EPs1PsZWvCNo%EtCGqT% zR@16o>fk3|FXGF0L~;LVkPBw1P$dF{u9E)a6gcg)y*nN>*~4@YA;9%o&R)Q zvsQATNDB=3Q}*y=HF#dw4-W0ceV^E@N%Gmu9~*uYRQ~1gKJ3k+2Ub!$xrQ7?eV4#8&^l}No)D|p%L}=AWN`nLMk+_syU6A-mj3>NF^j+MzMt0 zGFBwB(Rhf^0!YF;pF3RGavs**81P+2T-*|@A_?cco8_l zO{7sqdI4f!Bk@`6@0WV0Qlera#Sc^6Iw;LO((fhQUI|`&AO>D-*LzR7u&f5$87oP2 zwX_;HxFEojf1Ss-yX4g((SCv*oeC=~iJv0XQ6&|rk@ys3HT1rYb^>c~s`v{DIhD-7 zF68|bOM-SqB-cQ_&_GXNJ4Tq}24b1v_<0Cd2s?{O!l3n!^n`qZe8frDPdZhGudu|E zcVuhjVlL#aMuf!$`#M%}#nmojH{<$Qdr zO&_$uCR4R&QBjF$0?+WCkd#n1!fP z&M0dpH}mOuF`g}k06yUdg^v~3^on9w4>Vb2{eT}1n_U8Sh-4$8DA;}@6zG2nz(to! zSe|aoWw@;po}p1Qb%2Fc@b{_y!GS6Q=3NNy!kn4epKfUU{tNKorD|w(&U%BXC=}^L z#;vYE(3swhWLI>MC|$bKMM$D_qU&jmI}>mdRyROn{`I8zFGhpBE6MPT3Yj#ee%2-x zzq_vh!!2OLfd zf=jqR0XOP*E`dIlA?~mki(~g@@LVrQ56Vfn#~P#HB8I|M<4n%Zo<|=@!d#!YQgeQK zrHY;wmQJmN0&F`1GCt7>4xH$OsF1hM$m=nZuoj7dCm5|rX{OgxwZ#N8@Q5{jKMH=m z|3P@kD*B2$Mjd#li}F0)O)PKghWqH4>BqaF3`8FE$sWixR`}K{z<6@21zAP8^V=T^&sB+D%_%Pc@-v^K(bsvvj;vl>BWO^ zDdUHp)AJ4xq`B838E!lXiYtAcq`wmq{A|Am-#ZAqZb;9GSxqHaNYFNnz+KPj7L1R; z+-n7@e>Vc1GH*NLwa;8U`1nz1;A8vbyjmh#nghmMm4d|d5*jIW{NxuNe&Bg8_8q0# zvtx9jk-TGCzJy_>+2lnvOv&#!2D?r2%#@+Riw`e+`F$F9x4#do;Q!v~e>(xIoU@X2 z+dtrb^BccDP5gb}9Qm3|xp`Zb14qp!Be@KBor5=Z_FwqMZ5OkqieN6p)VGkMD-*Cg z!{CB`!faY;!C(O7KqK`=knXQWf>T1w7vE@fKA!uW z=bSq-ka_D^=FvMf^4>~va8-2yH=fSttE+N2R^upn&V)^m37BU5hD@dD#`;CmV78bR z6X3CxzU%<@!q{zqO*8jz@a5Y7do9lhtZK(RTz@TR@b8*?*;K|a-&)9!xh}3RcMjgT^y_l=hVuEqKTdgP zm~v#jo1A>0s6uuer%d_w2<7n2WnQkh?d6>xCT(22cy}3J7`09EY>3Us-Hva!X_0|p zDvSrqsz?}S*oG15sv}=YLZ#@lUHJ(+SzAF~t~)h7#isg^ITCY|JKK)Y1U@=VZ!(dj ze?`emajOnkfm^kjG33lA2TfjgsLDg$44K_WZr=aLVp%akE{gJ)yHG~EshE#GIZamY zrJV9;{8mj?u^Lbs-HBKwi_-TXoB5*a^2uC!T!p_@mj zb9z=QoWdv)YRktS^U02Lv^pcc$^E0Ap&!rDZZF>mZ1HV{D1sK38%>iXkcerM+B}+B zv@UTJNFKpBWZHNlP|Ds?FJE}m$+6i`-|G%uRO;sX(rG+W@AO6C5druXSZ@SQ#kyJO zI2Fl)6XaZ;daeqz&S4$EY7&s@GlH`aXlS*Fh}hZXyG`VmdjnAY_m>>PBtN<_lCo7dd*`I}%BvGRwk!YZZ)ob{jd$l+IMR&J+t zXuCE8!ho|-Vuz?IqXW?)a^8NjWJyarHVoh&wDLr)+}hRJ-Q8eL(cBL@Z%UeHC^Am_ zoHDl$&_fecAjK2pmd0H2O8o?l%f1Q9=N<1C$OsO3{0moB4;0^y9GDGE)s?*GqMMHv zxws-cjbpPM{CsWxLz-6+gn=x1N98)ThPDnAp{<XCsEdT!fKi`B~O~q(=wv*K0le_@aDSFih#oDfHpL2AUxsAs>y@B)e*4`S zc~NkqsjA{Hav67B@yO*pc&Na`{*NGYwxjlwBGW4?#?(>Nh}|~JZc->VxdH5>1G#Id lUq&2hbB3OBq+KU|WvSEpJ2eqgzRXDT|Gyw)US`@G{{rkoX5|0? delta 425 zcmYL_%}WAN6vgi~N*Vlu#;i=42&_f8sZ9%^4{2Al34&HrYRXJ0YDKn-cDaccyr76^ z;aZz$HvR!27(v^R+=R4{42s%yXE0p7`+kpe-Z}TXY^$8wNue+ z@h=vAGchU_?IcUmLK0NOSVoo#%4R*uh}NNL`o&mA3Zi^;z%r*lcq}dCxk1a(A6_SR A#{d8T diff --git a/iOSClient/Supporting Files/az.lproj/Localizable.strings b/iOSClient/Supporting Files/az.lproj/Localizable.strings index def050dc7fc6ef8de204f42c3d94c2f1e72e747b..d36bacc9229553836fa470d0ac5975bc46b80d2e 100644 GIT binary patch delta 2128 zcmbVMYiyHc6n;;crF`q!ueXh@boPIkrhPsAT`r1@!@#Po?w0*MI4?LmTMf7I5NV*yz7` zdDmRDtsh5|-7ppx1yfaNnU(;$cwj6 zW^(k-nM`*Nf|-{NujeGKYC}4-I5Z+O+)KyK zKoM3|6p_`+@PEkz7cLJwDck`SxMPU}PgOe0l8iCNPrX{ph->+glZ52u?+W?Qqag^U zdJCYnn~pL(+~j96u~=h8|8Hh0|5JFnh-a5zK+Tc{7nB9)pEKa6jc1{9bjCuZUxP0- za2Dcz?6Nun>(nY0)zw#U{Z(8&p_<5JLW!obFl)7jt?VCHP1tFbj$(4~LQAka-;O(O z+wr_*;w@_|Is3o}BsoIZ7yegp-gW$}A$SggIPXuRbfj6D)@F9L_)QljPYo2{7fVeX znC`>@x0C9A;^A9%=FxhC70}lOez#Oea?9;ptDa}Pr42&#$7ZlFXSNrQnfbW#oP!4A ztR(h#(bhI7rq3>b>9JIVqoiPZ)V281EFa$e%TMoZ2Xl0ik89Oh+7|*JE$oB|?pEE7 zC?d|GQM$Z>Nn#}Vw5S)nd8{!0$OPFzhZQ^#!N#aIR?mb2k05W;2&5EGIyK`O>@jfC zF&AC=879!%Z}IGkZov)Q@A2Y0ALXO5+oavSV8`7d2Y!6YM01w|&tLcMJi?r;j;+nAWB+w@RN!*`Ca>nXV@@5u#+YN!s!u_&SG^F5sjwHnsC^#n_E+7!6Xy zCuJm6l5HZL2!9LQc77NBBraQ4_-#>jeAb{fFEE}28Obygri+zOg3IH@WCnq0t1@T) zCpnq57Hc|MzezpdDqXDABidNU zXk?gDKYjPfX%yeRHlFr=5B^T!s)?Hv*c&-(7Hopw8aXVc^^xGYOWpK^!&|F88#zXR zCs_fOs^%PgK|IbdQKPh2J+*wkhR;Q=F-=cHmX->}C8e@bOmio!%OLeKYspC;ENR?# zpv048KBZKJ3adHFP+|Pww2vZPP);@9fPpU_HfY=?-qrn|o3v~in4>r=UhEFoTXZw2 zd$*uFIHs9P%f(<(+i?gzD;1>`sVz)pSD=nhn>6U`WTg?ooL-&^JbW)eaj;E3^3Zk% h@Wees{~IIwrIV6pJOpwrUZ9Z$pZT8!=|`Jw`@bBzE7kx2 delta 433 zcmYLFPbhPLcy>T01y7dACHr-hNNd`}Foa&+(C5T$RsLJ>*?OE17Ch&5!jyS@M}- zf4x{5SMslumsxVUQLPEX0>mIju667a9<#a|L4`I;DAPd+ty3Mk4x;+e8?rnNKpCfEiY$`E>%_m$2WWL-1cuxle4i1S;}H z7P2dGabp*6ZA$*|aet^f%okF^)ncc39jfHHLX**6hcff?+<)7d|$ZHG!WvP!C%8s?kB=Y%iW|oZ=jM^FMIxFlN_qtO#*`AY_P%b MS*1paI&9L)53;Lxa{vGU diff --git a/iOSClient/Supporting Files/be.lproj/Localizable.strings b/iOSClient/Supporting Files/be.lproj/Localizable.strings index 3cabf54b59482ea5ce38192a7f649490339616fb..14eefaff9a5f64b88c481ec1010cd71ccdd69312 100644 GIT binary patch delta 2157 zcmbtVYiv|S6rNMccDpaO`|69fm&&6UC{z+l3P=lL0zaTA!9R^n=`QU`yW8xRHYJ22 zDTo9s^h8EUkdSBuW1yty8YJOm#Hi606C=cs0>&Ud6Ep#1!0*g<+xpvV=HA&k&+j|u z%>BF1e&!wf;JiiLS~Z=+cfD*qQP1C3*>zuve0s+@vgyNbk%I^CT0;vL{oQ#w^#=Xt4ETOM-h zeh)eHQ=igI-RGu|_CBBhb?Rf?fkI`Y3j5G&Pl%*2-;BpF1@*D|cz~`RFM6C_0S#<(Q6V{laF|Snk=|}gdICVcnj}P$DrFQ+* zakBIE{7T*LCRf&MD^+lU))YB6BU~Kiffuf_2le*LRG%|;s9)Mc{?x6@6btZ$<2G)v zI)dG*RYgtgMnrE#?1XCJk$gi4Us!AlNnX~34B9c2p6e}3kLFP+kM6Y%?6>h@%gbl1 z0zLf{If6;(Ctw$WZXHen{xu|P!vvKXk?3|RsNUcZ}4 z*mK9F8@7;L-+zTHePIWh=Cj=ta7(--uy2KGv%Yu*l{tV8)7DOOdV)jd-H ziSye0dawhoLS67O@CEtugnMa<7=pF|YcnrDm9O9KCvScd88u-E9dU>y!&WrG^E+tn zv<`zeZ5dWkzIn7vk9J{v_N82U!!h#Zw8Jo>I{CrblAu&tVn7p4qGq4eGp?e4fyGOS z)avi(h(rI_gS^(>#<2Wz3u7>IZ$MYxrW^dsX&>)AGFzWKMmB!vvvd8$Y(DU2fY(%& z@{7f#yeAgog&S?WtJXJFikjquwf-3vn9h`2UcBe={jjl+rW1Sd zlY~s#CX8p;=*CQjVrTfqTgZ;9P4E#htu8s>0>HnC48vo|m#ZcEuO-qqu~PlV*BF|< zTJHoWCI96_e8iAfdzuqoAM-mW(V^)Q2I&U4-^%S|Fh300}*LT=8KD@D^!V-aRT17& zwjXF;ac;Yr2GFBl{)kGdC77U)u@cTQ!yu(?G3mz4vjsBY%fdOvh%H;?OqA s{=k!Umw5jTmmJKMNlQCzMByy8469tN#hF}H7M?PV|2Mw=nX+E{7pX!nk^lez delta 390 zcmdn8l;c+m#|9na$rE0&O#jEi$g}yM@dMt?dQJ(VlPBnKP1o7a$gz1*cvj_P9%%vA zQidFcoXHo3)F*Fnl$#zfiIHpiop+2J%83j`49N@`44DjN45?W24A+tp|+LxTeQA z1ATbo6{F1Lb@6=D6?hm$rgOYzyth5&Gh>UiWh&4MDNwg$0{xi*^h6>M7X!VL2=<5q i&_hK~(E^}*LEg@1NSS`0pUH818$Z)K#_2{vOichaKyF0< diff --git a/iOSClient/Supporting Files/bg_BG.lproj/Localizable.strings b/iOSClient/Supporting Files/bg_BG.lproj/Localizable.strings index 16723969754af1e5a2a987cce7d9b4fa65653a65..0497ee85ff46468ae6313c72742f4142f32a5ceb 100644 GIT binary patch delta 2765 zcmcImTWphM82;X$u|ro%>F8m!rT;czL18Ev7$6)br*MOfhkz)KuI*;CLu*#ShmC))3pEhf8Tp} z-se4hy?Lhg)^D{RA6qG3YY)k)OKy3x-KRb+(3)8>kx&P(kyjr5z%#E~#6(`CMOLK5 zph$|Gx}YdHdTQlv7d5LUgC3K|mpPr=MLz_4SU`+UY&z+u4WlQXUvfZ=A0_ALi&vZ* z8+Ss0ZKT8i1Wl~c_#F}%Raj46buUcLnqf;uZV$CCUx(9XF}+qCc#@E1L~Q9LkyK3) zYE*xR$u(PBk(XazBBM3c&C^>*K(h~DwxI5WDNy!(p}v*%{L|}Y)$%CNvY)aBU22eD zUz*Qa?bB<;fiR}yX1r55eb7IxU$$^2D`}At`z*Y5!zDgDRp%aB#Lj0>>j%+!J4|&d zOnuzZ z(L=OU9)71*y&E=MEx8iKagb~Rtclu4!YyOTiZ=eJb6z7@99u+zw`T!`8d*h6^1Hmx zGnIhFX7T&;bT3%nt#S9WLAl|)SKS(?XiaIexb6y_m@kiiQY|N%GTvd3p3+I~6jlPV zqDNi`M3}49JIkkL<+N0s&0?)^p!hn0I)RT>wrM0VCi>Jt6CkgDSS?pI`&^t6u4z8Z z3F&@nzBdPZT%Jj6u?42LS035sWxwjIL5`-$B20}Ifdc13gI1SguBv$gHUYpQCIJU&l|>XSlW};F*<8I7w?a{dnibG&LqB*cp%9@xD*qKIv9Z{7fGC>Nif6k5Qn? z6pKx@_gC`CT_Lx8#vPCwKdP3yH+c%&!wfVHiYnYBuu~~DrqZ5Gm43o}f>bv&`#?UI z{~GZE0$wCGq78V(->5ED_QBFNr+vC%z;ywo!3QY_5^t!VM4jvidFNO(cLHxpWYvDR(IOo# zk5|7U5FS6){l&KzU8Tl3oMKb-;xLKpC-OVB)Wx)1O628+kpGd2$u6{T2E=n|ruCZR z?kQy_Cz|E`Ig!SS1g8Lr)ZU0$yFkm#)u_9+@nL2%l{8dThS`PdZP=k4LtF1qveb6?AE!q^j9flhVB!lH3 zc$b6ZE0_bedfpls{Kkp12J7VD<0VZFwnwj1fZh91qkJIjFqlr(QX*#+dl`? zrg#$0cC7?H)p9A65L!o8W~mvOcp10t616Muqv=~qRxsNJtu2MBJ*-cA-ihB z-J)+8^>UsX)QV$BJ=o&W=&0cv1%$}n&w_2ZZgiUtV}~=qUaWCwjcUeYqB+t!tHt#b zbT+i7U-vL`Y!`^dDGzVU0CzY8wdO95^h#rMq4a1uy9C)GhpK0S`Zj2E6!SsjHmUKT i(O#(7>O@qgu>Q8~fPJ3h42~zt|AYqrAf5{wZ~O};LVlhA delta 415 zcmaELo8#GTjt#R+r)vl^3T)0Xi{jfX=DI+9@*h2}&4*&-8Ycgd=3`G|C}qfD$eHe# z!e}}9LoCna1X-@>8@w3>L=9LRSX@~wSd3T{81%qUfk9@vBO{Y0zY$Q%l*I;!^%xWw zQJB+`#R3S`lL8UA#O(&FBVUr zG6e>U$vds2rzc1=im;mk722`rO*Z^0zWL1^9*5}{4l{~OPqJYYne4#DHGP5tBj5CA z@{DgL_X+R_>HuwZXE9*00%8RQM;6oRj4F)Mlg}vgZI@DHyv8@;{+tYZNcF0(#GUPF&FcbrgDQC!JC}GF|vJ!!~n4thjCo`l1+4&4b mP|*S)zX+%_vYh! z%{IDg1?%v^aelcglE397AIJ=9)1EUiN2B2f;zx45*>Vx=oI0pSvp)Ch@};y8mll?WkbeU>YXV|_T}PCw7sg5EnbaI(94r7` zs)PR61&@}*bzneC#B$-Srz&vIY4-9?X8hMtuop#mVFx#Rv&* zHZW4Z`~*kMv~?S=p11>LG;oLOJ$(m$$Gb QdG&8pfdvKiy=)!Ef`6dsUpt$CzJ` zCJUg3J~|6#iXVV{Y;;>O|3*0;{IHM)nz>b%8ZA$X6B1lOjEUsx4Dv|MG2#jlQEJw% zz^A8NFRoUvF!4pIk^L%f^0a7&38HvO)+89YM{U>twMsl5=Fvn(`gC@Q5*eib355F6 z^*PM7yuOELW>m(jB~tLE8@>F*`CVk};74MmQ|x#Arn175&iHRW!8C95S=`tZU5FD~ zOf=ZcI{kN9gn(>>NJf^Wuay?hkcH+3!8s?rkJ@yL4Q##cp`jIEE6~H_OF}8XvQE-1 z50;Isq~vOFxN@u7dFRC0UooyI#_LP@*gd)eT)uROH#eCPMYeZXB+o&{uR^I zg)mqS9RPil=DQ50pN6UB#nMap?IYjgP(l0O20PT^;ugWT1#lM4=VWdYn^n_?G<3B@rUeQwxsV_?GnM>!JNb^ za_z`|^XrUZY`wM!w+uOU#~7+pkA~Gw2A#g5_$|b%dNezOT_I_6-j&D@W6MdG;O=|X z^Nbvrfl!n2R6C;wR5wS6E9EeVa+KCa#p;-r!BbhwW&0pYvWKG>RLWrM;@-paXD9d; zl~i>&GUcZ7e!fb$W}MhyrawPn6^_5j7u(GdYk_z*itk)2HObu~DyN3V;gI{)h_<=N z5#_2ysnYiWAW!CcEXNah*CY+i)pOs?ghp=0e`uFTcwB4QL+ZJ+V6n h)y8MPSFPcvS~WAM99~}jmOb}Bk>T6#a9-Y9{{V9BJR|@B delta 410 zcmaE~jAKO`$A%)~>A%_-1va~wJmB42;uIq~xgmsW^PTXj%IQAG7`dkNy=7!$EoI1I z$eHfg!Du>pf{eiQ1-3wmy7!D6%83j`49N@`44DjN45~j^eA^ER zGV(FPgtosCV%)_xU4n^8Wcws3#)e7TGuAR%NwFt^-Rw4fVFIJ@^h3uP1DJs#(;H7P z3UAjr!I&qq{lo*tSzOcKIRm|3^_o#;@`rf7$qwmU(?3Wua!geeaGIPwI4q)7iSIIaQhq|rn4R#Ug5^$b8PibtEB2yhHB&Agqo%{@w!Vz)n?VM z4BXZ4?OzP`(kP#|;aB0wRR&um*``PPrP-z`J9(&gZrV+&Ujh#<`P0Ci4K6(1Q&JXH zogBp+`Xh48OIJPu7k&O2*oQuN>XGL$9`+4&T`E~hD~6#G8|Hb)@WWJGUss1mgLaAx zgD+rZYE@~D66L&%i2ruUFAXk)TAJYpA6|7d;J_I>Jvt2TD$AvfP2&7z7Yh0z3~9Qx z0>XEiN!$cS-k3APk?eRV+W!kwPwLQABpE_%EE=sj4EJc<6?ue7l~O%A6*XUi@gPpi zU?Y>Y8TYgYGDrQ;08(z|$6BZ?qtp*jLfiI$3mbob0GmH_fr*2@8rpdV{50?zRO2<% zL9!!nfYq;;p#s!2X{{Z|NN93fao#amRAj?IP;8fSePY*C|;SvL&J zrZ_v;E6G)J6l%EuURpB()ig2!i_3CHnX0ehzT({d!1E;|rDm0O*wFx^IywZtraF@4C+9L@MF0VmXr38wX-B^*OztLMPw?B*GWb2z>$=%MpGolO)!8TI%b}k#4G~xbkAKcHF{Z zOzx1FwBz3s;O#Y2ikUr{`6dMM)%)#%CpFOtcH${lGng{zxcv5za}m@|?$!)j_w-hV z-lk??#p-%``!aay3!VcxV6T`SH2sEA>;(vLjrt#Kx#o3>fzUUTugAwOgSWVyZ`MP* zS<&Br43+faS@2PBDcA$@Mv7Q+^UQ?{3@I^zd%l^zL*vuCqGsOZGlAHt*D)nP3trL* zx;8b=@fi@y4#q9QF>#AITV!c4{AiCUH9uP7 zGIksAxs=3;$IQFIa+A4{R86?|uR#BPdEUY{O1%e;a(VY;3Zry3EAR0M-m@l|17qK7 zURzR+L*|*E;Xt5#qn;9`h(7%U>gP#PsYVzR-tt4To_9Mn?0m;9-VEMPWbT_(G`onF fOe&ho$Wpr4cDn8^s<|Wlga0c$UtW(_747{8`$$rs delta 385 zcmbPsgyYd-jt#R+CJTIG*_>dS!naw(WrgS@J+93sBK>M7cS#GdmNMipj9 zee#bak?jF0j0w}Xv+ZHDl44H+JJW4C<6B1I=?T{u1DJs#(;a6p3U5Dkjj=>zd(L~t zWn9x0ele;|-WSR@NtS2&24zOJ=@GveA8g;o#56_PG8O2V6sV&zfj-UvIxi83i-Dd> m1bayV=(QrKXaUgSpa95cNSW>^&*ZrMg&fm6#_3ZOnVJBs;A&C; diff --git a/iOSClient/Supporting Files/bs.lproj/Localizable.strings b/iOSClient/Supporting Files/bs.lproj/Localizable.strings index d7651b1cc8e1f3ac9313567a83710b8d6b4911ae..0ba14caf3914c891c53b541c21baf3a97f37a030 100644 GIT binary patch delta 2202 zcmbtVdu&r>6#vdPyV7oVmu_9xb=@v683KdBjAjCEJ`zj}W12urh)~*Xlyz&@t_%>V z8pOoJ2OQ!tz8J=6FeWZMRzXa*NCGCHfEb1bGUCENL?Vih7)?CqyPZY<(Zn?O_PgJC z|IY7x-_@PY!S|d)dluumMK#5pDx@~4IL3Ru_`SObUtQEv7*Y{z<8PzKS-eHXRazx6 zGwQ`{*Nj(oK*_;(7JpqzW1GQ4>sEt_=878kL#l@zb*T=OIylgp_QJgfudVuQ3vKIW zee>L=rPws+!Bc;@C}Ds>E(2V6=!}W$0u{9EFgR$L0p`?x4rZyaO0u7j>Q^1u{*w>S z47%v|QmDqwt4eU?vz3%6gGxNN$b+@dxyTrv^!JQ=rWO_3Zjuyg1@A5L<`G+tk$O*| zr8+S<;?Il8B)CJTQONdq;~uXBt-^+&ZRNJT_Hc@u2!sB zvc`UE8tw6L5krFxTzJfZH!Tz5xNm`vO0I*;o60q@gSFSP*8zUI@k%s63on6-E4F(q zVyRe*xmt@?xu7)dJd_tFbMbcF@6%Mm9l?6Cdb#5Vc5-D8WWc1z38=xheHFIC%w`WQ zKMj>yt5k^8$NXhls(zY(66)xK(@Zo=Mf9%4kPGEP*mrmN?xBj=VIe#JBs1v-n{j^3 zL|(^LC%^kS!wzcAz?`y#Rt&SCm;FVt?}T3fq466~LJd!Vi$>lBPjL@hcc^~MejM~l zmviduDXP~gYX1tnQ~#DL8zi%h7h#_ZV;>oG?pwGWZyv3nhBR}|nt(AZI>7VvW+guS z7EjR^4%~j(iC0HVd?Zsv2?ryzAp!DJp@5?o~Iu?U3VoM`Kr!2L?rGXv z0p2O%N`Re@3w(ZPz^Om_@%*TX6JJ&ee`#opvk=k@a1Jttax>XSrtC)XlZ&(F>U^|B zXdKcc6Rf-RI;ysEfLv4@eXu+u`gyvGtdqS<+9_s;rP|m-IB%n7>l7^}sx+vZHH649 zjY@}&hK`ty*26y+TnAbM1x<5gxNKKz)pS-`%FlGQg7@THv-m9o_`joGC@6U8 EPk&=M82|tP delta 394 zcmZ3moa0#=$A&4!(|52k3T#d@xy8GAf>WO8) zviyl)<>f$yKvyy7O-^(apUk7owfWw~99z~jpczGz8~;jfml9%>VMOq!XGt;gZBGzp zT*J2gi4M_x0^OunGcH+@1gqsa6xuNm)c5BS2^B5j@u^h*lVQJKIn$N>5VR02OAwI6AmN7rXHsK~VlX72StRg71HvGSm>37fMB@*@=e+IK4J9!#OV_*m zo{#4|=Q-!yAD_7fueeVA+^E(y&Yjs|gpHj>%19e=HEXS@#+Es)E%>|7u#G6blg50b z7th2A znq-HRIS3I{5HrFmHDL1Q7MLIKQ!*+!n^`BvzA33ELZ>WX+^kX)2e$7Waa=rC2P)iZlypNQ^n7{KLCTH@n(gZwZ#9ai@8d>T2#o6nMmlgr6gcb1oUA1tNb&YnC= z+vh+}X<+O|w6F^Hn!NBDZJ3dNneD$u2P@RQ;7)TF7>n!l?EoO+N*kSgagYMi5Np%K zv0XUME91{#P=!Jj>cF8AW&54{)?IRz<%A=04I2qQ93m^wg*swjwjFil)DgpRI}CQ= z)1`t*>)i`f5}bSr)VWi5+IZ3+Rr6~mr8#hf{A$=!%J1zZm-@EGR-Q-QK~dcfObetf zg2!OkC`JB~8A=-w-f@!xd}I*BU0&f;mEKD6E1TlS3vQ5~U%Ex!TA4bDHm+;wO9Zi! z7_9^tK(~0k&tdu^I=qBQ>IO>sFbcEB?MqDAN{jICgBp0OH&mnZpb(8`n? z;}uCe6M8gIX*!jAVyO4WfL>B7rhE zlYR;^siqIlf?oW_`IT;@jBR)q(_8s>>$L*XBAsJ~!Kl#E% zW*c2h7s$tnU&uo?AN~^0pI@a?1f3N{b?sncJG|hD@a4EdF1C?bTL?aJjN*ktSt4=0 z8y}iLJ+a9%4V3cz<5)92Yg||};+o7OUWibl1Nq*~)Pfc3_>jD`P~ARjsmGcs{L&ZU zJvZoL7spuU?>{~|Kvwa@WyrHHQF*c02$_pG_%>BjgIamtr#}A1%jZTR?8VnzqUqq% zT3I5SU5F^1mgON+6$UzFQNNDPf;RJ97 zg0PMC2o@>$;AYQBZyVx8j4oJ+c4f;XVWnx%K;&W{7v^|##Y4Iw9wI;hFM?}ASsw(7 zGx(1{QZ?|RwPf+yBe>L1Cu7$X@#4S8Su9tcOkbE6eFvVaTF6r)p#?%me$@#G8xlJ-EuT%ot9&aIAJ-K d@xCm1s#oGTWUNCd*@BWx;5AF=HH}cO}PL7 delta 436 zcmX9)J4ixd6h7ysAtpCPlgzYhg^4|Asb~?4ipH9S;QGjno0^Xv5Jc@wfAR}1)>cCp z4i}=qskTO!B&wwc(b&*n9k0U==ljn2zVn^G_-OdKF;w2Cs+ZGgvAVhFw=?&Nv04t< zj@VbfS)T_56#iGn85WF+JZMET3y7ve1x7%I4D3My^5B&uSb-GeK%$o+F0OSY*dkUT zZh|@sG?^hjsm;l-O*jTw4W+4*f(U#N9@AW8(uN7QJldKBDdUSuc@Vv7-Ggq#bv}27R&E?X6T7mQRQC`zJNa^t zU{ejF_*_y0_qf*A9fo~cy+r0pDu!B7X0s`V8MHnskX6(c{@3ts;HhMj!!07YgV z*6~^ijr>6`JgI(BICsv*D0GyLl3DA zyZJrtJ&*64^PO||@h7tX@nZJwZN=vKH4@M#Cwldkk`i6JEz=y#mw-9`r1*wT7WaJ#JZn`SeZk4sENrGEn}y%99q=tDQ`%2Tn(sUl~7R= ze@+Ta*LEx~nx5g&4_{iMKYD4r*&h(UK6q(WPM2CLZK_$t&Gs2mnjghwC&ctX&?jnz zTBUcKs?f(zW(-9yJ2J*xvs3&98?b@qB(bg)|9e%tN}5w+W#Z7R88b5UruRbHGg4-L zx>&sCTf3opaMtpSF$p!IKf7jr)-e3w^@95AOjoI7=$&iB8BrCCnugg@txwc1kxKK- zQn|_mz6}8riXdS2wReZjtbG9F;F9VEdhnYW7tvK~AuJeBBXF8l_rmo)SXP5O>zdy8 z$Yj0qwUCZJnvvTH0Gm~}+8`tPz;B9!w7wfVhq3QU?C4hAdi&GU+AhO&o{T_VhiU@~ z1eW)dPPk*ZZ@^yo4SBR=*y`u|c08wL4x6;}C|qvA^M19;zE9e4)#&-v1&w?Xg%8bE z&j2J9O9FZs)GFBEJY9_^YpitQ{&@?}w~yYpV}A=g`b2leA|Yy~1&AnY>{V0YgWj|) zI&}Icr^lLO56gs&T#_U-)*XV`VegypO)$HynQ@4Tl0%{f1TKN z2&MR5n+Fcj8wKx8osq&D%A(8g^6Vli9I$?^!4-q)6nEjO8Mc;08NN7#FH`jh{4MHM zwMfk$>Ks~;p<~aEpWB3&9k4Y|+UW`$r;83s0`@kZ#qpHc)Q+na)-SF~XV7Zvanii8 zUm{tf8%)J>Qf%flNXQ&LBL1N*zt}H&u-B(=S?MiLLR^cwQJgX+wF8XM^{Nr`*b$Ut z;+NA4bLnlu`js?W_epT(g_bzyXb(|qhIMq=5lScSTcB(y@;;%jdjE@n&n-T^_wGu) z+nZ@uXZ4_jz>Q{KNXo55={ASvO0nLZ<^Pfuq)Y9^CoQCcIp%7VX-wD|`zD~oz@eJ; zzLtoUK6V9Ocj^2Ch#+0*K@KIXtm7fcG_6ad z%5?n(M%X`$kwdd=%A#?`D_{_e!?(n~I|nF_Sh)%q%|&5O&?_ zHN8Fwr52aT5AuUIqhL2cLlhcVq~J`m=Q;_RU6%mqw_@T;Ro2SO7g>JJi6g7YQ0->H z7Af`=%R>z^%QW3A<5SC*NUO)LGI!l3KA3(^G<0KDacw&M6yV$_eyP5);hcfshZ6tGR>Tv;}Xd4nG#J!_sY?rKHlXwFMTM1NFJo6ja>~BX{zOG z9nXpY(L*~SVD%p)YHFX5=}XzVT(P8y+$9J3!kOM;ONfQy2&&im+Y5K1c+!dw$(qTO zZmo2qEmiPG$(QoQK!&Y{L^v8OOETowV(B&YFUwr>)_n0xn_hcGPP5LViwwz1Ntv+oUj$0cckrS_9YU_#+XMeL!mTy!HNkg0RqnOsKmtd344~$6Cqe zl`T>jJUei7S%iAkaBAxTX&Ez>ksuE`I&HIVw?agx4LE^mg*hFF(K828h}R#Is^YKM zle+(q+~F(rfx-JAs~Z{MXqp;E02(;e$T9hgsrdk5_2qcHmy>`T%@AkuAF)VxB*(C= zz&xQ=jtWCQnmv0Y7vt(8F~}E{lBqgs4k{%hd6%b zpl$%8^^NgLjur}mW+BiO)EfPY6uRc?7PE%PK?yaif3^MM7RICxR+8)8wX1*;tYJrB zsxscIfwxQZZYawGu<83G@~9K?I3Mxp=X*6P5@r-uu>GyF_0HygVe6^Un{bu=Mw`HWRX9;fH4N2iUrer0 z-G~j-r4cL0&+U3gZh<}+_nKYrh(Bm2csMgUeE`L{sb4My7k45ZUEknIhoEqf;sDPP zhKH9CNT+0Ej;ZgLa(&O@kQsaj$BmOK{Bsw%X2aAPQ7?S9`l-~*NfjQ zKP6Ml_fN^c##E|Cy>;7U{p-PiDcgm4$A1`y;E(Q?O1qfFOxMLK#> zwt4=qh-Kz$;apdjYy$oI(Y2**S6XJAtgZnCks}v_zBx|Cc>sVXHenIJ4h(B-f3Et; zGaEo>!`4^wN;7uxEX3npnr|25j^hw98aAiym%{m@lRLJ#lQRb(r_S-<=M^$dNnmVH zE>KiKIj+-Oj^Lh2Wt`dE)B3loxEh={fK!$OJkRD1LhxO^LIfc3Bc&#_*m5xPh zRktbjNvX*`EaA)3bJ?&pjs)#ac=Vd{M^sITvVXYaF~=RCWe)ur^s?bdz#Q9y>3d7w z_=(Lxt`T;SlSyg3X4S20_{R_d@Rk5umU}}10l3D(-Wa>}H z1vZk{7)&>2k=%*63uT%OiC^0&VS^(;4ySD&V!50fSa$LbJR3a*AlQiu`qWwJcFHe7 zPLTeHK6&MB{AbnKtTWrcBU2@&H~urCLk|Y@;5Wj^%}klAfBfJ?-EeHYZm14joy1~B ze5vp_$!6kOTUy*X2@K(``?@2I4VFGNC#av?5vou3@3ilB{KF2GG7Tg)u4(ZI&QxbF z=6RD{8)r`Q{u4OrJoR{yZvJmsPE1v*naZmwb^e1v6MH}=(RO{j+B=n#0aKi0fD&r>dsQ{VkSPKV5*T#4qKhJOBV)vq^~L{fXRJfC`WL%HAl zeZNc?yGYGb3(ZWgXWA84+Y)21N*}~2i?Jr*&?|Rdac#FA_VF~0i2~hpBj*Vwnw#dB zyUcDvo`g6$5W`)anvN^BFRte_%-)}_i}dM{@#RjYjXS2#$32Z%h`)JwOEzR3j*Obx z9>!}D$;Gq?g+SE$Rm^Pkc_yZ^eV&~j-5M@wB)8BSLP*!xzk0BkmBf@_V$xZz!L)5x zF&Rh{c}N}mGu!B1JKsPG-@CDEh2HmOu^AlasWP!~o+;U7Q5H=}RBt*wA&@ADNY<;<<734qX9Z^V6IYiQ(0+Q!#uQK-qJhkS;T~cCB_&ojyHMt13 z+h5euX2kSxX`wYQkHz2;s}Z?(M|g7ljA_=)|2s0O&5XD%e5Q&-n*FsBYp>-^kzomxMpY0@v+G+C0atKjlW z5nLV0R8z745qT%}cJ?zdcVwsS+%@|a5Cl~z*_&qHKPi@J@{qKRY0BPV4430x26l;H&U%~2Z!@?XS0i|jsY(CE-b8V* z?~yh?Gf-drb>Px%gg#0jMrl6m^6NaJ9lO1wX)>0Z8)D+uH2yIPS zJW{w~f)e9v2KsIlY&)d^+ZWqGf&uN>CNr=!GQIZ3$!uJ^<_15#373jzswF2hG zASdR6q!OBLNZbs4>M+?JWSO8z{H1df&>FX4&;coeN^6ABY0_~*o`58;MhfNlc@VK- ztuiBfATnf2&biKtpoHM&3iR}@(=;(BVxzbg^Um=>oOTXVp0aW0@ipleo?BL2^x|*x z3NDm5yvRUz%I2RHPRLjbS=%1;^JE_x9CI`SNr5D7A*5~snyjW#3+;dfm9@vN$>P3k zzc7?NKlM<1!Ig(|o2I4V*lGkNfQIm8a6lIKPtM!4#hg$x+c|t2gcT`3DbVUtw^QIPjg_lsJA}!G3}Cd3fYb$h zeG=%=9j0unJ>0Pkf2&Rt%YiLKoBhy2CJN&m(hAm`-1>^{?w|^ndD@?;l;*V7mX(jq|!ZPi^w+Oy?G&c01l# z^(9(40n1WdE=-2ZoUl`KmcUp7rLwhokyJvooq2N-r$Zx6C(f3qmcWqn-NU=5zXBgupPd=u;D0 zs@u)RRfz2(UteVIU#HtjAFS2~E{?{m9HF5LS$wiWp=qwzu3LNP2K#9S-UfJ5kWtl` zTIQpzI#N1uw;m4pUtAjALHZKGB*fWAOb2Iy>nJk%wDm%mTR1)euu||y*oH3HMFP42 z%{1SqqosFu>aDBpkh&HuWp>FPz&w*L1qSCY^Hp8t997~JCFdq<>E2m=>m{W>59{L* z2C`lP5gC~SLSM2mq;LwM9=4<)0n;f9_upLCZm#-7UqkUFrxh>F zexmPfXzf?K;7i;VEw`jBd`X&~4m9D?uk_m{bC(Wdwz51(3M`Z$cS0C4kXdM4aRoWj zg{uJ>>5j~56CbOm5(*(3<`DC@0JAdcxKK%@x{-V)^MoC>1@N6I=@$43{AG=FG z@d{)sNWXv-6IxEbd+^D3tp^eAQ3SGjJ&}dTrHut07^$5MVWnWS6{sS!Ef}&wCY7`9 zRI`&k#UWokJ!Si-%sH8M(iS64OTtOtDH+_Z6`935Yz`~ol1Tm&$~h#7QxXO_SC(BEy%A|K!ZnB#~pAXrwa9H+C^M*gDBt-S)`;7 zdelnX;W|#ZNKiTJ6qCVbB-}ysRiZ=*y2BYC^+=E`Gj6GLgYnL-Y!$UuxDAmo4hp%! z!J4jBOfChdVgvFgN1W_Ou4MoH);VSk4z*{DZwMV^yN=!fBr_#TY7{B{V?WA@NwjP<5RAR2iAxo#+nl8&kWhv!KYdb>kh59x_pPP`7bkbrN7$y=qxJ7%Ek?!DK znIPPklBW5BP|Un>eyFRRtNSu_U3puV5eaFt?SfEb0m(-86(~FyrEK>prI%E0L40Z^ z=1fz0MWe|UFSK%-bDu@Zb681l-nctMrR@e5^4H zm5fDkzz*RAH98F;keDzS%S01C0s%uNVTK<-!Z3jTkRLb`zY+z{dAri#Z*y~d?|siX z&w1YGocHupkLAxpmccjYqwRzlN9zi5+q7nFvli9jS`eG+UPGhKg1?6fr+4w%CM}Ll z_bO>T8>~aI`6e4w5UhrBjP(@fzNm#cv|S5miJ=V*!F*UsV@tp}^waCdyXbN+Sn-Dn zedxhh>{+ZUE2d&Q6ykF4OtKAwnbz6CK>s2aF0RyR%~sISIH0F|_HVP{x}E|Y&{J;H zI<+XmP7jR_gN^3eL5Da^EP={L9y#ox`FIBbY}n<@_+inRnU9=kdpWqE4gWbk1qlx> znya|vr65`vD5iDM3_HX3TQkwQDSMdhn`vwqydxjk!3MH3Tw7rqxoW_dW?X*KLA~do zh-wZ)@%}t!n!r^R9_;U}!YlO_9JpkwOt4@fzFW0U=G@G;ti-tn@>?wzjosq}``qwy zKkYceLMrCQc~W904;SXjiRi!-0Fz~Ej>K8>CO7rk^I7EhnFQ0-1|j(l<+ zA})O2w>f`{)}aMe_Vt{+l~jyt%hHyZW)1V^y`d_}bNjJt*J4K(2ZFV_5~KQ+}Xn^tCa_VHVql8*(6vn|4c zw22sa_6;Zgz068|AA%XLd}AhS2rQT^@lmWBZ0K8^MQ`_jVVA@SR)X7=X$0uuMJU08 zZWmqI4%S?$X@n6&+~5z5lE-58{og_SjE)(22MIaM7!~!I+-l z4fqmwrFfE(f?)2_qkIil-$ia4SEfneX0QWxXgI1-jOXS5;XA&ILjZ`KUG4>uZpf% ztlA3B?q{^MEa5Dzp1V`t?_L=@NugAB%<^YCXvI#H6m`E8QMd+m$wu?-+lH zPf%-7h?xux{2gMCO&#nMVz*{4k%UOwN}&We!8U0+KmYp0ncs!K1g1%d;?YTXIW0m; zjSGH8*;9_5U$#cwi}~CqBc)`_<4(I#Z%h$u(c@wXg$Pp#XT+M^bDK&Z(59$Hds`tt zFQw{w&K2bD-g~nIw{mHAn zLIrO0x+Od(vkqfUdM*o!1OeZDvWOz5p^!p-JYdI1Equg9yT~NvDpo;?D$g#etpN{x z+6NA(!iJS4gUBk=s*`WOat!iOVt3}TiJ#I-B42u9@T^dtX^%J^djZ_pBGx>t zb$g&1?@xH}#94zbT;ksBb=k_FR|pZt*uuo5Xo8)ZjAgQY9<^u3iRS(Q0-$l@Admj(AePIWE z{Q(%Xq6Umo!IQ*oe5F(SxWS?jc@GSX3#Y4^W< CJuz1R delta 425 zcmeydjALIL$A$#s%}ORhe4AUGl0+v@@aNk6DSTJu^jqnSeC%lqr3^U?Inxh*XOy12 z;F!wv1-6U=lY4kMG7=ez7?K$>7%~~k7*ZJ&7}OLL7dO|Z$gpZqvZTgwlj5oF`ePQg9 zwoCA3G>8%Aq11=yZg%)x3~U z5Cc@?Xx3u;UBmI^R=lY-Goz4T^XU-t>tKw8VtG%lFKS0}xvjHOahxMXg?pJ%`8O~U zPFXQ@sU|+6{?)HGQ!ekJUs6mMkWH?z#s1ymUN?vs07YL+h7sKtH z)c+CW{BIO%Gl4`a>MP6L-wa#-7&A5V#gf$ip?K~Wb^iCIUwNZmB&400ioJ4`3Gf!i@kA8HGO zH&^X(1IQRpP`3?AY4e}#-j>sl6X0VEsDX$EDkNowjV|(Rt}dHoH&{t2nEGVM2T^#D&SQ5(imsvqsy{1PC#*nBHU) z8`^W6rGftqAyleL3~r1duQ&6$bh```VUdxwTG>pDD}`FmeDQ`H8Qu0$m~@AM<#ldc z^uB$($udg9Dk`sq04?w0TjVyTR5#f3#K{6@Wed|l6xU4`3LL|hm3>D0)_NSk| z4toOb4n8E^DZLwE%FS$=fj02^Wl9~BnvUgPbnEYi`fhwV~iQik4YAOQnIknEk{aO zSXfET;_dvDBxIv3u&|I)Q&P%;BSYCXaVjEcdxvO*R7Fv4F15)g(sEq0?Vt3hH;S9QLYZa1k8ZOxJFf>pV?VH$9bR3uzp4%?(tg_=hsur zyP56czEQ^68>tjyewx_`D{G8mP{)i^(wLXJ0nn&J$9(d4$xbXH=N1ZBi>$Y6uLxj4imzEY}0Qqai&yXe!?9K`FhbpZLe4*zGKRU z3ri^YijVsK2X0k%$XTeo!kj~9Pi71R_$22RL=N5rc{lsRdH_)lW Gu=E2}y^0S2 diff --git a/iOSClient/Supporting Files/de.lproj/Localizable.strings b/iOSClient/Supporting Files/de.lproj/Localizable.strings index ece2e399b5f306bf6a2f09ae321771b2f2bafbbf..489d84a7c12a17fb4ccf64204d5a6e6d64847d62 100644 GIT binary patch delta 2446 zcmb_dZA?>V6n@VIODPpfKlv(LSV2Vw=%PP#L+bR0$|8}mEo@GU6sTa^&;mv?mBl!9 zPK6h9oFkhIV>DS93>QphjPuW&af->7IJYcg>ay&Eu#I417PE8iwOE$z-_kVgeeXHX zIp;agIq&=TjDv3^4~;GDS!j^kmpGFeg->*dklfwjkf)be6Cxsj<9%Sm^B;kV-wuE| zZHH(Vs#qhMMVs)8pmdM9lAA?91O>g3<8NgSj4e%r0>mIZBL|-{%AU2>kz1<^TylD! zWyH9#cdxAUI7c4-u;*Dk;Q^DhyK`_d8H{+v0~TA5zbWld%KS_l9`Zm&$|GJVU;u}UArG%p<`6`05!fge}3AnMN3S!a6vS~HnkRji_3MEL|$CaCIDzro7vf48g^8${J8S$*Ne z+b>gIxAQw3oy1CjIPr^38bdw;Z9=a>$-~Xd!ITsrwpCGui#`Y2u}UzIlSNMD+hyr? zlbpP7NUI|yM6?`sh>!@&^jj`jUSo1Z2#)2)joL)-sW8!kU0*>qNzR_EYCR-$*|pz> zi;bWr`LqO7+28NNp(9|IeRXyj_M2t(8Z)js3puD91v^f4Ll&xoV8O$0Lk1}|H915v z8mWi?dRKu%4&_>~yc!I+?;S{8p^6qtEJz=wDJI<}O0J#G8sg|=YS(GD8W{OSNxkVM zifsYqP7mz_(5jKe5~`sS=AP@<1x5WpI12fgdmQG=u?Z7pXHE-iJZ#Z`NyON-i~2N) zq=33>VBOLv{?b6Bahc4(i5JMNjlyscR&SJNnDTDYh@H`~@X}eMX3H~3Q%7h)>MW~1 zI(jov8BURx(i3qr%ky=MQiRXvfEBM;lxcZ#XCiK`g;e?ODW|MBYr%(~QKK*BQiC5D z`i?1W*2{w`@aDH$>B+Pc^;3@nXd#FI5!<+b(jr&n6pI*h$ninsR*9Cf+%* zq;*m2buwCz-gBF{)r`j+aK*FR{y3Z1W?%_4YdOcH;nh}FK*RLQfe|-OnITsW-ZcG# zjQeL`9u5UaujLzzIE7$;T35D8@mcrlZknoN6ll+4B}-l~Tkyg?F#ZkRW^HT$ delta 454 zcmYk1&nrYx6vxlK#<<2a4bxyS(|9$6yktRA3?nN^ilt_l@!KLSann1(wTfH*NaM2L#~@xPuf7$dF!$Jq}PxWu?0m$P_IdYJMiiJih7 zgE=v<$AocyyAU+HOWCHt`b;j+;pd9^SjA4a&94hKB}% z%O0HhU;-Z-x!oP1UXzqKN|z>Ql%`JaC|))3f}%018>NE(1p2o_f}mtQ4R5T>g_RAw q%xn&Dg=1cjx1{xuiBja*u~T|4g}h8CXy2e4W`aClXrMwJ*x(y`&40H5 diff --git a/iOSClient/Supporting Files/el.lproj/Localizable.strings b/iOSClient/Supporting Files/el.lproj/Localizable.strings index 32fdd6b7538a30a725bd4a010a122d0df81ad54c..b0b7e8b0023475d024c2e7ff5303b8c719d995fa 100644 GIT binary patch delta 3272 zcmcInTWnNS6g}q(!_0JOYoXHuZMhBwYOUo}sgDo^L?sHXuYg1;L#NYDXglMbLZK#B z6JIfckz;bgkJ^}MG_ev&G9-R!{NTeFAd>K5jQ&VKULqgFFV{L}Ztqa2Bqrvjnfut! zwfA28^!&x}f&G!g_qVH`&xF;1`{t_ePgbt#5^0eU{h~*7iG*;)8vG^13nD9a<1U5Y zL9tW};l58K#U9AH`nRL9dbW#~S-eg{D5dU9S10WAKBMa4OWG)8g$D;iP|ORx{4P0v z;)!;*Lc53L3_bF(EY~Mqk(KJin{_oC#Q-#VjbuVRB%-*MiDuog%Bh)nBc=HAB4k)#l$P2p=9vA7xFgX z-6ChKFcIxG%Ub=z-7>1yHPorjb>S5jkXx&EBexzHWp*^z$6x(Nhn%Ndnq`cJVruKV zG36eNt1IEqG!dkr#tP6(Nd#BR8vjL!o(JK3;6^3XRE?jSOU1V7f@UR&0E3=dq`<#k z`K@ejn9fEPsO>9_)}3vlokqbg7|l26J@?4#%QpZ#57qKS3*s2QZEZeu;)U;8a+jm(wc|6Qp6Lb;Ee0vQ?vQLfcqpuYUFnoJ=8-1B?FPK;Ru@7&aLV@a zM60<9%iH9UG5ywQbm9TGN_`Tlt(uzn61Q;Xv^-Hqt23ICNZu{BVFs26B$F8^`tXNJ zpT8;}H7fI`S1IKFk{`v@^~9d)Vf2tcj@@SDJkhJin&sj(TP8obNQO1Ur(!SLaYri{kC99jAcb^O$yy8g+Leksf^O!du(gY%$tob#^IP zU(LRH_Q|3gp$y~*u8}iv1c`7kJba_CG`_Lw{sj6k!11$DHlN|k>R*R^p>+ObPA)>KNMt|_ex)Hn^jCh)w8Zi%bL z>znkKW8l`!dt<8etI+6LAm<`hp9t)dVJ;-~05u|C+E4-S`iI+Bj(?qo3zCQkxahA}J2OYwUH!#v;+x4pAYUvsh1uy@TnyJf=kP?CjM!;R1}@g( zncJuo7FyuEvKX|R2SA)H1iHaAvRzr6{Gb&qhSofJg+T=aVM9(8b5lE>sa4xzQFVN{ zj=lc)e}&dGDPkQkh3v&*pHo%>q&hddekOWQc&g&hdi~2~8QQeoB*u33t6>C?Am_pE zf7_w_e(Tu^RiVex6j1I0W_>V6lf1vN!FYDzW!Lr1AqBd&5@b+g zA3C=U%MrPox<(17yKs=fngjv7E7*#|*I=)(Q)KFW&D4pggzm z$du6)V>05j3jJyf=b)dqV$Y`kmgVa3xv&}74Q5UlmQQkIX1iGVmg(~pLntf-S=Qnc zo5Aj!AZyYmHvW`(ZBdZpKofi?CH|ASCaBN{D4~XfiRew_{7wE=uGy`aD+jFM3 zSVnGIqG5-_@`oK`e{*p^^9(x-ITnqMsh7s(hiDHPY=pvlac8K0`+%Vk!tR@~FmtI` z10)#6Zs(5f172Zo*@J`MuNHnXF)2yAQ563|o6}SnX>%?~-4exFme1fy-)!lB3!CAJ TONVii+y1Y>TJ=k-o&Em+uOY<{ delta 569 zcma)2&r4Ks5TE%Dww23k{ejP5rALs}Qcvl!)ge&(agS~Z6}hp(rm_~@mgtg)-7a5f zB0Q*c+_rr@mxo)iPurAWz1PiCf|MY{9t6et>iz*8zBBV3K0jvWP2t4W-{a-0Nq;vv zc&hCr3_yZAhL!EVy;HJvKHts%_OQ3BPBr>U4wxL$5>=_8ckbJM{z3XRj~391Jb~ws zfs8)0c~Q^h;v6er59e}dwe>p=%<@jJ>eB8)bUYM^&$KRLYqU%ak*$#n4!kFi>Qp>5 z2_!CxO;He4sz~KY$D{9pDA7s_W3oYcWMWnFH~J)Tg)WKSBJZWp=G=yiUlEszX!*i+ zz#x}+y^MX4nly{#a_u9Y_SI&2L=Sczn-)P1xPh>p$a(Igp z@k<*MdbHCDe!G~BaQcGk4sIs#-F$H67p}&Q)IcbNWqCgRXt~#oy9gakYPJgH3g)!3 zBhRS~|ol_Hi*HSP6 yV=yh}c>&|_4AN3Pk~a-gk{?4#>Pg79x>NGc3T_g{c<7cI3LcKAZRF@}_38kQaJZ8I diff --git a/iOSClient/Supporting Files/en-GB.lproj/Localizable.strings b/iOSClient/Supporting Files/en-GB.lproj/Localizable.strings index 60866739003b87c5be3adfa497718a09bbc8954e..53a2e57d7c566a89f1fc3467a4e65c844af745e9 100644 GIT binary patch delta 2117 zcmbVNZETZO6uzh1y6$aR%i6ANZSft$F>qANM@DesM#PU~3Bwo^MFy;6YqoyaZj32` z&A=as!aT+kV-QS0AtAA_m=Z(W5JLPx2x#KB6BUUO5~CrS5F(y)-+hIIKc;#6-rRf6 zbIy6rbMCu;x8vdwM=rFOb~J$VbYrDYXHdPOw znW~@vdsPPETmWx>VxW1waM)aYV+Pr`K^g5|3m$9=f zZELsnQ)G}=&6h7ef-64{(dl6*rFUn5LC>6rxm4(ea<>(!jWcOykRE!V3o3BHWlS2o zXyrnfLx-+|gIsPX#YH>I(B+&mFz{99+^O?o3o%1YE5JXAVqoXUG$Xt@uMBPHe0X|f z8W62`3l@;!22TNl;NZj4{K@wjr#zWOk&c2h!(IvD%Qw5h0a7F$9V*Qac;L9*IY+gs zESHs0TPa!u)p*E0i&|3PqA#6H-SHuVp6-PRZfx*Xi(NNAZA`t%M3@tv+CQTBM+dJ# zFy9z}TSHja;Gn;2AcU9Jm+ORWHD>;yWAJ1pw>HCVO9%=^9e%1k3qE@PEHo64pX7f% z3xhs<+}!H#Q=KZV58dcLRq*`sEBaLT-?#upS z7N?GOIU2wXzqv3HuEku=AhQPw>B?^Kxg|>05-G=QrxR%i)7$|t@zeS;JoR81rjNPE zlLWiim(s~J%%*QIfT!A2F_yN}X30euZ)2pS+QNLqdEJBi{^9<=4xT`kjaZx7JjDj$ z)XCveJ7B&$t!uS>V=9hk4&FyCFGD%~egw*j6YSK^SU-Lo_DXjp0$VXaT-G$Q1OhXq zjF)se5{U(qZ~Yn$7<4VoIp6*nYUr?woA}r-Fjl~#!Zin$;&b6r@{B+sR*t%G``Zp& z9-U2xrIA#p7dVF-;4*Ah2$^TF*(| zuNql&vXZAoi-bWqJ5xIe?cIEbSQIiFydUTD>^QArD zSEc!#;`5mH5D5yF;AHtsL=@xyn6^!7;<|BfWd)Za@$SwhW@U|4t{~OfY!1AY2;oNN zr>D_#PaY&>l2b@nJm60+dY#i~z%@bt46Q}hvve;*Ckj|;we{c$Tl=CX-e(+{V12Rj zI*ZY0xWH6h)cQRHaO~1-YB&N0{#$7Trtmg1YMQ%}5>G+)@_koT4}q!)?8#FuR|t|> zjSKnrzz^*yr8#k^!Ch`&bsalNH|1^J=oW~uoYX;08hH);P>0{G4%3QZD6f#Q7bV8D zS6pje`%Q1w`nDKB%A=Q-g0TE7uyNv9G`$NU~ucr!BK1 z-xcekOX9QN%*Vez@CAgl4lXarHkQtGL|n7VyHkQpiB&S+!d8-Hmiuk0)$GL=0I#eH z=joJ8wVCnSR2T2d;K^Vo*=mZ344+JBY=Ug@7#~lqFV5Mfy7Ax_0bKBoQIh2l5-dXN z>LEyP?&i;e9<)QX29D+msreP2+M^%w5Z>lqmsrwea6HrRbtE_uk#jzD74`&65?bZV z1RXd9;iVEtMbcu{2TEw{2fu@$W z2EpNOQ4qNa(intOLqZm94bizbqv<=}^PTVIec#5Fsd8khdPi!lk#|c?nvL`;`+l)j zUM&MAw|PI=6K9PaHJ#|8fECT^tplz2OI4ds4=1;B8dS?+ilI(cJ%GGu%O#; z5hMl&hj5}H1kHXBAvi?W1ovYhV2}iysDz+_NCf?Z7-Jj(HJ_THXy_8h8af*nTHi^^8g|O0$Y7L(NpHl!HGD)dC8GrRwBxXrp#u7@Q}kuYb9l zHon25u}cTn;1XYuoWtOzU)|uq!9Wn#I_=bQ6e_WQl^^@Z{RgbzD26!k%Ii+N_+}*z zmDwnH6g>1&1iaY0b0WquDXn67qH{(urYysy=_*a3M$n2_4jdbuU5uf9Gr>Rg0UcC~ zU>It0)XuD0VCHk>up*4uuC|o~9Cl8C<%1F&)R{x6%O3K89n5-Hj)6OSsRD*11Ih1f(BZ&2mIv{+Qv^rWw2tzH@QLahx%?q zAbX%1?(L&xBFXCcpyN=D!z7Hq-|=MkABE-9%9}V%io2w%Fz!ui)O{BGboVT*F3YcE zS9}hAejL<0wJz1dUXWayI9m&MHm#Zws_R5CZGCZ7FIi2wtMIt)ffR-p*5c?BHXK-H z!=H5zT4q$!?i1k93=Ze^@F&T@Vb#S?EAB1VQ2)a-t8|q~=2kgY=TV*xz5gxvAWiS? zgjyQu1t;EE7o<@u*vNC9%kk_2ze_S0TxtjBiqH?|ppMqO7gAY%S`{~{Qc(_X$W|SfL34HWHFU+0VZdM{rNb=S= zL74OOZfE$$Bj7DdFjkEFv}UAMlaA%X7$$Cx4f^UbXeQUKW@64jFM`FF9lZ+s9aL#B zy>&NXF8$cgg&n*J*Kn}Hi|y}KlH(R1kf*NOry5MRgPqvMkYN?2@G#87d(+%>EDdhl z|9v$cPT2N^ypKx=dN?gYtMo$u-@F9%okeErHU=&cN?V^^HuptD|n$EYd#Cm z=mj?I;ZMO@BbP{qpQsrbS4|SnBs=K2E(nz6Cm1%ga)A=&kEt+iy#U&zm} zX%7y5Qz>qyMOT@M^s0-Ah}Lq@GdvHQ&WdBt)j7C%zQ0Gh*lfOyt8PZkXX3XylSr{g z#7SvZN%Bs3Um^>=GuSJ-dV!H6B?$9OmO85+=J%}}A+BySg^A?^QK<%|C8-LgHIEd} zWlQlA;JK8+=En_3>ZTa1jHGJ7zs3V}W+%*+lCkl52kIYNY5Qw@C)~~QKkmg5w=V7$ z9a7A;shC}7C#KBGvYA_OC@>{q9vv2lZl8kMCnddjLx_lK1-EYDx6LMEp}~SI1?0|L t162UK;ctuOk?h+R%P&AdU}MOfX}6nY#mHr~R6Ue&#sADm39IFue*hqVK_mbG delta 386 zcmdnCisMfw$A&)R%>gDne4E!eRf$f1r^B_`C?c$CdQJu-AA1@@DMJoJ&h&|0jOLR+ zGzmH6;&W%(1q%FBTYfo@^Y zn_k$(C_ed@64&N=mnPY=rU4Bpnl3NGC_Ozvn2~2YpD-gIBSdsNQ1m+6^c$>9BHJHH zGya;i-Do|dl@xmtLnY9Dx5*EUgs1nMWDH;i3T%ITlCe=_`=m#VbGWA4yk%6IzVAFE z+w=#5jC|7%@B)dlw~Y6;U;4^8L)tPG=$90zqcVYh%>bH~2*kxeKP7^FqyTahRI~u- baFGA=8B(Sv3Nblu=MZ9g$2fh92vZXP8eMC> diff --git a/iOSClient/Supporting Files/es-419.lproj/Localizable.strings b/iOSClient/Supporting Files/es-419.lproj/Localizable.strings index 4a6997b041737dcff8ffbc1465ea65657dabf6a6..e409c634a5d8f16ac6099d9b0301e832b069a5fd 100644 GIT binary patch delta 2246 zcmbVNZEVwJ5Pz;4rF32QUuoBM?YeG27z78?2?8^+eZcrZgoGGEFl=kLQE9iX9a}Jj zHzOJnba1eXx-CdB(ZrZ7n{0`SQ-?8X3X!Cz{7J@DAz z3!C2Fje%On;K57lYOwub7yaggLfTRYcDlYB=F{Y-V5OcBFzA}i;+~})z>j-;wCfBw z<(vZ_+3&#M!2+xsvr*p&I5Bd?MMGm#Fp#8yC&7<(jZW$+1S8M%ILThLus3lU7z4jH zsKb8uj4J~~T}!~7+r+0jIKCvu+&+Dp`rm}b8ZnCzal@mYj7xSp{vFKEz~q<~lwjRbxAq#}iaUl+kHFd@ z?0DRXC*Nnzz5zEqcpj=}H!(`9iVwG)hZo&=wY0<2$(c8EzLl+RW5lFt7Q_X(ca_y+ ziXpSXM_W`gUM?-A>j4N-e-Bu3nUPK16Rd^mvLYJ42X;@2p(GKKimePOj&7&Q<(!b4 zH^5#xb6>*L`+PdYBkd0UzRxpT5+l|IR(nNCI20_$P=%9fk8wB`4zYBC7a>3mH+eew zD$D8YVX)!Rl?L9~7@##TgN>?Q;!~lEP(+b&R_yXCVB`y{VP>I))gEILVfkr2@CQ2KoC0;1sL+M0&87+B)xL5l?e#z!WM(l0H$Ri zGsgd1|qzhRIAh$j&Ffl!=jXteAjF7iVcZdP*Q9wrsc~hd#1Wfl$TRF zN=d#rB*%oV^gO{cITKY48gkl14bNkIZbqycL3Ard1vkY!i1fr!32q0EB^ZcdPQX_j z;-MJ*r+*PM4$40ZK6E(jc(*lxiBcPGsx_AWH{t|P_a6M{HDAsQg0L;%!QHvGS*#nC zq={_HJPIbkUYsUauk&rGd$S`kb&|v4tPoeutsCH_brayiic1bm2HXOc7h!U?a;V$* zinOZAdvl?T@{`~#P2*-ZIb2~#3OOq2_&4mYHx^|vUDVbK9_s1@2krffD~lU!_Pitq zX96!_ILW;Hn@@xZ-$>29Py$PEVnZ>FegXwP=^s*05q&nQYf9gs%g=*E8DON~GJ;iw883+rfgxA36VYOX_Y8kGD z0fnl@85Hp=n$cxCKXPTN0cc4|Sgdc{`A*546jLjvDvkm7%{xc=<&ip0oO06UgZvtP zF=WvAYA}KVX7($yp_yGro+T-O;n6?fgnv$4*9B8WC7(m_N--?eDR~qA(iPsotIgW= zbgoVALcA^oRY8>?S~M)>!{d3DNuX+Ak;t4KPG}Aq>6v_s_dbg&{-;IyFyHdZ-!>CP A&Hw-a delta 419 zcmZoW%5i21$A(uXo7b4K@oi>unI<~fAdqYN9zRBo&1)j3)lIIE5nwN6$N|Fm$$~en zCohoan!JIRZF!iWzhmreFBSXu+$%kb+P@ z`EQf7paReikot0_37`9F|uuMkYQwFM2JdH zmYL48eS<9H4YuhTvP>e|OH>)BP1~NZhtW-nJqhdsx9Nd-jKb58U1JPj28wK#z0TM! zvi;I~#uZ%CAN^t!n?COsqrl{OU-+gwXftw6I>>ddE1OMTw~i018HPD8 z?K$VX?+5Q%fBeRJ@K6lvW3^*@RHs^}(zq+*#jW$o@#{xg%Q{sY`!e1tL*-Rk8EOOn z7gSamIQzJV&hLQW;Hj9y2@%@zEX=acSG{axmFiZx!P9g0cL&0(BV?)h7WyVPn%q|_=L zxb4CA6AtlRgNtU}1Fal}rd395(0=L8cO3gjbR|SD^R&)%7N8wIs3@NDLj{cF?I_-; zfeRIMVLQ0+&YC95tN|OgA9g^FuGByP^RGAI6o(bJo^v+lN~v~nvIfU!D3N5v0sl{y zA2rx4q2f>jOxuIkXIatWYN4lm;Id3Kishff;-I~Up=l<74Pf|jFImq(HNA2MW^ z4)!1O6fd5Ed^H{kuk)^FIue@U4rNl4Wop3HpmPljZyKYU3>Wsk42SV*xEkM^V#8aD zDzG(Bj+TfU>mxO|V7bkkV-2Z?RHTEmSM5cYw2*?3+G)-UrO~bB?(^V@lx7L8{Yw!pH(?L?n4Hp86Zll zx%T1h@X+{-&ehb7s|4PT1T-r9Ccg8u40-6Thmh z$0aV`Ti3ygGv~PKWE2FF=2W}%#EYXiOPGAR^gMXWxYl&D0RAmA)N+n|f@ol(gv<O~xyK1}bGL%)#Lfr{?_-;6|$m&(-Z9#a_>5WoYDbG0!-WCb09; zgh@I$y(2RyC$zLBA4YneP%pB-;TXbCc(w@MJUNDkvWKlpd2(D?LoH+rU86ec0Ufcy z+;*~jt+t+Gyin9_Qlq`|r8m+|CVkSRQmxWJ@;>KL&Z-z(YBDvcX1x5=Bq@>KDr-O_BKyLL;A<187WeRhj~#sBD4#mVEe Gyzw_}C?Wa) delta 396 zcmcb!mt)afjtx6ZCV%i_*<5ET!MFLDbD!vBE!+x)J~swfsuzZo*|8) zlp%*9XX-^p<;f0pGLsWz1*UKK&B!tR$Ztl02sH%-2499mhDrtnh7us1&5#OYB?55{ zLq0T?#K1Px0BHtPBZ7=xGxIo%673hN$s7o?|KFFNx380CddE0DN{*=s03+;e>Hq)$ diff --git a/iOSClient/Supporting Files/es-CL.lproj/Localizable.strings b/iOSClient/Supporting Files/es-CL.lproj/Localizable.strings index 3d7601e6cec8822789ace3775adc9d4875ca3e4c..ad78c7427609fcc8b9ae12528f4fc99221981e79 100644 GIT binary patch delta 2357 zcmbVOYiyHc6n@Wyt?Rn&+q!Oa-PpG>F6ta84uiVMgkVI%MI#0UXV-PJFxJs;6EV@v zOVF?%1YW~21Z7JMCPd{jLrGM0CThYD1f!^Elz=f23=xw}6Bgog-fv|EV`5Cx_U(Dk zBc_|*2=Z7t6&M9Bxr=Jpx(JKHhPLrs!2iEnc>TY<-}~qn9@Ora9wT zMooz#D2Pasrcg;$Coera8aiAPyK0 zpZ}UxYY#tgsUl` z58hg-&u}*P&8KYrMA+g#rXsInYrO#n7E%T&L^F^I+V#5WV2FbfWA9H@9;%e#t9up$n>ynjBL>mZ*tEhhVa2bydkd@MWkGc4Agi5(@1YLXfRVv{_BlM$w=}57@drv9bqnM==k9Z0lMC(D5cEGL7F@1wE zdHOS$FUiX)&~~N8kM?5#yDXnxFe{s%dmJ@;d!escR)H8NKu6S&DPUSBYJp!ul}N#g zTM|gj@Z!4-mL1TSPQ6pD0in~?0`#h!@aB^xS^l|Frw(`HTs4d%tTSE{7lLR>MWiJh zZrVxy-m8986C8BlDkDjQJ7SQ@tn}dB1st6QjbzY+vmh3`G+wDhLbX5^GeaUUY~y1B zWa~Kou-=nb#ut~6M{n6URSztjGW);LC1jZuq}T2a=4H^rMS~Q`H*JlXAo4{R9F+KQVD92VaY+J48kA|s??Q>+2&Alm{D+bAX^MuzH`0m0a zC5*kCCFl0>EFBf*VTa19)8Qsmg?UVq8A4fQ(nbFoRqtA5am`7Z!UY55<;F%#Czk7^ zvLteMeb<6oX$Se<%&#|4(BES!;b!#=a%@$#`mTp6^`d#jl^J_pe(zhDaVJ)FCR zgaY@uRCO+pD+)r>Ej+lDf>fsq$IAFDQ86Dnjp})IVx)1s z{Ot5z1J2w?pSw4Luic#EN>Gl6h4k8vcMQsj=@88}#FFlE&_%Xdj!4lA4>Jv98yy&d ze7&+4A#^7Ugt3^*lmR25((RnCl$cyy9k`dY%vk7^5BPQMyWU&}{7Aq${*EZbpD@+P zykj{{(gXKp=g5N$qpF)u`E}P}4=+7}(j9G}?5Pq%)<7FxX*4!QB!N=IRBG!e?G53f zFH;5v`NA=(oG*AJQ6W@lO%GWMr0pi7bSH>s$;chgI=%fey4IgHE48mU>muphmwo2v ijWWWn*Jk{8v6cHDW*KC%RRvEbfII#t0PZiz+V(dJsZFr} delta 416 zcmca|ieuI)jtxIdr{@SV3T!@QX2G{P$aR6}Zi-xVC0(q;U6O#Ybiqx zL(b%j(u$KmoKcy)K!#&_93vB#aw0lmeA!GUPF&gQY>*ih-;GplCkOm{cIIm_Y|9l07}~D5LoF1|}vs zUIm5}gucnMTciaQfChu~l>>DEUB#d`{o)5k$;}$Kd>p6SNHKCuzcGuEXL^A=kVueX z6xtpk$H>Nrkl7BD`M|c_LY;Bm^zAzP8QrA#lfVvk1G*;%_;~eOd$+EdbgD3Xgn-l5Z4$wa&L~vn1)gXZc;zvwCLkI>j5J`+*Q<7L8#D+VA9@IP&2+f+2U%_rN_Wmz?i6y8Y3`GW7Sx}3AL5!cWMEK zn&5=mp;~Dm2tkWxWa(_3rX@5TnTU^@A_a<5E!YnBYv&k1?l~3JShd*zTj3uRwhA*vH)UE3n&( z@#A*kMyD^!@g@^*<=AG1?cr-1-g+;D$D15ABCW74V)Z&l8~mgxQT-+eOtMOy z|0OWqWOTR}9JIa>3~G3jyXWYB@RmrWth6rIOz?KBxdvex*#`!$TkOHpyZqy^Zd!W{ zB6Rr%ltvAeK1nJ_g0N3_5O{O(e90Ba%4)F(Lf^v$^>+~rY>Z-J8~Po zhht8v>}NwVqu{5=C^yHK`@l=_QTPTQ^?2~>K|h`4HOxHWprOl91l@G$DLz(J^W1d6 z1UJ533Za-a$Zv@yjr2S~pzRvTH^ zbk)FpD^GI8b*Bla2+rG;)*W6zN%4AiY-20}vbAE9G>Q<)@SDV>R;w)=HRxmQ_iQ4-gemn81Xs6kLY^Bt%s#awhR?(9(Ur8 zH)r9A+1@^Jzn$?#ey`?N;4|?IWVI%1(B(%)8|O$TWVxXX)x;P)u*Mb5WMp~U!srRL zji03NWf;kj7_)aSQ?jI6U7X)iNg10Hx`Rc6J`H$hRXKkCt}~itVr2Y|}9_!A62YSC# delta 392 zcmaENoMX#!jt%cjHg7Qf!@K#9^EA=P4tiYEbuKV+Y+e>AQa?SXl#!1;jiHnwhaqS3 zd|~y;3a)a~4ayj~rvLlT$f2CbP{feTkin42P{xqTpunJ}puphEkjPL8WS0QxY=+e7 z`@b+s>gxdoT!6+D0hQ)4q=ThF+KPd!0-$I<(3n&pub4pxD3U$>?_oyq$seu?O*dd< z;^9?bNI~eGY~3s^r~ot^q_-TX3+OZkz3CI{fI93{xHez7;o>+wVJ{=wbS61QscDjo zLfe1HFtRa%#kK>*{;_Spp~^UG`t}oh89k)flfbTZo9t*JJl)_1V*oQyV0-%w#&(hI zHXj+6aZUg5n^A3YPblAX2USL)=^B3+A8c=9VVWatnF{nu3e*{yK>ua{9h3;f#Xv75 mf<2@F^jHy8v;gQ-koWT$Ql>9dVshMmK!NEUDgU5e4xFTL=LpvQgS@YgoIc-I!tm z(~u~{1m?jV!`IM|kPwUmg!+{UiHh<=1RDKI9ORr77vuk9qz34V#CgS>MjO@>b+n$WmfH~MSq!vY#u&d zl>`R`yil(1AVs@Zz?|H5ff;a0Md{KUaLzCy>D7Ja$Vrx%l3~@&VJB1G~e9K$_qqc6H=MsK=XCRvJ7A_Bm;WuZLGy^)c8XUUHHW!~6`&k9HU3c{4`>@DkC5 z_Zg77Aut2QZ{xPigCD@MilR=I?&X584IHU;Q{@$K(Y`D2*!@#C)N&P^nP09zp9_Dg z+2S5x7-HIW2Rj!?QmTt?)j+iX=3Se+_D^54g5Y+orUrWPT!RmdE(_j$*n;x{1}+Vh zS8wL1qbjL7IEyeFl(z)Gh13AA&3K~7t;@0gEELwv6p_@m2rKw`r(>f9?JyhnZZPnv zCY$CIjs`7scq>?GT?@n7wu1?CY#3Z6;wnAG*)Z3nbpm|QiKSPacyGdvYZe(ee8x{3 zKZSfzuv6OvRMD5$z$o>r2vc3$nq>{)x%E!0J?(lltXn`#CvpCg+*4`8C&tRr@3vsuQio4Ckk+Y%IdJKINg=`~Tn}0h1vlpJH@5dOX(D`)Qj65H zoWTQX3A0povf4DI#E@i`#id8{c^a=6->0WgH=RoH1P>vC1d-%7k%_5I+G5Vb;^Ch-;=ZAoTJV;9DpzB^PlnC}pS9CO+-bQD@67nMlyOMQEfnuVr`_>WHYaTTFGZJzx@1^Ci~y6=Q{It{{>PsDAfP} delta 396 zcmYk2%}WAd5XNWL$YsH;$g-?t$?_ytU<477CEkJ#UAncUr6?+X(j`Iq2V#loXqUPQ z3+>4+5q0bk1=^H4TT>cXuq$S(eoduZeM$2R|!G`npBe*_PmOC=mS{qgNBB22^XR z%>8T&0(5$eE^T#2(w~~)5WS3{NZXN+xIND z5iJxPrkiJU(s~nDY^-V*cf-bc6AMmb`5iAMYJZ|n`!33~EAs(7Cbo=|KA|?T6l11n l&sBoK#13G-n~P{s3^jMKxwZ~{RN<1~gD^^W=mePEuOU3ler?7>~ZVEu!t9Yv?RlB|s@#wSzx_ zZ=Z_s&moFZimO*rm>cGjcLFR0NDtEK&caM7t6%r!*rE%d zQfffIdLz82B%&9M7EoFrkA~R48Qs~nkb?TtCbw=_TN?tHuAG`&W55^;lp)* zQaJNggtjqXID-k*&LcnDJ_<_Sc{A-y`B`eJDC|JYUUURBd~z=ZxcoA3Lzij6l&mT9 z`DNPb*Eg#N0vphoxQX0?*wUqx>eRc_)1}_8F#+}JXpSv2s&>tG2 zD&Dsl1?;#(6>L2~9v;0#`8;|WKSzehUnEh|sJI{T+Ia8^P1nO`{Csa0g!R;XAD@1h zf_(cYXbo^LZ@EI1eDNCjR*A~`O-yIz()BhF^*|vBRO!rOgHg#k*!qx}WY}gxIntA9 zrD17ReZ0PtN{j!I4OH2%IN4OB(@RTJ@_r{!Hj2q({ zjZ`LuSd_{<{Ug0sz`<8gRo-2i$&cQpU-e@?pMLT1Og=VDE}eYFJvj~9e!kR9QMS$l zI+P->ezVf6Ym((`-68j24-leM(M<~XA_MX2MXBS(w&TgTT%-+BD++f60c);79q>$; zBb-w6)eC6o4AlS$%bk3ijNl}Ov@$PfA5`eI(y3;CO)^DN((2nic3-y&@UIcCqpnQrR%<{;vWu@6*h1r zWgde>H0o@!m|nOgEECS3YO5}oVk*gpZcub;t3k!p02imIOjwKC(XbYjzYfkI0Dmb( z_?v@NR-KD5W<)gUnrsJfsF+^7CG>A`3diCAQ}1r=b^Ht%6(IxRaV?RApld(zGrhMAou&dVm`5 zic^+oMFuV}Fd2pn-E0G+Pb8fW*Mz=vaQf6V+9{!GdFN8RE}t8cH&?*ynu00vN`qGQ zAE&~+VgXsxgi=CxWH+7!xAE%eMYx#CM1H3u;pe-}QIU4^z#vy=E6LJx2;?A1R#L6x yCz#wWSc|8f2A7+?1eCf^Rpu`%uP%?hC3*GiKZcpSZ{=Cd|6@Sa&e77moqq!wo;@G{ delta 392 zcmaEJoMY2+jt%cjHg7Qf!@K#9^EA=P4tiYEb^IAQHZO}5sGq*>DkBePJVP2oDMJoJ z&g6$TG$$wAmzjRx0wc?GgEB_8sRx)ire0wZU{zC4VDO#%f4Zn$4nsae9z!~V0z(Ny zCPOYmDuW)7?E*BR2*`)YWB}R4K=A^gXg)(SP*pJy>M%_G$7I2)z>tDaKK&92k>s!i?*<(nQL$H+DP!ZaYE@`v%>_68QF1=5zOK;NW5-IWRSZ3fV=L?A8(`YI9Z kCk2qZprQppw}V0;pCM)Xd?hBw?fVp%-Z4(kQf6ub0Ekv|6aWAK diff --git a/iOSClient/Supporting Files/es-EC.lproj/Localizable.strings b/iOSClient/Supporting Files/es-EC.lproj/Localizable.strings index 463d17824b8be60ac28abbc9b13f07a53eb0f00c..f81b17c7679101d818db410f7215a5dbd4497a66 100644 GIT binary patch delta 2778 zcmcImZ){Ul6uA4rn)pE>7>ELfFBrd=OcO~kG9+1y8OC^i_x8_#Eb_&?H1EFm z&OPUMe&=`2?RS@*pWSet-m+OuZE92Vm%ZxYO-=1DiHs-+TO|2DDprdzUZ+G{jEezb zQ~Z?l#cY0=;n#6CO7eY-4fvxN6lpQ6SLXWx?J6g16}!|(zXW8<%+Adzw+vE`L&ns5 zT`szkmdk45Vo3F*8fU)j&idu!GY|F_4y)fUxYRr0x|!(g?30~)I8>U$q#4nW4v6pt zk>zk1z@W218B$-hg&Vi=hoHD)1MO?K{L3{+x)_$tbSWqUYW!_Coq0;GRI~MtB?2Tl z7vwW026O)wOs7e+I%`WmX6oToD$mx7dSep7m zu5DjxcZOLwpyAQ(qzog{61F4km#YZdX4-gFI%sD~u2EM)PC6Wt=Q}625`9_bQaInHXN%% zbkc|O(o6jj*`oe_rOuaUb4UWB=wk-J@P4&D+q&)<{WhyPsoH=gZKHE5fudctmah?l zFyb$FSzi5o!GGe2bgG3_Hl4XH-88=-y(?@TGS9RWc$J{e>r6)fNolL5ZC>?pu8n$s zlgbH&=*va4P#oO9E9hZUhy=jAFo=&=5v9~_of*`Gwz#-AJw zJJ=1T0NfPlNJ@s5*&<@AH`^nWIw4(Thgtrf#~doq?d?MtSZ0{BDm$2xRgHmL@Bu*O zs#a%chNEHfQW8NFf3-pih;)gtE9p@#P5tS*sL8?Lw0i&^Qe zEzzzv?Df@fVd+>(HaDQCw%E@}4u~v0J1;}^aV`8bn1-8Bl2hwr9#WhnjySB5@Q~&S z-15K z%d^CBav3iu(nI{dP!b1=g}#1Y*2^e$)<{1c|519?KU-I#b7D_xF#}F{#$bAxV*_uI zEv5=G*CRkpf8$ffXPPDvaEQZzk=Oa#VpxL=r`$_$#mX(A41K&uCdrW=)QO5&$W)7jEbjvAQ>8}s? z)MgU)KCh(#YnCY&XGns0HF;-ymOQh~}h3eT%7D>$SFhBf5>j zYT1pMy_ng?0h%1p&9tiwOnx?!8PaxT^&udV*JD8)n&W>~VQWosZ`hiX>gb$HKXSSy WpR{X>^z{K1rpf;VgSJMjcmDZtG3W|8?VK!cK+C}jV-ToLPAPM^rqa6Vg)aFuP9WtO3 zqJsiy!ee66!A9p*N|%XBf(CISB&|d0FOpZ85N;wS1c;0M)zu-+adWg&BQ{M^2mCbe zK|?HlqlYJk96eG+qw-xMsTfr8TEM~b?eqH*R|eWdw2Cf~^`Tu{`>{_1e3X`aXcHG> z_=UPKeK^>n7F2Xu)p6|Yux?bW<*lbGZh84msBNa@FGBvqB$Fh63G!+{gK{|~jgTy$3LKJN8uloXE>h%7&PvYLY&w;wbSpp0gow~)Sjfswvkw%3Aj|y%r>}~m diff --git a/iOSClient/Supporting Files/es-GT.lproj/Localizable.strings b/iOSClient/Supporting Files/es-GT.lproj/Localizable.strings index b10fa0204d877f6afdb8c169fdd14452abeaffa7..667f6598b27043c936e8c9ecd7ce182fec6ba8ab 100644 GIT binary patch delta 2292 zcmcIlZERCz6n@WuI@_{!T|4NykISfh%sC*N5eQ!+lR!*>5F*3?qio&Qu(eIgnDB!M zF(H_k0mtkmf((fPH6(}w8a0`c7@0&8BbcFK0)8a=2WLco_(z}f-gYgR_|Gi2yZ64& z`FhTC&b@rLaQtfFd-oglwsqv-`3+@!@tT)UB%FM|p_xxM6zT&buEc@Y|!3 zxNBEowHr@K6;->2?liiJ^vFT->bu*h&b3B$*r((kdTn!ApLSd1 z=4B!0kms9~O_ShN_XjDcPX)-Wi^|E#FI9x}7oEIfnR{j`Q*kS5wve5rczE_FSz{T{>kSuIH@y-lgbSarxxx^g1q2>pO0?! z^6Eyn5u84`mkRX=L1W{uqrUB*LrQNCkZ-EQOJzjes`rmlHMMie*QEnTAk&SDy}EOZ z$|mqddhIQ$(qD{0cb00yC`eSh^f-j=>R&PV@9o zbSQ!>npT`JB(wq=Lx?Dspz$mIME;*#rdiKGulN(Ua{h0Zk5~g46{PNOqG?jHhmz?{ zztDLnpBQ#ii|&4a_Z1JwqrW^1X4XES?|7Bh%Xi-><9(++dQOCjrlb(AM_=DcGr6Ln zh~FLc@ZkzKHxEx|%kSXzE4`I6AOee{IXME_@CHrM^CINe)*_nBg~#1}acD>eHmMe? zEvVuNwE}XLJGs^yg^4$*E=y6D8H5~lQJkkT4X`6i8-){*gk+MylVGB1hw(TOVVP>e zi!MNV0O|rPjC8~#Y{E&$dT=F>fV3cjORzCRW)~M%1cDaaip9g}wMiMedkuK1O;qvt zwZi3+k5npCCVTTgd4f|}VzyheotTaXTzM?<;HaPXO)Z=PWvC?ocCS*ezCm90+;Z?+ z&wHjsVV>f?pMnLmEZ%o(q4p)oCkDlBprHjdbs`Zc7N(_CqnmC}K#T(u2Z$~-$b3hI z^E_w#)y8t_(fuz%GR=eJoxRCGV*p~vRUJ&FmGa;IP-Ga@*H2=u>Uiwqpx%C)e9QB; zHMkXg)XYWJN=TBumt7>{-R^=x@RfUhmskn=PpPmjxlR5_5mnC@{+h<$U-jzdcc@IH zS^2E-BTeW<44FImfY}=}#=qX3%kS6vAV@4S#FR?*nz;ENx}027CRcm=WGA{EHqx{d zdT*(9@X?y*VYbQ90{3CO-wA|Bv$1@Ay1aOnO`F|DFMRekIq#; zo&v=Zs$L&y#`kl}0O31>Sxj7X>J7tCg!_`GG%RpL>_sfmFdXxp0ly<|RGpsn2b~FS z?KWZ;N@7MDLb@a=OG`45aY&e4q)5q1u-O&11Fky^E-M2mQK~V+qX$b0n$oLE3ht9W lS5nZ}m-E%4YJggbt46JbSJ`Y9er5Fjr)L zJ47^9|3HLO)Dlf%P)mt*d$Xa(&}y2ru2~C+-mGQ z%9P$lt9%3{NJEC33(wx|#r+32l@^OKt4I^%hc z1%5iIqEinoD6OaVMwnh^P@!E#a#WpY7e6jE;YbrMH@=%lktMt6EMkSk!!1T;lAHn8tx&QzG diff --git a/iOSClient/Supporting Files/es-HN.lproj/Localizable.strings b/iOSClient/Supporting Files/es-HN.lproj/Localizable.strings index de1042d64aac38dc2cb8033a2c9f30ddea0fc28a..9f986af5fd89242c80ae4683a901650819a502de 100644 GIT binary patch delta 2286 zcmbVNZERCz6n;;Km#(y3*RJczHg?xdK9(^ipvZ^I_)sAt4x^$8VU%?ng|6M!4KhqB z_yY|=K6a3!BpVqa{?h40QZ)me6Jw&007jR9WRN8!CN7AHzfAO;_io)p6BE<)_V&K# zJm);m`M85;?Z1C%|6qJ6RxByP@=IQPeZ3nGFRjJiC3bq`1bA{y)u9r~R9$@SRPD;d zEBAco#+Djxx_uD5)Efa$cWypzkJ$Y!Dy*7Sqe`lfTB#iTS)?kc&ut5wGuH0&lQ|5| zbBA7hD@B7{e3u&8xSDRi%pf;|P(Ygsz(H42P)?N>!A^rgaMM-0Ee}tXJLp&!IPgfn zgDQvlF`GKkI21xN-=I%};KA+#UJ4CQpg=o?H$YkDG}ofg&Z)&Y#TL~_rNa=^5rs89 zx*WV2YkIX3@^YHka1V2CS5a1Ay2G5uDs)Vjh1$SIt*UDp;E8L$bX7B<>rp_HWm z2!a6_;!YeabEc~bVZ^}o7d&X(&hO3*(e|U@fF!c3b3ZeU6y1LG5$7i zRwn0Yvf7QDC8P7TNH2H%S{{y-?(y$cJ6Qq=S#boOnr-?80%iL7jDTMn^VW^ z5LeCkQ)wwAaHzT%M}IN!?!yMo4|#A+r~r3ub@-EPFC~$p)H5uns&>=B1t_G^i{L1o zelAS)Ia`~2r&Ge!BmS9EABmNCJRn__N3plTWclq>eFO}ueiQOs5+%u8rGFb}^N&zM z9Y@*fxfNdgs-=kb|IGcymTq$X07dld4Q|Qu>)`Q;yi(t|YGMVVeBVKRr@&7Iz2FHX zxddTp4z^0Lg^bEhF7jt@z=E7s&CPO-s3;DdEwSQkIuGV@ZbAM?1qJ**0DgL(H(N%v zWs(MNfnO79V>&{sgO0mxzV!GV=yl-63vN1m59U(pJ0|bwW=h}RgPZt-*Nd~y%*9-^meTj2KoCIxr0LXnh--j;8oB<1iGU={K-$jkjLPE%phf-k|jpEhGG7sx3q! zc`8!qTJWhBf`6OXGsgDPn=8~-=K7$jMD5^- zYvr61;)ercJ_uo-%U?EyoH=D~qN0p`RPV5VN5>mOo-CG>>U(%Ts(Uz8?KS(nE!sKG zt~{iwlEI__uz%42(ndD zZGnqA{n-*h#9m0J4?rNRQB~2xgAkl0b85|UoGyO@h0^J??G6|LDd=9#Q(9M6g|%Me z*gU!bKdi`mMdWSf$iiixj-GiX{;kcEI^(I4kUX2%M;Il!olNx%V>C=dn5Q!}xhP?N zzMCzi&t>1pYM9Wfnp2WKo9yAq{t*|cnz%^+2?ae1`0EBAetz1Kon)ycU=_L6@opVE z0sMOM8{KU)aC?P8>kjd@+jxsNpm~We@$=xww$$Vhc>JW_Na&GCzxx}!Q<&YNCyB)& z*Vjf3W<7ajQ6&4F*7ZYwoC>3Ck5)ROkg-< u09^AIXL8)`AAf`SB%I&*HfTkyr0oBMOl znKSdvH}jpl<0I$Uap!Q(5}a5Z#@gS!cz5yYS{sZ4I;7In{)ql7@re!uP@zR2+h08p_Kt@JNLVFT8KipQGTBbvJ&xBZ^%v2Zcl6 zEwphZI7pIjMnbnnA!s}LB(LIO!xS5BVpJUu7)r9&$Bb!XI`%sl1rMb-!*OH%0v%lg z|KJe#r_s4ERhA8!Z=>sAo;KS7%hOFEa6l&G5H+0ycX}WUlMY;a%7+L4sHNpjm`zo? z!AX09oU>Tq=O;Z>_a>Nl`k0f?vM6WiR1MChq9DBJCF;1khe=){7W z0PXu5Tz-p^N`U#41Q~a&f$RJP?R$W!McSC9$|L&8mlXwLwwA1M#>OcJ@P1gRuQ1R6Eur zg3OErouAL`$(MNHDB44yOA|D0&Wr7O@$d==6pH(9jxR}x8dK?`H{h@f*Pn7z;~kh! zE6=mw_TGV?@Os*ZUmhsNWgmL!-g)prFa6jA^Kj8(53RSrv%8eF&|Qo}l@5G&g)b%* zwlt79CoSTaU}r7P=HBa`;1TF%`mUwqRUBd2J) zlp?8HH@mT&!&*8gDYrlnD`7n|?&6Wyw5tueA*Y?H1|BEbu2*&OyOZ}PvF7-e;NV3cwq179YK!&Ez6uh|P>l0!<1M_6y%w<~y2Al+nVX$IKUqXCk3q@5T%obB zgx>Ch3VP-~I4ajMqbW{6t4&pL+EP?`+kXhD5T{7xLr@M?xc_vB`bS~z(y23==@#2| zZjS5@Ax;jj%)!A&29KoCv`^f1&7Ui~gt~?F&JG9$s#R1iRW5$UX7(9IsE7_ufNo-1 zbORg>(llE*pme4D!t66sW+eqJ8GhT6vo$6o;aICoRne}mp=i4h-pV*7^Q~-7PpY8Ro8Av!`(p|#zW_RyhI;KA|ZUP{)3w@kcA*cQGrfldBX zc8Ww)dfQz%5X$Y)%3=EG$~crhEs;eZfig54+V;-HWT~F{~4R!FmiVO116bHhyVZp delta 421 zcmYL^JxD@v5Ww&Ll|96-!K7Yjj}Qq8p+U%oAdr^ITKiaDX(?Hn_CYi#EkQ#_Z@8g` zS{j;!2feLA8loX+6ilL_mS|`Ul+K6I^uPPv{qOE~|0o?@NhJ}|iZku>=|US_%jnSG zLXtxtjl6bg#W^cY&7!0S=c9S5PNJi81-2mtDSGzepjJ4QwL7~+vJlpU3Rzf&1SDYx z;-El4QD7QW*yDJM-!+q}oTVTQ8?b8fB&;(Javb5wS^oW3X->*9&oE8%#* zF=AIB_TNw826~;U*}lt-d>{n%%}RcpN;hbsnh#}Cd}uSCyl6uc8Vnm2dhZi%;~{`Q zWOUzRRCX?z2T$*&(Ph8>!#?508;Yeuz0 zob`s|tPpG6l4SJ+OEk4}tfHDC3JYahG{ak(+G&Vs7au+3@{0k((=Wb|%6@V13+tV9 AQUCw| diff --git a/iOSClient/Supporting Files/es-NI.lproj/Localizable.strings b/iOSClient/Supporting Files/es-NI.lproj/Localizable.strings index 45865112ca9da67b06cc1b3c37fa22e07c3f59ef..34206c870d7842650cee80b1bf53b62ebb899c7e 100644 GIT binary patch delta 2173 zcmcImU2KzO6n;-PO6j()>(*^uH}>tv`G<4JBse9kO9Tvwj6{ivo6ENDr=jacyN!Tk zb6^xr1j0KxBuWg3Ua0{$=}Z)j5bjKfh(jk5TuT9c7vpQ9nCp7ul_Zj)}dR34ZJ`8je z$FS;Q3Q)jA%|Z-)>{itRc&QDJ^SVu-lN%t9!3k47T|s3gVokb&e&mlCGM2iJb!HBp z{D>Ygep&F-Th?a8<=1CvvC=#{?g03WrXw_*C1-LzKu!`-k`U!DC-?6suU>t3_2EMh zt*fMz=l4>B;qdo+^i{5xfH^A|V)>MHwTOP;GfX#`4;cEbWlKp;MfffY&K!MS)785 zMQV2%R?$He=!It#>d8blsW#~FfNI8M%AFjoW{xC>phc;K@sJrtl<`!U2wo>rEwBp` zm^47fKnfhflVBpM%Yk=YDGv01)6SlbPAopNN;q_o3Ue}~(%Z}ZdMwYi z1mmINy5mL#KePe+!tPlYH|`-{79`X6o~Y2bXV-A!b}ABZK^QW0fK)$fk(uq_svj|P z)f=c-JeMt4^u%nTcC0EBcYW@KvMf;Nf}>QaudR2>tXocSPO^}a&N2P((77Q^38^Ii;JoxLq6kM8m(2w8nFB5=>@EC{oVaKl5m3K?@ z{5_xUYAV*2-&ygj+0mrc=@llK?L9B1+vgsBG_#&=R3upGL7%ygs9mPfredBrPle;+ zu<&Iu#$FUDMJLcyOKnDo?nNi{psCP;sGWGf&4iBuFS7Jx$RTY{Pm(Q}2qfEsS)pc# zKq4EiQGM)#%4G@YD5>i8ulIs{Wl}y9ezW}D-1Ij2^z^isH=ZYN7Q*RcbA>MXY{-}< zkKdt>gD(%5=@l_zFv!*3Ok|^x3S_>AObx6C(?2DMJoJ&gA*R z>XRp!%T0GU&&V>p&Wn+4y2F1)j_D!)83kC?6ciYIC$FC_YL~;1&ydHE&Y-|h!jQ?3 z%aF>T2V}c2lrj_n`7oIbAiEeSUH}x$XGjLBDh5IwhUsW&f~E z&yr*k*&d_HIBD8;mpzPbQtU|#l|b9wCO5hXPhWM7F@PB;u$}QbW4p-qZSNVEaZS(q z#i%y-udi-Nx7b;7{g+I1^ozZ6t0Oh>i=5QOFo;x7D?4Y4;O| z0S5k%CBpO!jsfvvqTYaRY+5e(nL4=eM&(Blh_J-OgcyvN7($J?+L96HXfQf(>sx(vaPzLNthAkmqCYL*@%;lNhBJ>{Hu<>Hz~ z?0wWjSN!0kH9p9p=?hRvNBvNg*UN~eYG>52YQwL08MGw;ZrX4fTw>}(+fX^0MRwZm z19yRC9b(%kU%e_!-F~QmB;8pB6?0|_Ewd!mujb_YIjZLN-f8cg32mdG3%rvf@ zvg5{LAKlyu%W%V42mN&boE51IlXm7}vVf+Nm85|3y+%GXehUI=UpWlzqo!jlO&!aI z8*X`Zj<`SOOphLerp4Ib?4sHcF33A)!AnKw;E9DD3=&td^mFIn4KH2`_8SALi_0hC zwX*1495SgoX*uVQ+w1IxY)CaS8*JL6I`M~Kkj|Gt8IGN?<1)7m%ga5uqTEOCo`f7D zm8o8sZCe>|fUj;G&o#7K4O5U4yxT-*Y7-qQlMl`e8eGLsMxaz$hE8q<2OcjoDRG_q zVCh~abnH#$p7kA+(93(k#J3;!VAE$NHGB#-F>qOXtFC}2XsVFv)9_X!h_3A{M33rV z>JinB-%b`6x3f)>Ka#hopW=I)>ZQRBD3oYR^Amd4@(ig6zO~!0B`_y#61Orh#?I1) zrhse5;(z$?W<+m7BNR)XZb+st{0v8OD7u@I-fF>QyHPH48%%g zmKsY&3H~KE5w%ex&P7^tf(meo_oO!Q_2lIf!@3j6mVj9DPvKyT5h%? z;*mMTy4^Tw8@ooa`|y%QDV9`1)!=V;0(5CaUJ-^J&B+|98v+j=pLFA&P3~+4!#(>9 zdtCQ&dh~brFz`}Lx401R;e6!V8Z|P8B)S{|5n7l?qw{d$`|Ro4$g7>&Zno0GN*Hkd j>23L0%d=>AepW4w9Ol9KSbmm!{r?z{|0%p*khSM;WUe9J delta 411 zcmaEJgk#eZjt#F&Hm@=L!@K#1^8(Sy27z3gmqbpeo33|-k!QL>6C(?2DMJoJ&gA$T znv)a0$xL@R&&V>p&Wn+4y2F1)j_D!)83iKL6ciYI84?*P859^wfOIxPDv*^3#5oN4 z40#OcVDU_bT%fofknO@y%233R375$LvWtP@1whe!hGd|sVj$FEm|pjv(SlcjAqAm+ z^7|%fL6F%X_2od7K({gIO@BXyQF5}43dd%z8%~a_X+TqorZ1FXRNtN@!^p#k5RjhC zH=Squ0$IizY}2zOnMAgys4{j<+wQW5(M^g!3G4wkpks1CPMALb8e=pwP;firb;fp) z?c3fnF5{Y>^@~w$^14#K=?2=2T+;)l0*NKR81HRkW?CR^nF{n$3e;(tz@W$gIuaBN p#Xzqmf<2}HavD^$0B8#+81flXrpqfbIc~R;XL`pt{gon9697(-a>4)r diff --git a/iOSClient/Supporting Files/es-PE.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PE.lproj/Localizable.strings index 509628fdedb8bc0f9096d6558ecf824cb9967087..91a96aef5452bf38aa114a0306536e47236fa9c4 100644 GIT binary patch delta 2300 zcmb_dZETZO6n;-vO6j`Nb!)e>b$h!F3<)j^4nLUUm_RftOd^U%GRHc`LbrA8${a46 z_(P|OEbc1DFEBC2kf=BuSP~UbAR0AhQ$B?FF%5|c8XfG95PtZa`?jnk{_uxs-oE#} z_uS_^=bY!ByZ0^Uug9IE7iv{wZE5aiquJPIbQy7@U7fvYT~4zR;o(!{=g}v~KBvnt zjXta zKQzG0UMB}nJx&g_w8}Ns|9!)9)w9IMHS?&Lw+G3^hX<&N_Xa62w+Eg~qXk@A0olVH zeH7%Yb%5){vU05Vs6EdHI95!7e9JZh%P5{6qm`S3R7r83xQ8mUO$4kHa2;ur?1=gO z5S8=3AbBe@zPG@%Q-@<3S-H99VO9%B#?(Z8xptC+ed>Wb{2UEZaZU$5_9w%7CC@^{ z6!P1enh4T-%0%wweLs>bWtPx62Ol3Iud2Orvl>6*R-1~7dGtxD9*H@*{x$McCejhM zfPo2arXe$N!Nh;H{Fq#}l9Z={p4-K@A0jXBSxki-8=`;?&9=Zy?L0({^ALF)Y&rx- zg+KE1-``T>?DS;n-tXv{LNyuQ=I=GO80|W(O(=#WByO~+I}QxoqT_Gwi-wML5M$BkJ09J{mE=@r1#j};W*H8(6eVAO~ z>~#rQuVb%?k#^35)8wBG#cHqn>xyPX#^e3uR3Dd_%6q`Yu?uA9(wC@kuBae^;FE}@ z1xonzG==!FVKUXoZ9etkVBk7?4}W%=%DDaq2*WfY*d^Yrf=UTQ1D%jZ6ckVxCb!Mc z_C4e+O27)z)%IPmkX>8g@15sqVO|W*Thp5(d@+K6YktQG&@@NSfa|h@RN4fk;wR-6 zFXA~1318R2fI$x)u}q}BiT|`;yv1d=jzG9UG^wwCqxW3=#SnRT@)AU{^Aa?(Vi=k^ zdWn8gt9(A?d2<0Tn@07b4>@NEZjvW9;#}2u$fM4cmZ_(Y1(fM`sQVgxOGE@>(}mDl zbcV%e1T!;A$~x9o*|@8ZiXK^NYy|Q7Mm=;aBY7IN^aN5u&lE1I6+aVJJJu8F!dUL5 zypjfyOhWDWz`rtsxEzR4%qNk$C~VN=$t5)U0pbHE5CYxT&lm+B|!NRV-rqY465wX zTbg*sNh;uYHwD5O(3w?9rkof#cW62P2K>!~US8WvRcdmTqvT<2-wA7JqKiOKiYxCv zexnYgR_`VsEmG}AN(N1!TBX|r(vk-W=UPL@SoXE+Q=~5;e8~=JeVOKCwQ4DtWf8|H zrco16pCrUWkv!js`6Ob-d6eS10o{TrITY0@bft)GE6B&^^K4q$p_ST^km`k{Y?Jn3 zb>U9+=;T6Pe4Yvi#YY=pBz-#UUSw=Gx_^EjMG2Twe(Y$u!mg1HB0& z3*iPWHxu=0#{5>yi@c@hNLSD~HM*+7$qL$1Z>EH@CA%?53^)a<`Q7q42{;f})qMFb z^q}zLfm>+pluvCu>16i_a`J|QXiZ1!$dLm~wZFf>DR-F^DE0C*ygxdo zTo_7$$}<`A7}CMgAZ^7!Rsm2nA81S}kXOv00~E=go_~l@d~(87q3H|$Gx7*4Fr=XC zov!$nQB+U?Xa~rMa-e3Q3mNpLKU4&2v{K>P>~O=$ku?oyaMAR9Sw`vU7bF;YwkOCi z@-RX~w*y80vu$@%Wt=x{yVV{>H!1ccu)EzRH@XQ=UvP~vfEg&T{rfe>c9HFC-ZL)a zn!YXo=<%#yj53qYB=Su+kYnVSK4TIi&-7Wp81HTW#>BKh+B_BLy%eaMGJ#=`0d!p= j5ElcznF#iz0?18J(E^~mK|zwg{i7U{A=C5+icC!aX9aOe diff --git a/iOSClient/Supporting Files/es-PR.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PR.lproj/Localizable.strings index e6242e8957ddb00573b2240febdf29742ff423b0..8287da2f58f65ece8234e0866f3dd94c40286a66 100644 GIT binary patch delta 2213 zcmcIlYiv|i5I$4rvTob$df5xR-EMokEkzJ3AQ~R>C<(97@X$nvV(NBx+iuvGyWLiZ z0TB}oiaZ9Ggs4%|h*60&G+9C*NYsDE(30>{p!kT!AW<}iA0|3;_R+!*|C#3Ap1m_? zX1;H}GiR{he(MYSv0vukx;eGj^rb-`y1|LJ9$8WzRACjS>Q*qTdsI?cY74)!Dy=LW zx;^pu$Q+v!0(AXZSXi-2#Z^#ssffxPzrOT{NuJN4^7!73`?t{>#H($W+FG&aQG>2| z!9#02;J_Dan`qDrldAHuLmW4vf;jY=NgEe{L0j2|<99WoHOWRNz2L(AI}A#AApmKD zRnS;6Ovpz_Q|oFl=>^W9am=k_9Pb7ldNaK|1Kib}Tx2hUNUNyoQ4xxJp|L!~A5u89 zq`nkz90$bp3-htc<81FGcoU}RNQHX!R~5-!^}-Y=LaW5T4_4+{d~nHzD^I)Vsl_mp z*6jfUJ0^SZ-0Lm)+_2-&TTt1UVX!fNLR^Q%Rar{Dq`5kIx5yt31%0`i0PH$MD^Gxn zKAH_~{O#(bJf1s)P?_sL0n2N!cZCxNQwAL#1~+-m!{ZY=IYv^&a~saXt8N@==r#LP zH)EEJ?VPilGp1D+y*C^DT#_AMnr}13XpI`#J)yerazg`-54iE%Ob3qqYQxkS8#V=8 zxG3Pkt!o`-hIdHkBJADHVf*;mi~|*>u=(I>ZAoc(qkaNd`{|5@(y;ZC$(@l>6KR*RD!I0)`4$&+DVqQMZs1*oUk90>Xf$T zV}(@|&-T|-YA4v~9Y>jyE*t}+D$d&@dOIi7TfO(}Vq;WSlcC?gXZ12p7o>9+e}bb9 zY&-3wS8hYINXSiJ-G*zp+%T}_{brgQ24{JO>&?>DP0&b-TrBo0wRPA$&y97a9ov=~ zerdD#BpGE$vnF`L8gDl(4uiR!TRaAIC%eYjK{PmDZDQzC)DqUBtmIjz6(m80qGCH@ zTD7CpMcJMyke)9;m$M*L8V7#x)ASYK@)x)2&m2}%mmfo{X#8ikAOE^I7t^(F*<2k~ z9ekEjti+`FahQn|SOK1fVwjStz>b{gsC1zeYWQpoWNDxeCgWr04c`V$VxjyH=*&*=O(^`phqGpdI0)$v zsUYpU0_L;`a~@C&l!M>xymM4n?Y!#7Okmf@ zG<@Qyd%LjK#b^Y}R?ax)R=O)qMi*VURWuBX?fHgfxHwC#ZAh4P-{z-j9hA&P@HI|XPIk=1&USz*(E?an;~_&^=C#& z9X+6c3qvVTc_sryYYA9eF;Kh!D4Gv6CY7NW2z3~yH~eR`;8kEqL8zbnu}NAGWF1I- zIZ!3gK@57+KTctkoNS}QvDxQ_ljHPzyBXQ0Pm*L*+nyuC$ioQdOHJ3?$H=pNku2j4 zw&@bWOd{LUR2jRbZFk$l=qAOV1a_<&&|Nto7ffGxjWL=TD7c;ZI%B)Y_8spTmvK!# z*TOdaKma4l^s--!GSlzf0b1msz$h?1U@9Ze^ku&o?`>ydW?CR^nF{n=3e=IAz!1m) uIyVuBi-Dd@1ba~d=+z>qXaUedP=Mq!q)b;-WOCf@AkXxUarzrYrX~PbNp+O~ diff --git a/iOSClient/Supporting Files/es-PY.lproj/Localizable.strings b/iOSClient/Supporting Files/es-PY.lproj/Localizable.strings index 90a4bdd075ced7d8c42690aad2a113f8e3b777a1..fb77f53082e7679010136118bb4ee016b3d91198 100644 GIT binary patch delta 2213 zcmcIlYiLtv82;X9V@%UDjXB!braeuy-nt@P2VUl-1+Aj2mi<_N)OBssw3%&^C5@$H zUWVYLW4igezA)xc5e8!yrH+(Mkuv=wgXsk6oJbYj4RmWa#sW}>!<(!_J z@4YUjlyXsb5Dmn6Z!*PR_eg&?P6D>#gVs(RSKAQFz65$*66pCG5Ry@PMu6gtHD+tNHFxQExzI69>c*BPi z6}ycA)x+eaYa93N;jRhQO=oK%Ah>Y<8oMEj=JX;PZ8@NB*zP*-fzD{)P6I!;LIINdP=4V%Xe9`V>Q zhQyYktApUc^JONQHEvqguChX z4Jf15Ti|Ij*~cl}%Nn9cF5{1?FiR5QXBotRc1I6C^|L^h7Sc-EMs-Az@9nCe>i>Xw zbmc9mDvaw@t5!%w@Y?Be+J68X)N%>j^urnO7W8p?mrmz~I@SL~7B5CLnk2zJwlb6T z&}p~Llm7EZ_`r#+7uItQgrrbkKAu#swq-XFBb7(IIh#l9cR%X9gZDd=@ zlUzHrk))`olpRa2Ht@815nfMbbhiM=@I-Z)zW|E!FNulZiqW~7veoOkdM~f0c)gb+ zr^prS6U`gcOYpzyjQS z-COd47BQOPP|ayFSouFlU#qw#0h+oA-hk*U0`RHHI!j=L_e|zPK;ImI zk~x_lxFKcaOMr;}lxnAwH^Ere#hM4zTIJ+>8)r`Ys*Rt+nf({#Gx^$w;78N22R3ON ziAk~>Qg*y`QbuuLVi|UP;@c;xbu$~`@+x6$`+E??D5vYe7bd9))B&0XjxmU1}EgfW3}pzbN`cBV86a9qz}kv|;srp_e4sI@48=gG!!Uiqe?|*l1%?!a`pJyV(t;rCKW)x)8Fl8WShQ7l2L7YjSM3XBb+Za-EJQv&-P8Sj5pY( zYX~!mY%fz~?3%VcY!9QG6n_%fv2H+j<$zo;ed9I8Xl9_`cH!%c?IPPxyk}g-HThl( z+w=-Mf29}*ZJn=vjEyG8tpz%WGpfy}avIT`C{Hwfqi z2|;HJ!`;A9qGR|&6Eh=Yh9+vvG=L$I2$3isA__Cn9}<5IeqrKs-gjLGCdQcNZEx>A z_nhaP=Q-#7@B_!h*N&q%YB~BK+4Yi|fPUztNAG>qqu;2F>Y*A32Yw(AzjTHiMcY+e z^{Rw2)J7FkTk$WUI#kct)!JM)MfAD9mJSZMb2j6wUA3v?*!9MD{k-o4xyD|9Ca_bl z`_iSCS2@S*7aJS0dQ_ZOme4%@rkI?1S7{Y5E1>{Koi?97RORGbA@cE|6XX^HmwxRP zm%h;-;Njck;JOlW7r+64v?5R^o*vc411BhGx}yX)KTehZFiya*3+Oh_G&b`sJLSt* zbJH#I<=>Iw9T&*HTgt_n`j5Na|K&nl8G}o_&vF<*zQ_ zYy1ioR*Iy0%^()NQeC0A3!3jlB-EVq-yuKmJxre2NiYz@&YBML-W6%|00r~A%^a=! zt*S#8R0gFQeD)7u{QNi>dC($0!Xsjseu8&h!eNIAAj##|$zLD|Z80%QbTFP;`8$2& z?xU}K7>!!xSW}#bd#F2W7Sw#WRik*xsq(ridQdnf*vNj(-17VX>Tv!mqN|+EaB=xyt{uNVBR0D2z zVt+cd0f_mxH44@Cs9O5@#Fyie_a}SYx$I|N8Si`q%>B zPQlrZf=EJwb~+$dGxeP`@nw3!(!Vu2laP|3R>Mcj3=i{6qz0IYVSf`WMBp-LfYK_M zidO@YNGz@2G+T0IdN|-*Qa~T*(UJ0;Bnl*sO$E_l zI6R1&8iJNuiZ?|{lY}AElu=i4h=%X`?mgcO_$+L$VuV#p{Syzqn0DZe1y2?=sstVX2K*HH0*ul=)v0<^Mwxi_ zoOd{1Yj;5m*Zf(7n_hL6J)_!HqiR*nDm(n+iUWSC{urE;S_#JRt_`pE)4m?EtL2wr_4z69|m$1P}?QFM&qQAgQeOPV<(Yuysh(?28&;1bsJ3J^V7YW04 zJJyYQ$X9BsMblGE>D^#pQ)Lj3oOaW)esJOZnjj5)2(IZ_&N;=OIHzNBa;B1<3@0h? zYI*2Fku97H#NhD0l3q69%xTYO;KhrEsWZd3+MT(RA3)udvKDsgWC9op_nh+6luJ-w zGPa3u&YK&!1l>N&N4NNU)n(3dXf`K`1z%CuC6~B!}u{h?# zi}M{+-wt*fxCeHe5p(0xSa4b!gEp%)183P&+9HkF$RQ;EHauSDM>Aljo1@@}PBak) zvD_yOxY9^7>xK=pF8_yRb~dYJ7LL&~^D-7p)k zMyAsJ9pJ!p+KHy$PD>lWzFagV0^teP9nb0{<6OV6Cut;jXIvS=jO;QA7X7rR4=TM2 zRRhyCQ!Qh8%8EY(9-DW8^dY6SE9`D&Pm}$!{8)@P@=6rm$xjEb_&0SSc%OcNqn0y3 zijO8#q@6^b0w+*eu2vM4;I|~)!)r+}g=LXYl@-#u4#pEoSh?b@=R)Xo^5xbU!F1H1 zWerfg=)dzK$>_TV-+Mb$Hh~cAiukd=+?d4jP#OBF0;=#xxeEs_JN$Z!9c9?9JBXz# z{i-T5RU6$Jg-WW=bEhL$Ty%PbySFXk5U`4bYU24xGo_v4E3jU^dJmQR1(@|UcAz=s zIpE((>BGN*f4R<0-Muga^L6&{3*3whm!zjm&0`?xuYC8>*+mfY_UMjUX>ZlU&%>@W z;q~K6YT{jCMJCL;)kMq*4U**PVkl0(hri{vksGdXvG8DP%YsFQ);ee0okk`Vnc z9UFwoqGnZ%gZD#Nyw8nI3j-qSs;4xL#O&qNMRO9tx=TASo}c@&E@3Qgoa2hERMD!} zNhP&Bh|8-MK27NgGAuOl9-jfRY~eU^Yi3zcrdq>z@d*gC$7I^X=yBDi%oPmR!4AS} zv&E-QMnEprk4`JiGI+xZJDEui@JuVTY6p1aQ6^2$?*{X9c zD8eGm%(O~75(o%-ionM@0Fqlz)!68^;0YTlDu(d4&G@Ba-? JrzYEW{|z=|IUxW5 delta 401 zcmZoU%5iE5$A(uXo7b5B;obbhd79|t1qZk`&x)K;H@QYefW4F<2MFUQGv2hGyg;67 zdcc210p&!7B8Fs!42DdGGKN$J1qL+*1qNS+M21Qry97vQGo(%r{LCn+uLl%xVJHPE z&t%ABNC!)Uv=swc1whe!pfRaHUNM6XP$YZ$#6yhY(>MHQl;c%kNI~eE{I*G2PyuK# zNMAWn2gpr&(;43}N^W+z;pNDfHl1+>qww@~GK>=2Gh`Tf7?DJHrprBKz?mhlGL zbPX;hk?l#Uj7z3%kJ-cMCdHlvcC_2%MmOQ^}vdKZ~HE1whw>LL;9cW%_z~CdcUkJ&Y{dqvV-hF-~VvVrl{a5*Krz diff --git a/iOSClient/Supporting Files/es.lproj/Localizable.strings b/iOSClient/Supporting Files/es.lproj/Localizable.strings index efe4c411add2452d796a1b6e8465a8fbac257286..99d54df1ebe4e142dbea737056cc7ac49a71ac86 100644 GIT binary patch delta 3211 zcmcImYiyHM7(S@&-b18 zyqD*F-{(Ek7Xu&s5jZ)#T5VX>S~V#8#kk0boJgtuRpIz6SdR%uB=DOS9by#gJw_&p zRaPV+IU-Ubk0)hh9LQv_*N-y^{EkANvV+(kHY>Y-z-Z11M{T{`FgvjNy;|9(k5|Yp zeJCZX)!Xr)+B8%*Te0a{W6vg|(VlR`1azm3CstDqT1Ox;ZnO_$6^F#I@ga^iBY^yD z`;Ik-^`}!%UL6b0p1qQII=UT#DX2<8V8}R2*T)bT)gS~t^N?JkKRzmB`od-DUvuvR zaiEZd_-HXcH{uM`B=9XqHSOxuk!p1#*r*N#{PS3*FhPPMV_D|3+$NXY=L8L>VS1!^ zLLEIHqxci^dCKfDSeuhlwo4|}<@L)V^Bv9rnFAJ+lRryiXdc;OLMYp!S31(KhokaV zbOP4z3%HW?Erk!;sc6)`|{awXE7F(VIx@}4}E9Y?I^wEHRzQb3& zBwqwBjyPC*4(gHOtcjfHB_VD)7mg8;NN>%SiqcWpbbVbyqR=>Iy zZJcgvRtL8RLwSg@Bv=%E&_=xXsqRd;cZ>O&HOP1}#LCDR|2%Klw>j*kf#V?da(LV( zF@fsnCo-f@PRUyR`DWm?Q3rZ$+0*=f^iUE<*$^ZFe#@Ii%)vpHo^4CEhXW%SlePh~ zGic<}NX+pm*;GZrG<+BaBkiw`(9omzGBl4$6@8~6WSvMDr%L>!`gb*KA)Bxs_rML8 zsp61hyu^^z#4z06&cG8-a!y#f|FmqW$(n=KX|P8|hNqVW`mDp{C21*@sCeP)SE-0xeepZg8fefk@u>`lM#x{57Y zq;{TYR98;atz8deZQ@xdCH>H;G7`iab5Fi!{SKKB$@q>~>P$zLsztvvPygkg>^4HS zSU5uLsGt}r*@KliFE2HZKHU;?Q^8s&Qm9xfc0zx<*obE4ihpb~5-*W;CX0UYh@Azl z8atm@lB{<+#9}1K?t2z*c7_cYm7a-iymOY6<3SDVRO-uPCvPUggG1T#?>ae3{&++O=MvL zk~!7)Yvd(c5YLz(S^TaBBLQDXwC0+CHRG7SKpe3TGD(ruy?@GR6_MlNlo;31A7s;{ z)!YZ%20&R^v?AFcW7c!atqpeg%;%eT)~1Jq3Nx^MAOC{RTnB5)9280Q+VZfza8)+z z;Ab+ZSN6z27so;lwPPKB49Z!Q{B>_AZfz8aO@Dh;h9*4$b_PKxT}|*#>L-rmKoSr6 zp>!9(nx|8Sd|V^mHOvkUJJeZyrK=Utq}DvTE%PvZwrW@w%ds&Am`O}zxfGnS9M`EI zTr6AE+1qtG=J*z?JGTN|JqAQN<+3I3RCdzxE0G}86BR>Xs_*OafXU6AB_smOH?vNO zgODwEw!GQ%CSZ>kDCyoH^Jo%B^g^ERdWJzOdTDvv~MOJ-WU_F87zmw-b&ww$c_<1IfUO;W9JRWlvO}NA`G@S wY2M=@R@jh{b4%E!g$$D}T#fm;rIq~3kq{eO zqfs{-EEQpGL3Xq7N_HgYH|7t}t(%Uw%%pExR6h2IcSFIuF;K)!fg%PC_jIkUvr+N{ zSd_hxPiJw8QL1Is*&+*3j8YY+R|e&Sq{%u&N|5Rxi)FJ2>2kVM!mN>c3;2@5OT*U8cg9)wZ>M#dh9{SG$+lnf3~*VytAS!nAt2wB+En;XLFRvTg8Y>UQeDo!;@u`tId-V Sz>#*|Z-rX)LMJh7ghk z6)-{q>_kpN5H&G^hJ-DPDPJZ)hU^EyVEmy((I03uQOI?e0mvvADvs=p%Q99W$>+Z5SyR2<8VU^w>CJjuhzgd-A>$c z+*LXiqk2_R^^Lx{tjq&7_+T1Kp&s$JzqPd4=jX~T!$9$mQk`c8D$mXE&h z>AuxPeJVpu1}vguUU1RR*I+)4d!gLR~&9Vj)|rCYE^w)X@V~q=@oFk*U2wG5rQ9vX6@kFBpwNb zFji5iDdOUkJAdl~ST$#M2M1@kI)=cBCxi6y7zF64tFVgOr)To7T!lRWJZlaFcc|^$ zCP^CSwA&fBS8c;YU6qLG3>PLv_o+z^PjkqKSxu8ZsKnTM2WBri@V;rR%&06=PjdD& zQ?F5t%EkA%>SFwMo{RIS)j;V$oU=(GU0Bu`A;GV%?u{cXO zBpO0&8#moefsmDN>|zg1V#!TKcmxsRgj*#eIp2BSE~b+QQwD#0(g47xwa9?i0R z(mxTPjXM-=39~>hYCZ^d+I(Rs{CHo-% z;0rk8!qe=Y(|2GYF24|<$vbcxzbp1(>{vOzdcwdHpE&TwA5I#+1a@3{*^O7C3+en7 zaMPdHz`#jw1=g21D7_IJIH$?ILVBCioLZTEKUX1n+opBrw;6kHO8<9$4Glh_wsNft zRE*szPjWSDLkXg+5~Lhgo6=Y>s{-y|lfS~TKxh9-Vb zaYTP72c+$jqSB&1=PDR|j2Lh7HC+pN-XlRGj? z?-iWzR$=TVCuKLXy#HN4p=KRCmVbL{{j+E2N6|(Img*_q8*CrdOJDuQ=hM2IzMy{P z^>Q|3m~@h#b9gyAugFwg0*%vFux3Qyy-Fj|&H&*-Ibs=fw(jNPmZ zPY+tc$sSKim;N0W>kfE|;;Nq}Iw3?G4uK2bUh9l()}p6*CGSb~Fv}-ykpG?Zbv=aX z-A?e$*6&P#y@ewo|FZ?hwQwDxifljY^b?o2>Ges~At-v4$=u}IWBE{?C7ab)E20;B ze)5+{9?|&`+!P8!9VUJW6SvPl(^S5zu&9O>IO&9~5b zOCgiTdCO7MW^2lS4z{~MYizdVOJp-zSaJR&m})g&HEIKo$%$z4{eMY&uE=)iFUqkk ArvLx| delta 399 zcmZo#$#J5CW5XKb&3PtFe47tA<%v$x<=Si&ai(&*Uj`!|dm2M2Lk>gEWW!6ElRpT^ zO#UFvrJc!;$B@pTz>vsL#E{I8!H~&N#*hl+t0^cj_yR>Mf$S0>oz0LsJ@F}{q`n?d zzy)Yf5l|^aYYA9eF;Kh!D4Gv6CY7NW2z7uW+0!>}VHBUvbDdFWI>$#w9$p286olr< z{SDHBAiF`D%YoW}u4B-f%&4rmS>{rkEo&Okw4%uu|4L53AjHVGy+Mdkj1eI?d7c>0 z_I<*P>)57S{9zQ?9wE(mZqoLeb&NJr{7GPky8+#p19HvuvnLp%nSp|n1rG>scRI^buLQ@}E!NxD%7FTOQN23zO<1j2R3CpyWW}&>L<;YWXcI&D-X(g(UePNY zb#!5LwS%=RR(l|m!g~n&*b_TNTJ)PQyT8v^%9$PNXGIPYLy*Ypr=zlQ;)TWiHL_V> z_Q?(vey6%>gBXC(J_<~Hx%%&VHGVvx{9n{Au=aA;7=RTM2|Q&*65{)kf(Cto+|X5f$xEG6_)R{ zZzJ@yuu{k@BeS=~x}_u~-+IfZ@{xEY3t8Z`uOvGK)lgqeZ=%x*D#dyk6}@7%4CwxZ z{Izu+KpVMVMbzaP4TX0d={%tQT85QBGfxc;hxBC7=bx4{ZIBarO6hBdWILnK7Zbjk zIr$={NubI$bp%&jjdp=YT}?F>zHXCSYtVK3i-6A4S6`QPkF)dH&AqmgFffEh8Nz$F z+1Z8vEL$2<8^ju{8O^3c8jlCmZz}@2{k#nL9WmD_ zEIBVTjgLNq!uN=Muwqe4!zTUaV1EE#47@l~`ub0DZR`CMo)=rh#=^j5*%nowG>t|_ zFqn2EyD=WDeopkM@AoDMRby9a*YS4ke5enbnslyN&Q=#<0d*wePo%KH;UOp3nPV%s zD|z+D_GmN@Gwe-vXE&r+;N7ZYepF4q724bjc^1NxVTMM=*}%H$gR^|~ z8`$jB_aFeR;Gs`y5U5-I-DVGxrY-cxPsy1xZbv>>IQ*Bq5z;{|YxIGea;`peQ(jSn zZ~N8y+8TA?WWB0B6<%tob{#B{3mHurGvgP_@1TJ=8hyo5QfjWF!~5kdJ$@DD{jK<# zJS~G!3fsQnnPbndyxx=l{EJA#rjj$8NA6^i-#p#Qp+ebJoGtl zbt zSX5nlFgTT*l2faGjz4S5zxs}17zx-z-cJRE-0YVEO zPe*ne-}Vya6}?DQRE>^#0_<{T>)nEiU5|Bg8tX$UDqS}4P3^Ry64y!To@X+2FAMA| z?=Ck8KWyp|k*z_@)(jh9zP^4$Hpq6}Sqp-R{1eu-4Zet)y*#vlH#YirX^y;Wk7lki zX|gl{FvO#7UW@80zstxNi{1x^M24qyc_8Hznm5UK(@e?IQGw2O#AevAv~N>sgJnwX zFtF{$7l)FJ%pi)uwj)j=>&}=hk43t4_FdgDH zMIHDv*u}2l=<)1izLt0_8`vH99O4Ld#`i5((3_eg?G(r4t{E~GmqkJ1&gabJf|kH&qA^ue<-tRIT|!rT5w Mhq#Wbgzuey0f`a3=l}o! delta 640 zcmZuuO-NKx6h7ywGvnVl>5N8=c|~y`$1$OTS@a{C6i8SYv~V-_B=bFe@@9r)Xk)7u zErLtjjf;wEZ4C6bgNxc0v=AYXS{2elT9ruOy^0{jz`6O*zq7s#C04gh}pM|c`Dk2-KLPk?k&VG_6tmmz zeLt$s#Ug66qecJj<^jFm%RA~tqP{LmQlGfY9R^N@=|0Xz_1%m}hUW))CVu@Ulop|h zGQ>rz>)Dfes-y{^<~adwN_H=43|;c^5C``+XdH1ss;hsK;h#KjJPkWPvU6J1#v*!` zMSIFLei=*;j$N=kC`tW}In`LQqJZ_AZ+z{D()(Wu-J#p)wCRu=wteSJD)jbvr%9c8 z6472%NcCtWu4^y(lztl(AHtt);&nQIF#IAsJOr_XmYd<5Q7$9jgD+`^dVm_A0qT5m W)aM^vdTmIY4YNby7wgSo@%SG!U7ULW diff --git a/iOSClient/Supporting Files/fa.lproj/Localizable.strings b/iOSClient/Supporting Files/fa.lproj/Localizable.strings index 9a2e066999aaa8bbf2d5661f0619199b85e47a9e..ec83359d32795eafbd15d89d870f9c76b88115aa 100644 GIT binary patch delta 2569 zcmcImYiv_>6#t*nUAnI8+I1`23fpBoCUGompoq9Y6=5T-n6kYvR;hwRE2WaHxpCrk0W^5RkbsaAlH<|(dIHC04&mQYE7{D224 zd>3YFZ0@XSLak;Z1y?oHY> zNA}q1pKIWN6b*Z!91o}6w5}VRv~UAVBv+ozJtf7CgprUgax0fo-XwK^q^gq--ToZ{ z>47qcA-b9!7^$qlL8DA#$C43G+V&|t>A`hR*~v2pT!l-F#YT;B)@U-GH%vT|EKd)7 z1s~@ZMA&VDnKV@^?mkpXH%>tbMNY#L<1>=c=cHsUVT)JHk6lrctIeJ~k++-%@=(?3@T?_mWIrp6d{ zG6iicYvH#cjx0}c?NIuqlY)yOKnq8}**I3tc_}StZIkb^8JHX+$ufx%VOf$t8Y;q~ z3FO+ylW?ya9CYtdFlpA8;HI6Mprla3rWjf;tA}XW8K{B?Za?RvNsalgk0j9D|oIxYAGO1u7v+;}NaNndQ?r7$bspo1&G=(o6+d9?OfO5+W2QbMwT2=kk zVyrJdA6Lzs^)$7R$3^bsTCTgdl}jl6YDW9DiHvT}O_UWQ$S@f^ZS@y z2H{L@I~IV67vhz)?IJj^F6engrt`m<6iC)4)lACa;VT$J98M0D;_^aAo=j|#AP*+d z8{crzC!PY=I38?V|AKE^RJGxXih$iz(@ng2{xPf$l!#SPnBli@t{n`RyIt&keEJlf zg7PZa3T95m_rj2@m8hg94U==|-cm*vV%%|qH^B(3)T(h-6eqQ8Dpi)>wRF24%6+|> zL~F&r#wmtXEw&sE)aM#(6~C3#Wg2s*59{%ps2)3gc+#?0A&@_Mk`nmAmoGX(+)VQJ z!3297*I?k!GWx3xJXCTl)7rZ(F;MZphW$xNWoO7J%F5vwIOEc^hc#p^k&S6d1a-T< zk1D~Q36AwkeEp)IYkrYM*t?t*DF$JxW$ZhYv*AApR?n?se<9V$8q&kOo%p-WB1}h@ z(aUyZcBI5&bRn>u^RTmN46lAtlF_vv4+!+Umr_I#RSm@0eUWTXdf!A1YeU3zAOy*OLRueI@H zRa)L!nIBg&wnQViR3WNc_N4sVCio z1YTiE&!tci754Bh@#Ek@#e=964@IbW=p})Qhy)K#R>i}cH}iip!~6eAi|UcBuCBL> z=%JT-<}u*pI;runP z&hYg8OLXDfXlTz`fzyKCv+4!*5mLB8C@FPbc#;&(sU2KuQ~;HjkohQ)F; zNI8wAy*Up11w4_FOZ@7k^fmf}w_bUFi~oN z#Ax6O;wu~z&_r&$@IodtRg42-Lc#@`i)0#qP;fynbW9~&n8o;<_gl9L(F@aj?fZS_ zoag*K=X@PI?Y#Sm^V~D7y5!;Nf?gF=!z!kC#C;8Q%2I8(>eTZprcyZd;5VvPs3Dwp z8_h6JJVLcOLDi!tqBZ{gMlk{LJdjGL2-L$WdA_~1twdk?BbQGdp%T6QPfH&@=`MO& z4FIUmXrwlO=cD%XJDywhGXLc#x87RsIzK&C-Olb&D%ICpJ-n+Nk7qaL^6WBl^4nf= z@z5kK=ff8%-)M11uEVQa=Dd3LjEhSyQ;BG~_0D5;`h?Tg(Pxi%^yYAdvCm1Uu%6r( zSdcCqLJ}4N4h-p2wz?^|Fd@1KI;kg@R`T>g zYSyJqPTg|dU7v)YA6LlqEr{IYNyeaA;gcSGh(0~3541UX=W1%wEn8eBHAkbBUOq}g zRmePncm`k`)Lnxpc3(hZbA6Dc5KYLQnLEcw~}l`0khF z;VV;AS|+^^E=1w9A8yeh9_b+8x{yJ(DJa7RTSO~!WI??b-$Ten?*0*pu%)zoN!&cO zfqPU$-)yVoA8un*BqDCgca-qw=g6}b)W%2fC4>jD@56y+L^Qd$jmnpbR+ zO`aOziYUcUip3iA;0b!itGmdTBP>}pSAJ-{dI3Q=pd4)iVAvWv~~S zJz|s5vZAcw>51Z+Jg`J1_~sbcIed*geiRw_<|#bN;t-D!oRd7#NtFebYP2kjUGM^x z^NnxGqYqBGc{<18Ew|^W3)bD3&p3D*6MI1~qdJaVqsm$ve?bOcQa{-h5oy+c`N+LR zRXlT*D)<*BmmYt@xu(-tM`0_tD}g4okYD?KWTQ(hq9zuLf4fRPTB*Mo^YivGs(4h^ zqfA$iv5J@p%h=Ap=wNN{GVx*e)oAE7DPO(#lK!tkB&qox+dU8Ai6A%gp+c(EqjPwV zeYMOmKCs0QQ)ssru?sU~qsTnUW&$VluQwVFW#=JiB9U(NnR7(F1oTX%_M0~brV01( zHZb|v`!cgX=Jy+9&9K`ByigXx3X_M0-Uab|kFqwySrmX$+;CP=#Z`vpcHt##p^8}& zhR%`}=0)FmueLCWW{9h1KHZL0-FqBw?Gr!aHTL^QScmIR;&ksamF56TZ#(77kC_zG z8}87X{=xw>Rj3S)d`30v#E&dg@hIiW3{^Lt_L)dCWUxymV!l(`Q~KE= h#|-g-d~%z6#1}X5;~$ONQGTDtwg1y2uP$~R{~PcjtyQWn|Vx)_%`=BUlW}?PnT;)2EDh3D3}h7mMe~8iqyl-x3_3uO?CJmaGKx>v`^_lFtH6+g&^MW}Sz1s5 zXfQ}$IZy}CNep_E9RQ8M&tOePfiF+>^*RIpBcEbh~eix3*9F$2dvaG8O2p6sWT@fmUY#9heBj x#X#>Rf<31IvL7m10JH@Z6!{D(lLZUJr|*+t(wf%J$g=&E6w@ol=|!?kO#s<#afSc@ diff --git a/iOSClient/Supporting Files/fo.lproj/Localizable.strings b/iOSClient/Supporting Files/fo.lproj/Localizable.strings index d85077dff8039edf22eac113fb6aa81ff61e5b17..d8286f9f40ab18849ffea32b05081fee6a68f153 100644 GIT binary patch delta 2270 zcmbVNYiv|i5I)nE?Xumjx7*z=?QYxKTB<0vR)VA=v^2)3k%x&vf0&eQw~wW}-MR}j zZ3t}@!Xt=^XDhoB8IOxkH=ncaPc!JPUBVDu89b7<6tn7&zyd+Vr3bVSS`9vrEO)dKJgdhP`#`btY-UxU4g8ZRa6C4i)vDd!9QwSyihaPxVonc1E-vWOUKqO z!O$W1o?Hj*KLIY9pA8Ot-5)^XLp!;Kz(%K!v!ArZ9=u!X!0rhL-E)AG4xZo_8$0on zEu}b+X`?mSYeTi}6a zTz)2hhE?6-Cp17hd!d}Y>>(BHXds1>WuZhFYXgl9fu9a%gDrX01$LOs^9ut`+i3h- z$iRIkUAXP42aTAC#x^H~vN(mJ90%HxE_$#IOsd@po<|c*U4)-TwT?3g@*_KOwL!lt z6#vn(+u%=L$^+LP+RTK}{&MhOe_fuAI<4GHes>&RD9CE&YcZ||yTK~Em%jQEJoM68 zcs_ITB)Ro0^m%Zpxxw41T2)vB*E4i01IJYh<+xz3|BdM*Gj+tq?WzSAnI+g@da!q^ z1N&dKVeh-PoG?coX5e;q5@GNdmQ?z1?Q(}V!S~~wM-!jb^JynPZCDlX;+hGgWag=4 zu20$4FKj!hdYi zy>yQR^>D}`D>W=}`e_u>?06>{!2a!ajMN&H|BX2bN_r%U(Y^UuGdQEqhryez>Et$H z4e=nWNryuEZ3NtYZaa=f@3I3K6ETiA%aE;qzbhL;*s0PoIDPIlj zv4hEu>3g!4Tw63lLTf4AAA$U03nXPI>~kCGKIBRCFF<(dQ~{l9g)-W7jR!do=nTB) zb7arwU_}Y?zl&b0gnT;k9=KsXj;$;t_t)UAky^;e6ETOhVOTen_02IEG&m^}Ycff2 zR`s+69*TQu{6)x^C5%2rAIHI$-lQsU?4A$D{TXN*aZ%atJR&bd?9}@`*nA?VPUcK1 zE)&8Uwe9%Rm3cinP`xYS4r=KM!!3FpR^1H3sc4HhubO!k;kac;Cv^0wmJrKk#*yq2 ztQb?RWPgn_JUphEH(6~#-fvT89lLAi3sSNsi?wN}Dd}3}l$uESCx-(&mz1~%(Em|! zPJ%-es0vl87AuD;MHtDYr?+r{iq7+AcX+@-KX>zo@s$(&1!=7WBc0t+Ul4Nb@h+oI!c*Kd)j5NGm!d$R8@s7k{?p_9h{ ogvaMeA6ky;IY3h_;ipus;60g+CcmXur!W`&FH9ApW03a#-*=Zt<^TWy delta 424 zcmYL_ze_?<6vxkbmWt@BmoE#IcrCSr4K5i372y`y)D~H4TBf2_2!Z&BptT2`C8(jX zK`^*$OSJ{9{gj3V2@CoMI`3h)+!StuxM(pWB-r9 zBoftDSF4gH8BJ+b*3uPInw3!zd(Vej7|eqoJk+m>Gc$w2aKS0AZ#0WJGZ%=9bxu6n z3`Vi5+$Qc^4B{aJ*zoieunxXh-!fV3o@c-nuhP={o53z_z8S=y_RzP>^UjpxwVmJ` zJQ?o~Oo}V*$~q;1Q;Jm2VDh^siwd$UO6y?l;YnW?Lw4)nl~3lB@}6m;Wrq`Q7@%drXaF7e`!z5v zv%3m5O~RfdM8PjCFcXt|h>@D3U?!X~XF!~L*M>FyP%6p4G1rNwz6Wm6__WoPCR{0{ zhkm>19_0(S1=Zq=RtFC)$B~^Rx9glKvvDUW<4AgO*BCK*{##@rvI|U^<1fL|#W|nF z?~YVpV}yc2k+G>^Epz)r*z3XvXR2D};B~kI1#C~Fob5(O8x&*z(_pP_rIijMp@m3T z%S1?-4dj!i#OY0=_dNgn6+3?eGv53QTmp=|O_{oMK16XrANVnN3&OBS47^s=w=fjo z@@Xd(?PPFNDvC+4&G?-&FkO80W4{Q#Zo{*;Ad=tYxPMawJCA{DB9~YiUs(@6v2uqM z7km!ZOw(VXXM(3)x~Gx5338PI#FY*)@VSrT*J%R}o?k_j9r%oruktoLkeAn?-6e0S z$m)nI6`3u!VOb$Y464o2Oa+jliy&ZUHTdvf6V!fBBQeHN%W<@Z-V@@9cRXmj4DJck zF2v#Op8XS5lhv`7Nu88eNmEHMqjxM z5=ta%HZ7`8l#9z(R%Gq@MB~-)!l<%}WcbgftsrgPk`u<;OZ!+uSJuWTHIbMMatEy> zWGm|=dG;yJJ`o1r+=P7BlQl+PlC&pHcizFN!`PS#9f|%sDs3<#LL;W1m_c5;XQas! z-WQiP$5~m+Qlv@Gk`B_J5S^P`0Y<%u{7sRCFZjh+A%fciY6*HzfmfVAYrybzuun;n z`flkWgJxJvSgVA`D>>TS!hF=`TSsO&EU9~`5QSHL-{>OaoRJJsk2K$Fl^Xi(km1OE z)h*~L$RW~$$xj^KZZUk$H+cJ1 zHG)BG{md_#BEkL51bb(>T_oOhV#`a^A2g?G6K!FCp+>fPm`I*UndSm@%LH*G)bL+t zn&&yCP3A}|wcuuY=7e8I?W9bcuQWRn1b`Lspo`O!hI?$P2elbs76*rn;&{l8ebrEk zyN75jHtaLH7&a5dd6A?mPGj+KZAsaBBC?gD@w3$rR)#v82j{O?SXKna7VrLJGJQ z%>Fk7=I2@l50L~lA8vL`Q?}936EzbO@1hlbo%4bVjGF>ml9b|-H^3u)ZTC)2N>+4r z)R4ukB>~aoGm4WLBWe$-&MmxyiPN3Yt>p@mZE+jS6(WZquZrxpOD}nq&5TM?W6B!x zMbB@Rr5T&wpJ#!jL&|K$te7)bt?E$=_8y_WA9Aadj(*jR?&A=|OMZ0-SFQr5Xgn6i zhJb3s8*6FZKLCF5`!QOsv*J_V2AhbM*l@=Yn*Jw$0@i(p=rDBP`EQCRN8EIL*wR8y zr0JppA+wLjK31+Y&~VIkwJ-KG29u_J-9)M0hZvF$bAtppl7}@yl3Ko-B=gpE8lvvJ znJ1+}7LMa@mqGa!wxCx5=XIJ$fYi=>`giTx=vwxX3@z@XisNRAiZ0ZB6h(1I4ZFd%O z3uTA--h=5SPz@0g8*=r%XR>ffX|j+;#gy;6Cb10V2$s~TK2M$i5}>Wpuaqdnk^&ZE zVyncj{JyEZN|pS|0gGlcn(19jJL0k$Q)0;{AKPr5bLQSOPD@bS&PK(b>dKb!U|UZ6I7Qex@6YuE4z@mb_!z(; z;2t5*`}8Ks$=b*r8DzV~#Z?fXfff~^54_NdPdH$zh#$4$eg~aQ7Yx8OS!p#@R@||V z`l9tMsI=(X<>?PU+6iTS&l)nv1L}&Bz#{3Je(L5!gWglfQ=I=POqo?d3tR#0g5Er0 zl(!6Flivry9Fa_^Q*o#-{)dn1SSdHRd1Z}o{bkxszI;SIKo)PEDVB8t5th*c+ z!;gCKnR>OddYp~blv4nw&A82f!rOwB#uh2gNLmyGgJ#7u)&wmI(Q!cOn E15qv7)Bpeg delta 2267 zcma)8U1*zC7(VA)T-$Z)+N3{A>$vS^wfu;U+Z8wK?h%#hplBOMLlI6$R z307|u2Gz0;IUp5gk>bRQX)eBtO;ImIL6E{KyBLGXK=DGWvNE20vyGJ$^?5!tj)Z{hk>5Pb!$cv=N z2^#@f{2j$-8lh3F2So?zc7a6Tf<+LwcOI z-mOxncgRidOi=EZD&3C9)w|`%GWXSevik@R-S1wU00DiU3$8;4-7hc8Q$DqJR@S@6 z=j9FgxD6ypAPVC>2#ph(NDjL;5T*cp82iMW0u38)5)b29Oa#^3ce366{V#b%HXYG= z+O)60ejl_kaeF*QI5#S@@h7((wjp1mA~I z^e{e+A<|e=ampH1pM5WV>dqFcT-~`YJDkZao)^jL|EDAbf-)fJz$G72k1U=IXr-d= z`2{(#MqT_>)^0TN(Ux>|*tz@yRB?DQVuY-jdtU1d-l z#U9@w74llXoe7X3khCryJwwrq^SB~tx@z(2al*eBtdap3$#N`*StN?xH0~oALy<10 zqgN7Vd#xzI(iCDgij_tQmnR@HfO4ap&^r}q2s&)DvtWt+Rm+RGqOR`!`2dp35pWCgMHa~F)S8-^Y z(Ggv2I)X9dKMSH^ovbwM`J-Tl2?t2j2`%{5(R+K{h(_=L_T??B`vg5s0uzv|YwdQ{YVXcFi>eP#> zP_G15Y_o`NW?Ih7j*NH&yUReU^L^Gc&Y9OdPsR{rO2ZNskt4WNsKo;5G81JAY3R%h L*aXC*Vx9OG1oi$A diff --git a/iOSClient/Supporting Files/ga.lproj/Localizable.strings b/iOSClient/Supporting Files/ga.lproj/Localizable.strings index a2f3aed475ce47f65082b0e44997058637fe097f..6f5245146931285f3fa9de842da434282ace1b90 100644 GIT binary patch delta 2360 zcmcgtZETZO6u#%B)Y6qwy0zU$$6niQ5C+pA!(<7th!Erl4p1a!33Th)Rkv=mWk_Pu z5eeer$KV(aL4$x1{h>Hq$o?1|i5N^k@P~w;61ou02wwrR#Oc6u?mM=r#Gj^l-@f;r zbI*CsbDwkX-JcwLMjgGkYVgH{K`bBj;IFGZSXdLHT|Yx1-YU1v*rHO(RIRF+-%-_$ z*|VkCHEN@4AA;+PMVH*<*$p;oY=u>Q7oJ`nfTevE4Y}9o%WiP>y_*|+ivA3P6AuT2 zc=b)=0o*3L&fSYQZ6uby-E5<-x z`M7zCsahy92tK+JhLA>n?M%VAdAg}0Dx+E$`-n2J?@Yy{IIg7=aq!lXAXQg9NO9LS zVd5B#s$Io7Qk0X;ya+{f-~!nDUoD4I4(hD}8_qrHqVrQ=5v@4LC9QaaUk8T4MGJOA zHTD!cu;(*B?yW39_i6vsYQ;Z_c#M+%4fA2|2h@EUJb2OZINb<=+o&}AubhVS!K!*@ zJEFER-))?~QzbO_F|MVZXQVPy#dwzYgocJ-dFg*H=wEjgj`^`X)al#Ggy>~zqiW*s z2#1X04~{bI8gTfAbix+3g?&<-Qmi9b8hVtD&W2goJ8b7_9Qabmi914mY6uukUz+2H zZ?j5r%!t~`&&!y5!AAo&a8limOsvgq6qF@7c!He-JG05Vl2B=#M1W-I{zp(jZ}Yxu z;gH!44(y&~;=xxO&$h6isncv`aLv3f!|&+WT$))CjW%hoCOvA^)+B#t*sD=nV%m~a zQ99NQ0o<|JONDpAFB!5A%V2UuiTtowVrf$yx+@-FiE3;nXKd%3Qm1$&d8b;r)+UW) zD}RY9#mmasB-j_|;*#1$iWBKd$?d#&!c}-4FP1hRf}$B6dZCpksuH*+Ttw%l!GnT? z;1ml;-nw~p1d~Bo#I*6aS!WfX|IYZ2@a&n;C8ftu3C_Rc8`~heE+BP$)L5K{T~FGu z`lOR;T}CCAFY;2C%eal}zw+WIAN%Q^%kbkpubYaGKoH+dxv(}Er03?c5)K{*JI?By zgWuQMG4`o9k>hM4mlTsG1dga$)xgLrR2{dLe95y`4QZMa+`2+_gy*bv%{(vsS|iy) zvA1$G>#z{tIFHh}xvB(Dm3YGwQJ3+&^o8teN8xD1 z*O^5!0!y075>zXfCv7OmqAU~)<9z#BJRdYF~K5eCrAIuzkqqWhnqDU}TEpJCC+&t3o^nd$M{?n6 z+Qs`HEEiL*mLlO+707bIp6Q|InU;&?fuG@er}@`FDjYi_8VvBvVXuM z(z2K;A0V6!8j|Z0&Is_@Tk?m*YEK-YL*vf+GZe7=%gA zF_?ik#A)Lm)l!`=Iu~_r%|cLW2W1p4M5`kS&D_!;!AFy;Yn7_0{G(@)Mh`<+=}2(d zbTMs1^m2rrjPvfkkJ5E$l}WW;?nmuLIDp$W%0`6S$cJ%j&&Xb2u1bctEH&Ra-wfmY zAfv$@2iwCm=cruz@X*;KcACSrzr*IBiTz)~ghtI1o|YRGMWVf!M%|)84z@UHJ&Pr@ zID>Y|oui9R+~V4(_K6W6MLL91@W{)@skcn1q-bbu`q@;6IYu$RCk}6%fA=@>bVxGb Z1Pu%HlF}&}N!dk<5$Y1JNYCBk@C&u&e(eAN diff --git a/iOSClient/Supporting Files/gd.lproj/Localizable.strings b/iOSClient/Supporting Files/gd.lproj/Localizable.strings index 649251c703bad837ccfe28b5c6c8b26f0c89b135..e99ef6c9225c9180db4e0c307a3214ff9edb8ef7 100644 GIT binary patch delta 2141 zcmcIlU2KzO6#h=PbzN7w@~z+6?OR7XCIZ35K}95E*$f&&Fd!~5s4xocMnnH>S0*zs z!M_MG)*bMWNFqxN7X)z;V?~YfCjm{!K!Z1+g9`&U{Dm0h%5&bftl`2t(|kXD-}64_ zInVj|ayi>i-yiEANkK#ORZqtU062HB2*
  • )yI-f92l;LLujiNG#h^~> zb@0-J3+y!hGqlk{9U5?TryI|{>!j@|uwts&PWPvS7bom(G%*G?>e9i9ZND+G_w4A5 zIMs2?)s}5n4>CW$mZn?Jf-z+y(HuUhm%&)(=oNmgAGe)soH85joQns%mDH_+qk@s> z#K&M1NbK~Ld8sXUiL?8Z(1ZSPqrjyb=L)_o3+nHiQX#@9w|kb?;EB`axdi@LXK{KLACnICL|);+ZN z2)HOQ7u>jhXTt;DxV*-Z|L6#GHP!a>(WDj^C>;CRptsI~n=I#`v%0vG?>PrM+}In~ zVhm}Uw5W34$L=?=^RzaAWBX<(%^FLa{;5qiPq4{zfdC}YeB6hr)mA+2wBo*i3%>|> z>F`~!8yP;3X8I8}?qk#;zBc2BhA}$bN;laeP;MbXD+{*w^P6%@S6={6Rl!?F(1ZJD zI_T~qm`$$T&{`+pnL<&rs|el7f{!-5&1&CUtmCrYW^(0N@s{1-q}y3&p@kP&HtPj& zK?r-Es>fW?P4^L8o(!uV;!9j7wSK-2GJyzv_9o1!OQ|R}@=nr!H%nb zsN6ejanjj?p!dc3a6~0f>>dJQz=u%SGO7?WMQ7r^=$4-n+N<1c5}?pZ=fAxIc{_EE za4hNTFpJim=0^E@my^D^4!@(@?!rGm@zU%Y&_K=~!GX(9IWV!`hP#?vTAnH!j-0hscE(EoC-u^F2(Ts@6da%W)B9(M;R1$$Znz&Yrc(n8~ zG(nce65zqs`8v-H=d^izMDi-ntu#Fdjnta}9p>S!?@SyGx_zRv=qP0vR(4UKkQ5zx=;iTv6sONWJGRhA%AEP_y=+TvOJKYG=| Jw4>Vc!M`BVD0Bb- delta 389 zcmX@Lfn&l1jtw3ro4HIi_%=5=XNXQth~S!Tdg?zy+wSh#`|9 zk0Bi_odIMQ16c(?(R_wvhEyQ0m_dhOdfrz?3tk0=6omTez8@K7c@x3P%Yh1kwoeY+ zFS*(2$~oKV7dA4oO@G$Q$T!u2k!!l0C?ntYcOr~@jHnWm&x!GDR}o{}z&723pGjo< z0a-?+DcdbJG1^J7CxIR8Hkr{_c>1(6i~-C*f$eY4FgA*8pYxP)1K0GZ4~%M)_w{g1 zPEh5VzJQ;RZ~BxEjQ6&G_`%pGZJi4AQ3}*)nLux60R5B*#Kl0LC4&8>0Q6rGRI~u- gd{8Ll1NG=lHWU_}-XqGSwOvM(=^f+r6XHxw0LwLRhX4Qo diff --git a/iOSClient/Supporting Files/gl.lproj/Localizable.strings b/iOSClient/Supporting Files/gl.lproj/Localizable.strings index 068f7fcb50b285498324194175e152a9711f2b2d..aa2b3b65cb005d41337c019f161a20387a372a31 100644 GIT binary patch delta 2375 zcmcIlYfKb(5Z_r(?pTiFShxqsV>u`iqzElogF&I1niv~_QYH2wCvW9(-T@k-^=eFg zSIyK;O06PkY-7?yL*q4uG}vmZiKrw-+QcYm<15ud{ZK;YphvD~na!eOJMiwj~qqV;?Gb-L`1rz4A{>&&sH)>G&`&A);3D zO6^jMVzR#@@MX?K*D&jOOhzZ$7c3gFXz2VNVq(RLSDdy3{LW(;mJK|7u< ze6eR^p=`#=^EN!TS^+*V|O8zjsqx^H3&BHrLgqTE=a>` z^%fjjU?HCd*6ChG;H7)JU^ZnBf>l>T=bkjG)F6RQ?F9vEKd@2P39wM+AlP*UPWDI{ zq#8Q0`4CU{%Q)CX9P@Z}a5O$~LdlJ1E;wnc2i!)Gujgmtz6B;)qk(Fqfe+@#O*)|~ z_rXX5ALm=KJkqY;g?>qa448s$=_uGjb5h|YGg{87bPga7eX1$k&;4oOa<$+IS0a{w zZzpXBqbb?qB;U&{bXBMr><+^XKicJVjVjaRc-oi=Nve<24$G*!76%~V% zIx|5Dsk7iOZMvU((2TiJDcIPrhL$XaoaI#C0Zt4qR;c78m_nW1uqB;(&#{A*ibj`z z=78g1ZB%gAW7L#u`xbd3bPTzHc8WG?bX=L3ho}J+}F*MQ%)`%)TY-SgH zfzcJIG;|L667j0Z>oZD2IXl2aeTPA%>wCe{AWWJhLs##OUTNXfs}H!5Z?yqK zdZZUX8`l@aGqimCFUq6osXUKD6s=XN39Wqs`4M#C05h)3ET>cbLzdS3z_a$dSsuZU zRTW3NnY0$!N%Lnx7**Es<7gvua^D7)t~z)CcP7a;yqI94)}vhNqVO=v`3$mfgqiSo z7sPHZklyBSFGxjFwl0acGOmn~lF?aFtp+~lzUY+H)8n%$1|Vc{Kb|z(P5OC*OBG<~ z_gKpywAg-v87+Dbm?d=h8!+ROVQZmq(W+Nu9h(U?Ls_)xKa-(LH(>?^Gh_wtwkBZT znJnyWi^pHBcIwNJ?GaN@(&i-DfkTm&>7qw|x)d*`(xF?t#!?VVTu)#obq((u@8!F_Z?jZeyeauRyX{eB2tP zcHHkyglvkE`P)=}$BtWvoq}EAuj}nGR{`xcPJZx9UKaaF*+_zZFMwI_4JDZ;DWO^$y Hi&A6FDA^M zRE(3p!xn^fdek@0HlvAB$C8y<6*m?k>pz?{v%>hU_>HO6ME7Mj7bdgWG@HX)c{t#I zxc;t$OJzv=FJ71W#cocMJ-Sm7FAw(PVlU{}yaUm^&1T4nhehISo_VCTKxdtEW(uo5 zdfXs>g77NYQ{NPYQ{C9xxr969+eRFAm^cf!KEm)|nZQ~oRu17BJ-664NpLdVlf&eD z$cW|p8W{NXDdpC-ibdI>qz?WZf-J5NfEwH#fFbw~6F33HDdwQ_525ULlrTA5G z?r|QkZ9^KP4Y=6Bg6RhQ%>>UQl99k~Nl6#=T~)mOq#%!w`G_VpgIB$hc4)jxQ?SUW v-72T{y|CH#WL!dSl0kdY9<3e5WggCJQjb_Uqo93K8VM=|=?~!8De1vKqdm5! diff --git a/iOSClient/Supporting Files/he.lproj/Localizable.strings b/iOSClient/Supporting Files/he.lproj/Localizable.strings index 060eaba15eaf7a5d47499d523ef4ca3042f0d278..42835ab0d7f995fa28626b1ac9b6f31917507fab 100644 GIT binary patch delta 2199 zcmbtVYiv|i5I%!-dwaWGF8k$M_)D=r4()H7*HQ#VFAPYE8gsYP1@o{y?N5VnYZxbGBPR6aSda-90-q zXXcx4zH{!^f9CcN7{@=a!i?cPzF(I5);hB}SY}0#_9M}4*XK1)?yZ|bw`Vm;3|FG)j_)Zm8sUd1^ z^U=k(z)4k2UPmNQyEuf#?`Ld zi)nDSnxaD`RfO9N(?lLr;!jKOqZKAt1=$^Ol~CKXk3t_aJgGnB>}C$OW;T}4E)yK- z6ZznRY-4!I=S*LA!V3E)?Q( zWj4g|W_189zk_ZRf&)j&EE;t|aak8b4l{FY+(J8Nlz%b)7gU0}3nAdcmRtvxZZOMl zj}WQp2#&XTrOVU7wNOMOJ7G49$4M<6U{G`q*qp(j+P&bQ)w{r3AXQRRvTUaxhM^2+ zyzAvHE}Yk3VMpt18s83vj@!b*m*^h)aTo%$;4^Sn%0UooR^l>%cFq@Lr9`=D9t#cb zWoZ>30P9}f%UD@A92MsX*|k~!y90*`<6wEw%!KA3lP#noSodiOS?9qVc#ws$@(Le- zrla6)=;j1rJ$)(r@6`WD!;ujINQ1RZ5R+g*GX2gdkcr>C@1nQIppZ6>frYQ_^U|R) z_#S`Bbz|d8g>+q9hg!iHH~~4>^q~`n2Mk)V7A#8402lst*24Z$C-!s|<9N9PbDG@& z(MC#l+s<6{a!1l>h%G!PI@}7ryI2?4Hc_;pNo{2ObJa@LtgPf|(IS%+(pQ$Gs8;ta z@*?a{O^qQ_PDUzhh%{1I{<3Gyt(>bkXOum?EVU>{$vq_yMZ{7?GNsrj;tNfI?d7%b z7R8yRC4QS%vg1xXA;C>wHt>b;bJsoCzsNvGv$f&VVAUo#q{W8AuMEI@d^x4}ce!6&N!^Y7?KO z1nVs>v69Eb*Y89<6iKGMX%DuZ^Wf?oh0{53<+@U`(ojsb`?+k*7l!{y%~mV}R#h<< zGB^3%Mz1b}5~>e@8>;Z;2Yz(ddjrCwG%C^$>9~lNzkVaoSPcAmyp@r$Lg;iEc=9Bd z@LNrTQ7Fm@Gr4I0QH%v@gFYrDi z)O2cVL_N(}r;G8bgLg&sq?0^VshhccvIn(;a|`wqL$KAOoVoqB4D&Tgo5>Bby^AA6 zcXIUQENVtnyjJ>RS5{P+%TQp--1KqOf|540IC0fY4@O@!^HR);6s*BTSNt@&lV2vI z7x=)v_a?aU@naUPJIe3C;f3IsB}p^aO6KZ`(djYpq@&-#Zhv047By2t-yVlRgA|sI z1cLCDy@ociwNqoy&Q>xCbbT7=qe)nm?wEv2KxZdmF<)p2-9(tBRl!lq`KeIr*(SG= P&EImC{&(3m*WmEqZ^2}W<*+df=iWrg^G8i%$${11^6d2SL6c~Ib@1Gz#UF;5{kd7Wu z$OWjo2&f>BAsxso0V^&BvI>Br`9RI748=gG!!W(BPJ*Al-$g5&dHWF4XC?lGUIv4?NWS{f-#p zuiouCOBwB?*pnD4fo8c)PJAppy=y;X05ecv`@{W=jUwA8++du*HQmadk!!leV@8?D zT(W%A4a69Qrl&n-e6ank9A62%vRjKoADbjvz*Ye1#Z82&1&!*3pmIw=hSP zuRtIuC>+UA0}2TeGz8K8sPQW!BoYzHY6$Q{$sGPrOqM8y;17f6-nQHL;UBZy_q~13 zInO!gInTX&cdzyOY3l`jF8{tm8oKZRs`0MhhQoWy=voh0aF@eID>QceAplKu@+{O; zWVj4NL=fz5Tp8B#5PR&8Sku%Pgb8NZ)8aa(0-#R~&a(Hej!VOZ0@@ZmMu#=fC*`rj zoS|3rQ@aM!=)KE@#P+-~!NzUa9`PBj5lN zt0vds_5A_ton*z~Z?$PA6BOqa78!0O#7llji*4%JEclP!zX#szH#Jall;USW$5EdX zPg-hKoj7k)&t5$X^Xu)M9F}6%83tRdZfdv$t1Aj0v#Cq4ySm&jIz)($iX zB3F4l^S%QeLfH7C3;l^|T7DRGkICZ*@sb6kIGMrwZV{z_UWHjzy{Z!_jiiW(7={|_ zaA*d1-4E6D@(19oNN`|OWH9%+&n0JD@MMsf+|!zQ;Gr8gId3}q+f6uOqu2XEqtOvo z{_zp6X~`ajx;6s0srn8ds$Y-S(BeDX(CT$o7E#AL9(rXZE1|7Lqva1kJL=Uilc>cv zTP@fbbe79englDs<$ILCJjt>%HAwoZ2RyD;v4K08E`qE!x#LASC~?g-x!AapVxfQL zA;#pJeE20ZoP?j!OE#!}WA23O*)bT=&hc@!Y~@oFXRi+l_tD}ZEB&$p^zs;EnDkO9`0(7&0=jYcdZ*7Xk4}6jw-D#zy!$o z$antkEl^1N55ZIj;PpbsEbf* zluN3fp!PI)N~59~8~>=orT6(5f5F;garvH6YLM7{%#=J6@*hF|>Pq5Iw`T2}SYnt- zm%b@xp-b&!VmoITOUk=Ww#HSZX`Zu;9@nV!?BqPM2a_jmh*gXio`5i;prg%<9u`}K zv4r8093bT#oxoR`gNriD-(iwLdF!eZsU#*_V!7hOGgB)~=2@m{rr+m-m;C#nNu91; z$ueBM)IvA5gOfJoz)A-Xu@pAW2CbBFvE``Sl2Ywt&)k87-pYhhI5R{uJ_FxEnNv0+ zvrF34wZd;Xv@TV3Qa!u)o21R)@A7Cg&j_{RMIa4fh(i%WL916<~^$6rJDVFxA^QW|1NG?<3_C zM{P>iw#NpJ1%CalUYbXh0u>yjs1wapwV>GuInatd*2k@;m+z@Lj@|i%EX1_&7dOccVbbu~gf`N%Lpb^a1A20I diff --git a/iOSClient/Supporting Files/hr.lproj/Localizable.strings b/iOSClient/Supporting Files/hr.lproj/Localizable.strings index cd8b123ed9710edf0f88a6611054c2c0f1c55279..6e87aa7f7d3e3e27c8518da30cfb37f322eabdd4 100644 GIT binary patch delta 2482 zcmbVOTWpk75I(cQcG+&rUR(Csf7=3Tu!umon~G?n5nEnF5L4RSwybQo+ufFm7)oN4 zfVaX(=fw~RL=EAAhR7zB2>VbC=mQ#PXcRES7(xgdLyd`~apwHp-C}$&rfK@0b7sDo z`OY_Udgn9S$qTkqx0m3Ti$i$x8#mrs>H-KtL+D$ajXYQE~_?`^78?NIH?z(tFF z6$bBh^KL8K#4*3s7d6*AwP%J;I<$Sa%BTd}wyW&v8%rLkq_Mr=#uJkU4!&PGtBY+q z^%*=pTtzR8g5&hrO=G+9y#@zfsp7%cD?94SxQ*IRN!^ zcm%4*Qf~3$f_gii33-bK4y@W=i-QMk^vMXjtV@HNUL1uv+8Hu9dv0Cvw8G3j!?~nX zD-8!Atj#ndPk!w#8oErwn5H8o$>X_+a51Kxj8EZFo$(Z8j0s7rowR-w{3hh3aD5SO z%BYKtm|HQb$z2Y>44{>V!0uYWJoYn3!eD|k>%^X=K1~Du=&H=shG5c4m-m5_dK=+! zY7Q|g&Oxw27Qb`(ss3$fz;uO;KKKG0by+Sh$-gmuovjQlI~jec8Py;^^y^GZFt?-* z4ide_tkdoD5WvA9r>@8{&&Zt}fwn+-2fKBvF1dePRPUqZSD?9UYBTrl71-;=WORqG zpIJ)im@OPt1e8&oZg-G!hN_AeCf6E6M+HeTMBDkU5V8iD( zdU0}>o6_T8s}uAdZUh#umT;VXWiLA=@)%zkjI&f~2f@JaUvcAdR~0t5*~s}7SSucI z7sDEQ=Q@-6(lv0pgn!XSiW?=#g~xEind)URj+W6eQu>x8ZmW)Mux$tX^m6V}!vlHH z@Gc1IcqU|AB`~_)Prv;EHoD}ncVxR-UYXS>FgxPtN^W?G{bA=ok@8Io%)RR5!z zywl>z1#ZK~cDlNcyDKumZTRgyZq&$GlyJN^E#d7d>Ks8&5YC50@n`e@aw zP)F(6P>B~qAw2%`96W!_iksKE2Sg@f)6Mjh=n(2lauVu<0HH?r1)mm(>0r+!PbHD| zO0|UxovRvI-0~&A&03HWV>*pVbGE78fhm3yY>?G=FWQlWu}P>l4Ot5D z6t9zP*T*_dvXu-;K@j~*P{GaenJ8BhmE!vNTZ$jMMjDpGr&e!?OtjxG3{Vrc{#Ey zJVP~caGrcci23X#t2Ty@@r0GuO|iWs+deHBA-|ycLeq*zA?>W1W!BBjgGZ0nxU!t1 zglgdFu}axhEy4v4y?F@g^{_bdxgDSQ-HofyIB9_cn78WCV~teIog9CPx%3eUN* z2{;+#e>aU0MTsiu_M@^J3H%W@Ym g6mjU~ENO0yN<9m~S8i~lO^>+je@3Kh9?OS+13=_u)&Kwi delta 451 zcmZ2;lH<(^jt#F&Hm@<2krmwrs$Wfoikj|jMkjPNPkj#+5kjYTSkP76hDJU@b0!1r<>=GcI4HiuV@^TpR z86bK}fU0wW;(9<)7lu-x37K#ikV(Zr@dBV|KG5V;Ag`D~2PgtEDHSLVGB<-Eb?RRx zbzTLA6omfC=Ub%tK`sL6F9)gxx%=T^$;lU#xi;^)xxj&whcOjsOd>$Cx_#qM+pVIdY8u*rsbKFo|q$Q)8SoeS6+M#t13?B(U$?fG*7e zxo`UI8;sG+K*8;{HyPVSwtxD_xP)tZPaq@L^mTt2WhSrt#y4GH8l%Ycdw&@3ZMR}& zS|)9o3iNOa)S;PR4}l!602CuoLS}IIU06e&UcK`qY diff --git a/iOSClient/Supporting Files/hsb.lproj/Localizable.strings b/iOSClient/Supporting Files/hsb.lproj/Localizable.strings index ede9c46d0c1d90779e23774943329349c8ee677f..a89752178f8e0b437bef753a45a8d5f58d68eda0 100644 GIT binary patch delta 2142 zcmbVNU2KzO6n@V-OV_ns?b>anlzkhMiH<=5Crlw-LSl>ya3Ne6gmvwHI{Gu(j%9=l zF+da%(GxlT5m7(C}~#|9T(n5FC2_dV}( zp7WgNJ>TJ-mY<^vY&j@8T<;`)RB=Alqqo@<+$>n zSu|k>2X#2WN>@)o1Ff?|l_{f=_+!0~&h~*7pR-x1JzEUI=XW;H!r#C`8~7YHYCtqS9yLaL_YJu5;mfsG${m!7(Sz3dZ;oRv9iM#2;Bn2{8X}mcQJwD)7H%8dM=_YlQju z%bQlswq&l6`}#dtIg=ZhVlHt3z<|X=qhEoWT28{_tb*Rj4V;7>ZfrHScm`Fkit3ab zIcYDaOsO8a?1oxhpm%cDnA+v|gzCY?Mjai$A7wS(k_(^A{u~krw!PZ%Hs}I~5`^!d0 zU*w89_ONuBUHoJo1}D{@hFKW#yJ;{RH?w!yohn0hr@>FXXTUYb zP!YXqu-al9(ObHmP~9v^l%F$r=(?vdEWX!8By?XGg<~_CWRhd$d@r}&Gz{}9`gLFd zCc;-JROe-hEZEUxrv7)qSQh7VT^dI+Nukp)_}4uE92wP|(o}H{JW{a}Qn~Br;h+`w z@Rm7&%L)^=DF2`$3FJand4C7m73)O zM08M8UjqGtpbs;%)0x!?Nwh|tGf8?D9-he7`A0^I%xmr3t z$TY=Y@}emWzyw~oFb|sO@+VNaUEWDOEOCOvwlZQd3$Na7$FwF@7(0{qw42We=`?rR zP^%duJPo0-=$}qT535ZaAr_PwAU9O&ppwnZN>WYQYf^4Ga_5rrzEr_B#qzgn%hD`_ zq-v%cEj;la+yxD~H delta 435 zcmcb$gkwPq#|8o8>0E7$0-GNhKjPgi;*=md`N1x(%?;s2mD7EWF!FH5Go&$;GUPDi zOb={lG@blGKwx^`QAVEnJce`z1%^b1B8Fs!42DdGGKN$jUrj-Q!51i631pW5>1?oQ zB9NEEkk0_oQvy_-3l!G_in;*JE&}plG9Z(Rf#L-~(R`rEsX$&agAPyxWKt?n9As_= zL+bRi?-kGSxRG`8#TGreP+enXI{ F2>|UdcXwNZt2 zd6T~+jAuzaZG}xtuUzjpKb#kLVUOLIg3*;feCF{3Qaw0-?zs|)nw9HiUeWWa3p+Z1 zk{+D2Xk9>_9c)=Uvdg@EQi=!rMm}whtigI5*m3yn0KVg8o7$o}0S#{^5|W@fRV7}t zVvo!;`@a)+?LY5oQz>|A)%|<@W@m|%nfj2Fa%-`E>-A#2Y;QoXnBp!Ox7kQa{3aTb zD(hns5cGl@5gJawjXn}!)}=P)xLGC7ovq})S>qgJV`{UCGD>Kw+VzKLgIR)J^UY^6 zXTq?OkfSHh*}dACbTol|7lcFN9Q#zY+%<>1u7XEWc$`x8@V(>TG}p%u28Ygsq%Ks=JBg-N_A4dh{#1zn{x(uk`Q-!@x4t?Q43zPWh{z&A&wLLYKhRopLGU3h5KX-S33R{*nB^#IKYE3W&O_@=a@Uf|2@ zz9`d8!-&+)dce+68UB;l8CI*+OX`K8_eSKaO1(d_-M<6buo$WZj9Kd`)nTrTN=Oga zdP8fn%i-LzQ>1!tB%(i$Ak}HF9{$#2u3kqFzecLfsYx!cKMi}92#cfzuoQc|4F)tz4t*}Cis&2FNlGK_LSEa)AU6C>$ zv)BfZ1Q>L}J92B1K9~Q=HjC@D0*3|mf@-%7AqiuW-(>G-ljXsbf$-EVmY#zgQ*r%u zGzcl(X7rMH4pz8ImvRtsUl^{Fz+(ETDX&O}Ei~TGeUPw>Tg$X5zkv>iDrJ%yS~4m< zUUPRJ=E9lVQuoQN+ajj!IV5oSgMjI~BY~#H*vLAj^Sn8PJE|W>{#a8TV*=BvF04mG zb-HhBmAU+~xb>Dd3iS4crIT4zX}x=)ua`ZSsW&Q0Q7Bg?pBb^*f!v7+%WOZTUV*<^ zsu`WbD}Gt!+{w<$rMT0X)4+2ik7GS;Q_9)`!(RL(tr}ww5Jb`%#nlE2nRBrK_niRG z51m1>LFkmgY3yfxlW5G^CTuX=mm%TE(wFN!x_e<+ zPP*Fh(m55!;cKPCi^x}$CzL*4bdO&Tv2Dm39WX{ z090m!jh@xdl(Poe>M;uxx9y&R%bpr+B%EG+-?2!>jtZu+(&Z~81LpEkvvQvV@fzv> zD_F#acIHgNymMYc>?Y4+JcH2l4g|sC;z;8dAhx3sxiCmO9?ao)#jig)TG`9gc388x z+KI(11zhDfTH1z@Yf4&X2P_IStd;}3Ee~O~_fhC!l4^S0)oSGU)w~k3!uhRg_0MALzdCocVKaoM!qUveo#VPKmVQY$+6Gid_V$vH^3XroCR{bFJh6;yC@k?6x* zadPP3B*hG$o4SaDxHt&`b*e%g9GnE{d5S}a_uhH$-gDl$r*P&emOQ`PyQJ4-bGQ5C zbt&*ATCd87-qOO?(}kkj@QOLZuF5g`NhwOnU4TESaQ*x0p9pM(WRp*zXM*bkfL0(gZ ztjFC3YEJ9}Zg^=mW#tchosM^W{Ys8Z!{@j{{A<&BJm5r=O82`eQZym_z;8Z3`?Y8g z9=^ko4e#1joW?PMp6CYX2A<(pR?9r_AHP}i^Qes=z*}VVl1mzDn96x5^S;lyzUPd0 zJz<1YU$jt&_ux_)fj3}az1A%X}a8ZU`J3}PhUQrdMi+uE(&xM|p? zFa-^_V?1C2!Nh?45pj+RB!nRmBWi473>d&Enix?8vk)T2c+UH+uEzMweEHfp@8x-( zbIx<#?|8rU<{|65n`YzitV;auj0;yLY`AIGLxo`#rM|7;#T#wb;*Bb)x>ZUUn7!`C zCD$DTn`i$~N{2Rp3%8FL)UgWeC6B3g6;>^(S)~WkjV)eSIB;pj;f?s<4Ew<4?DhrN z;I5=)4sg&H_uJ^|;3yWPFf^$~FBUV1GFWOe*1zf=+aj^i$9|Zgsc7T}@RY__ z?mFI{QgPKq*+B@H%0}i-(pXy1H}k!sVFDUKbGF1}Xy=3L%tOyQz>?WJ9;_fCW==cd zq=kmRf^j(Kgah~9YM`4*utORf_xS0zZm7c!y9NKe;HXJ+vfB8Ks4f;A=2vD?tV(|C zc zl6mPI^m%ZHvCg|*t>tiqw25igGHFV+(3@TeNz7Js-aoEfR*kV$-k;#Q;>(6XT`s7? znbR!(Miu3U6UT3o*Taly@Th}eF?i|gqhRx<8G|dqPlDOPYCXTLI1~@Oeg$k3^H(K! zy{ef-hQs3z^9p3+!>+z^Bl&xUNA}`T2zch z#j&g`NMD?1ukZ;6J=qCW^!#VwtT8x%ZcS*GRPvv9j>lbi?6#Ly?F460nj;OfO?C&t zO|Bl8GQLB{W**>7AT026W~_Lo)A#?ITmXXdF@ zEOD}0#O{-kyvwv<1VW6F%XypjslkfZ>iIaYr$;kt(n)0`U@0$4obP^Q46~6j#WLNh zga4DfBdR5eA%#Vd(!3_-iSn zI;XdmyV=9+QruY|EHqRr!4>dfB3ni84S{F+*x5DQBpoa($uTDNSfsX$#+O#rbn7hm zL(+>z-bP6-o1YKHr6B*J!Pll&(AhXl!q20YsZZY)|UF2o^{n_}xWe&M+4hw_TdtldmYknq@`m)}qI8wTYqsCgf3!SKG8v zQfvx2ZQecHvwCNYBNqK>wuhmXGF~JPVXBsDs5OipQLV~Y%y0=lAk}HkOOF^Q=Sj12 zk}$cy&1+B$wO&)T;nBmP;xwmKsOs>S+W~6m=PF3hXPy8X{rDWX=<(y6jFmINStW21 zV~n31vBADe>ZmfOzlR-x;&$B?{I#HkCm=Xis3j&5A)k<3E3V<$7A-zkX)}A$0o+a$ tf`Z1uj|y%8)j~nTz0zkURFkS^m<9Y+t0g=q*O!QpUi{yzhYJdJ{soO+Dpddg delta 400 zcmeycjAL6H$A&87#Wp5gEW0aHOezM77XU@`fhMN{dBqGm4AalOXSCo|U`RozpI-Tf zQI(*UxgUoR=aSTX6(Zqz{W{8MEyRmg!Wo=nISXib5 zFF+ug!JBZR!HIzo!bMRGRkE1P1fmI`LlP5Alm+Gm7X(7YU_yM(_jQ+F{xi+jetq9_ zp7WgNT#oIw-#BJJ)xU`QpCp^Uwjftu`NPor7S`$23+-IKlMG!Iv1e{oF|}F6wSTOP zPuR$L`lCfRr}N5g^04t7Rb@Y;BC1ifs*p;Y-n*hBhbzy($hme^H@gnw?X%0fm+AP! zLS3}n#(i$G^8+_I_(Yt_xzeCq9{h)Vx}nUWU%BtlMTHI?b5p+l>aB9U&2Hmr13Y}& zJi33UbwhC;{(`)^uH2X~$m|5kfQs|>m6T)oOD?wueG?{nL#2lTx@4%py1%q!u8BR} za%}>@CbgAE+*FnxQ88WrxL?oT?C3Eq2>X6o+GUEJSA0lsz)c{jX6j9pBaR;gA2K zf@EhtJ)!x<T61{={A^V&$xzxD8CXjkxk=guyn9P*Hfas~|+` znCI7lzwNs3c7`4c82q}I@{2bji4epLBB>@o%fBf8HL5N=8+ph}1$?lT93@llMF#U8 z)&abk6ggN=dHTEBLjBtBPWD`WQu5x@K`86^SL3=fH86<-P<4G>i=NGzSDtnLPL_X!iAh%x{OtlNo_$NOkIU1O6pa zdWi>9pje_NQiu{Gg^gc{-(j+q_-K#h^{aHq!ApC{#kWQ=KLq86>TCUm9$Vw0gkBKwva=pTc42`_I|^KUHV)s%W?;(ZXW8__H>FNlBXVEd)2?T_hAgvmp4DYQ)qAoQ2H6 z5E_=0lO&r{{7d!s*(JIqcWY24IJ=v{K8QTpUClHqKFj5j;;c8shs>; zdp)Hsztl&?RHb8ee!h2+JbqDE)RUHLHtoY^%bQ-ju8RsXQxJN4A}31#MUqGPPABE1 zhp@kXOvmoKao^x07s#b6vs@)lnav@k?*bpGl?<-60p&ETLsw^W-4EpJ6>(ZYU5IxB zcbskQk^RddH;EV#=%seJ!8SomMQOE%u|DtYwo zQ-;1d;0hp9SXxWOS~wCqfKH8>X(f;RLhls>BWAOUJW*W8ueCE|3t0d%3PMS86>?Me z#s-|Wns(#Gmw=L#H9XaNq&APM($gxFXVcPdkshmcntyvIQG==!_RH{;s@1@k4JI^Y O2>&-;{C-B-+y4SMN;sqd delta 391 zcmYL_u}cDR6vyAQGCA~w(kiH_Uo(oD#}2E-r^`w#^Ke1-wb;-#Pk6Oe)=Y%$wo zb(hru|L1vAhUb5!gk*yS4*1a4-m1 zX00?wts8XGM;_%%72UMzMYsK?pp1RtLB&_6STt4pQOA(5!?(CFICrSN?i?Tm5js~KL3>JZG?D761%~ba)aE0RuveVo)+bDTu;QGl4`z@CTX34C7bGLWseb7_)fJ+jd*jKmIVyy}kE+ z&-jwA--fjd{8KwryV{~sDx>0fX#U#Lb`@1o>MgbS$~W_} zskZaIM`dvBXgxX0z z8z(mGa^klRKejq-)LH=sS)<^=eJ@VHK!#dA1TS5!0%P2)EFXS`ZY~4gxJNuaIAh$z zmp6&1E)G4ZgEv(N?jNk0u&-;Luy+^jQ!358)TsyXtB6l$f`^^|u*Mhn87pZrZ!B zrW$9hbfM?2heAV8N&AMNxvYbMQe4`W&%v7;9fD*fe)oD_>p4-^Bu$EDs z1P06ut{dMnC$`v_X~pYixP&RA^AAA~?@Y0yan_2pVFOZler^DrPdj{BK9u1cVtk;D zakuid5jPzTQm6|Y3ny<&!g`yHO@1ndWHK2U-b*qW9h!~|x%;4o<|H|_Kkf$;2TVJf zb8OVt$1C55z(cDpLp54IsHBnIEVboL9=u`rC)>HG^)l4bl`p|?o7@{c+G}12?YL)= zmv%qKV2M3o1hO1KJ6{r4ier$7x>Sq;y)e5Xt@DfUMv`IT7+zAvT-Y%Ev7M^Nz(r5I z37)cUJ{(hHdn%!3UF{CMPmfuXf7$l}3$0orzvKiMEHI zg+)21%fkOsla$)PYcU>Q`6&J#W3-S<9pO@#j-$z1H22$SuobK=|B)y5An_$R&hC6` z$e{I3okSbX4f!x#ZZBnt@X$SUw}y^=DrH#=jvC!f45IfKNZMadZhp>E>LOHeQLVV) z+9Ikx0uHXOL$E5MoMIa%+r`#=f>FItN5>aKAY6nT-(+}UQMx-WHToCW(bWchy^#m^ z;CzmZN3u1L;iU3Va~MDbAphH`WCObC9 zAtx_^J-k9IQ1rExLl;FzIQe-ht8sjLcqaDGuG}RQcXG&*;tm}_;hV@-9egUGHI`w; ziTnH>c(}a~~ItyWH{W2?yQS2`>EeiXERFu;Y;xMhWBM zCiK}-I-Q*PGrSdirkkD1STh+;#3T7hj56pX5b3qFj$U$|wMxn(7m+klNDPW?J%9Fs0sErI_x2nv^4Uik~pCs5x2 delta 433 zcmaF0nqxyZ$A(SD)6cLm3T$pMdBVGShf|N}in*$<(swP{>2(Xqi=GcI4HiuV@^TpR8S)s? z!Qz<=xj=C}AlrqZ6lg*wTn1!PF;Kh!D4Gv6ITgq&X3$}nZt#iGf>(hd1)+ZW?RSi_ z{E1-YVcfFH88SG*LcTxV|(2<#s$)rsX+gw zKpmP143`X`4?$s44D@Fr*p~_*$3jI5fSv(`Nj^i$&LdP6fOgY$oLo_!UP@0Xe2tO!C}ZE8{JkaYdh}kMrM!& z1WiQ1d7>vFq9Z}WFG4beagQa>jqr7YgwtcUA3rgl~I;TV&5}iJh`k62W#AP`c3c`n<~pD zQ}yuLr7S!=7{;+Po&o>zTRy0ydoRNrYzWWvKBrO~CZXDtJ#eos5r!vebS(r2tj5hf zc&)k!4^?^wmW?*A#cW4_#)_ec#(ccg8{nZE8JI`c2Eav!PJn+ZXT+Z>qu5;(qFp{P zv=dtU=HaR7jtho*16co-2kYZ`>t1icI>Ui%sW#kkAUNSOJzpr+L}C+>=)w&MK|3xw z8<}vV&p&`iG!&l`DbB@rsvg?mgNTE;c|8>Jc^$75dv)Wwcr;ybr!M(K@RZ5~@jXO+7&OwOE&%+uf zLtAp2&cmJ%)|%bnoobs(>bzSx^ES?FsRRujhuOkbbkl^{ESUK~S|xC~Ifwd7U>1(d zcTvMaaN%y#4;hRtDZ}c`p0Ld)xE=gSt1TROCqLWq#bgv4?s&M|f>p}ZR`xY{mxn|{ z-$M{Ae6lu1mkzK{jAOh^TLBMK|1c}0aX;7f`y0%|$1l0aGYqBJ8Vk|MUMRxsssLW~ zmrghubZHo7(evMdzr<9nENe;S^soizyb{F9RH!U2a@U~Kx(hb1Eh+eAYGP2hL&UfAov-ik=n%?f&^sAeoT(RIVKAZhN%cDC7>920Zn98qU6wOR_@ZjhcFrSZ*sK9lZQywENfG92 z%%LKw+MB<)Y?qQ)!1(u(*)wcrS5hseUspnu zJp1|2Lh2v;g}bqDI|QiW3)V{GH-;(1I#srFHRGvG9@!bK^4y`{;GO7`_TBRS68?Z~V$_<+lkPF|F4G+k`)p-=}jM?nBKKdi6f6V1%8zf;NKtQ1-9zkNvOk KJOgc*a`+#Hhb_qL5S}4?B>A6dZ#F3d|`Atbqzet`GU# z>r<7xG-P2PcKbX98RkK*S6DgEuYYeWr2_LD(=01%+!Y`IblbssmjdzsdHv$l?@{;< zYyN~M*&zf;&ETYdIU^dqMs$&;T`1^Z-U(WAp=^ddDBvG6x4l?*PByb-ArL6K@L9HDdzCB3xirsgopbk)K)v;Bz` x|ANMe;sa+X-b9i;T3aY^GPR#e;e7c4&9QrH3t5O$SF$ae?onF@spheje*i)ZbPxal diff --git a/iOSClient/Supporting Files/is.lproj/Localizable.strings b/iOSClient/Supporting Files/is.lproj/Localizable.strings index 89c7444be6460b577fd06299d2920ed573d81393..f97c6a6c1a34e137b9a78dd551962421e3c3d598 100644 GIT binary patch delta 2260 zcmb_dZ)}rg5Pz;f*OhLoUE8&E-QKx5P_XhZs5s)b@JC{zLZ(B(Nw;>RtlLVvu|Pt= zm>3c>VL9P6S+FL4&?w2qh&4D2#KaIr7EFL>5ELQA1Vk`K6Jpf6=WW-*CqJ0xZJ$1O z&+qPj_q*pkbHMiIXSUInrD!av#HC-kaP^u}+_0o^R#-(;gvuJgQ`4{dl%aO;_kc?B zs#As4cK(-CG1af4Dn(@u2#iOT{#^nO(xp~dF?+4*VW%!Jj&E%4_Ryg(z%hQdwSE`Q z2{~|ct$lpch1E~bU}(DHg#g`lgM->UV5jv{P)99p@KQTi+&EFoTNS{INU3(fm3!-O z)NZ9$+~CA__PFp!#HTHg#1HCfr)@=M-buQ$3OxDaQl#y=Yub$>zks{!zM60>;y0s> zG(4gsm6&K9Tz5k4$lKe4`E0U2J?yhxMd@`nRO7*a4BWq=6c@fYldN92ASGwGIFX@J61}!2Z0Z^`DpVb%tYf=du@stjq}sN#2d`8 zp=2j1;rKfw9~!#_fy`78wjzB$$`rj_<-)Ypr>n5k8=24;JTqr@C;Rttmly?4)_SOZ z65RCRBs3IdEt#v6@P-@PKDFXqBjFiTTe#g4`!$Zfg<~gG7mnGgg-B29wACoEZrf4L zXWcaFgDOlvX~o4gUR+<}!;y`4Pl_Fr3>Q_s3>j8~{B&bD>=D5E&uU9T7nt{xlC>XO z4PNP#XhkbjQ1cK}ini!b3T$+97z{E#VmUQ_3^VETA#j&SuoTxUBJQA}DX6A%KXcK} zz2HF0Q!ZTAQc0sjEWG_wP)Qfgf$IT-r(i$_Gut1=J1>{xPecw^$Zvc z+{rohb54@mJ;78I%R+vgfas@}L1Q><_X{V~c$Sra`7l(j>EV-6-B|GvlzTA~8g&|h zJ!2Xzm5nQza0WCs#C+5hS)7^PA0chWqlMa8}Z`Sw3B}S8eA|y z7hi%}@=dVV{y68vbijcv6FxN3Ry@_@+AY175PjUR4$Ynv7vY_}CZy}KD=EY{`L1kA zQbZ@8jI+Nebh+BZ)`wIhD^*tVyr5MjF$#%`YfW=G^;t$^?2yVf!%Rq;DNKnfQZ)QN zl&{I7HZf|PO#?ixadwmYOsWvwN*sYou}$PBxk)_(yq5gq7zp@-2Cw8qu9ap+5i@gK zil&0=k+B(1YUkWMv$Gnk`O z@AGjPee>>RIb+S$0M9g@KK}$Y9A>$Hd62IWhAE4vb{?w)lh-E)1hAY=G=Z-s2RLoZ zvLaI*6V~qs#hnYB)Uplg(D{vZZmXuSmtCdJ3;3|KQ+~}iM3!PYe*mgs0fvtU78#6e zwyd2C5C&wf%$rPJAQ62$rUyuakbf5hin53p?)MZ6zL57Qo!ki(GomVlpZw{^Yj>RZ z+_zrQDqa2otn&qWkaLqclu2*x)WZzt6 z=DRO1=9Er5FMLz%<|Ub8nHcJM#_K3Rn5%0rxHVzk?^ec24%W;1l;olXBuc29Hgo-w zBZVU`WonJ%y0zulc*tIy;uc825MKN%K=C2&n31;O=qej7zU9KxN1Swb891v1(Y#zb zr(+xZF6h%;$%Jmg(LiyJ9uVe_-Z}xn$0fSlpVD4^%`m}IuynLb=bw{=Nua^|JXyH! nnxCd@mU)>?Hp?x*k!ue9mvc>M5~6ge*i!L7MWYpV%f5dBaoIZ= delta 395 zcmZqM#&KpU$A%D-&0?l}e49(0Q$#0k(B<0vBtorrde;|50oGE69EP0fikytH(-%x( z)R=zYI3vrnbBr9*&wXcP6HR0&Vn}AlV8~=BV@PFCU{F&~VDO!+I8D?phasOKk0Bi> zRsxjG1l?X{d7GMprh&}7*(dni7>Kl_mW`bV1x@wY%h>x+{ZS3 z1vit(b}2>1tjXJNZDF*NVozeI1X}Ml+0jIJy2W|M0A`@T_WtvX%_7?!UNElUnjY|( zQEWQTXGVd^6Abt!t>BvcU?10XgU^ijw)gyITqkXr3iL|~)KQs0zh(f9O9bL#pq~=K lK2iYstOzPv0CYIW|M?6l(+wq<9Jk*PXL`pty+w+t2>>GTZ5aRn diff --git a/iOSClient/Supporting Files/it.lproj/Localizable.strings b/iOSClient/Supporting Files/it.lproj/Localizable.strings index df01daf5cdfc5807ded3f5cb5ba263fd0ebfb887..cf8aac8f370445f2d76078f5dc1f510e448bc429 100644 GIT binary patch delta 4146 zcmbVPdvH|c6~AY*iOaGqn`}0lC9k_lh@dor8buzG2(+b5c@(Wv%1Bq(z}986=7H2F zWvI3cO@UK6PAU~TC>Ey-kksvTYzI2kKa@JgaYUm=v`Q(6LSKVU?ezSqhrR>9i}T=-pqyPn{)p=&oYnf5a=M+2)1|-boMhkWc)VD8 zmXnW9EDgp_I;Fk7t^3yMm9fvHm?_@3@S0{1_k@bJHND&8Ilu|BPQ=8K6=rf0aVK_>%@XwUYvUKWpj@^4XnUd3qZK#kZPfNspI``lio2 zRL1?ALVf3#9o;DVAJVN0-(KG$pH!1y`hQ6E;*WX)@@kxdk*s3sP-H9ho>WY@yOJoP zC?CGgFCSFX1b*;GwcN~p+2B%WZ2mynNyw3xC@j5yB0pD`o4w`KA@p^s%Wp3q-(hTR z&)Y2TRO?&V_((IA8;4z;PF6jxzL(wat1Z}B`7)K}j=FTOPfjp}q~XgnRkAx3M&(w& z1Zq{~A9|<}kNn(tYl#0jv9z$!(Z5{FWI7gXffY$-iizV|^_8O4{}I;(&OE)Z(kJj_ zTAy|6%5~aw|0=yYOtcd`ts*Q_uGA&z3_vD$HS z$9B>SlQ=NPC#}zsPZrK3m(OnYvm5gB;yGoxpFc-#iB);+Yjm~Tv|VAn`fn=zTBk~Y z6vRXG%2}#@Xfs&`PoX+c4a1Q#KEmFWdIFwyo!ixO?ap21bO+Dg7v-(10+N26Zjy$} zsPy5VP{|FDfjDH%-ZrypH&MzOTc8Q`X6rDmdy(aeN(P>!xP15m1?0LxIw2dL(zw?j z49UV@Y5BG_l|(IcK)NW6Wlo7FB5U5D8`=MOP)_twBTdl=hG%Cq+ORmGM>n3 zU)8O$!6?vT<*#(sIDgR7#jR0JIBgI3s(wvNb#z{G|oF>cjCpGon9HQ2On#IP3 z@9O!rYR@{Af@riE53hAx9+(l8@4QPhsy=sPZs1*dHXvs|QrfsvW06olXeG$Bda97$ ze?*T;b*0C5{c3%I0UR5B}t zwDXdequ#Wm@3x$*OS1qcbOGP99lEbQ}@QT5`(MD&vu5m5U z3(>^BB=1dBO~4v{w;?o2#`0vyc&n8~9s~Po9Y$;2Mu2 zC-BBrhV^mAyzSC^hvLe23B+0XM$Y}3!b(W4vxI*;7%k9n!yK9VH}ZlxCGkb72wP=I zJq)EthLrsHFh%M>0nlZ!XLZBJx+Seo;CLKMs{J(&B%Q0w3WzRX%!ddbfMqykc`ZHS zL0ATlLgqTFTAzSJG6v2xXR5BPUEKg?s?^p>(ga)U$EOE@JjthzR77kk-EM?gx1`h& z2TxP{s~6(5*dT#dfheU(l2U1xijv6z6+bGCfKA?cg{sDa5r-**2ljx3Prib;9M2(M1?yunHxnAD2Bv6OI+vKxJjC8|#6nV&}k5Nn>^?E7`kp?L~H$Nh~ z9#gj5x=C>^t9E5-xyQH2)&SI@#%-fP*F!E~glXTt@c$0jXRv%NGfR0s3!33N`3CCW6%=U=zq9d zgg^`As0vi4ncn1kSqhK4$RLhiRbif3vD*AO{z^3?eKfr z{oN#8&2D!YKmB&4WPCbDW{XtP48xccoo1bv^8C4!f0>9Dqay7bU_FuKrz&E_qq{aZ zIKkU;(OCZE%^Ew~#jsB!$1P*R(pBsURbi+@)3farpe-dO)RZ)T+FHR|4*9viBBV^I z;`rd$7*_}U+}c&iTW`34|Gy^Fb9YdrKn>t+UJLV^LzTSYV?RGSu4Mcw&GZ!teU0u`IS?MES}q=n&jwYY z4lxGUVGQM9N%?^QMvCd!^3bz2^X=g9 zxgRPTR*Y{b5m1&1ivF%uqm+?>l@tF8kB1t>CaI*alcrR>q#W`hD~ox{CvbXWQ0^rP z@Xfs;>32P4+`PZY3_H<$FU3(yudWKn@R|Fn9#}B!*AD$fy^JDiRfmXRgg}qE+YmEi zWjh;61)PN1hm#Q!RPEh-I+fjKyRn;yf$27ma&P1kTHs05$@31TlO zOxP*Kz{;l!5+$F#U|UlVOfOF4?4DZv^4Qj@gV8s3;ag^Z5eG9tnf|bT_1*Rh3Qo6! z9w-BC9C&PIfVa`-fuzg9h}o{DSds&>+;6;|4+;wx;bHEe-;<2W&?)^mXnlzM$u|g<}+L0z!)({Q&N|+~|UPZFbsDVMl3892q%R>zOOPwVrF~B%fOv9&g9-l*tcd zNAa1M1nN9p(=elAh=U0*##Ri<84iUDFFu;cADtwh)Oi|5(7STeF)E>?e6*V))Gl|_ zd#bqlOaNLYjj={(*O7r!`jPsX)?WXAS*v32srP(|r&fD;VW6@QPPbppzQ*BJ{~uC< B5{Li* delta 2887 zcma)8TWDNW6g}rAO{aN`#xxo8sJ$^#f~M0%)B1#VsHRG)X-%ON@-vyalMGH~rZbbK z6?IhbL;vK02ceb*^v9ou2E0Gg$A_XVLh(l^)wE5luUdQ*1S!R}&Y7E;38~63Gxwgm z&)#eAwe~*u;*G#N=K^!5x@ANS+?Flvwm2k`!VxpV78#Kh1>9vsTIA<{>F#cJ)935l zQ*NNs#&RAeGx*QKJdW#x$iZ|G5^YKS^So4$HJ`T<&o;lg(&K<8;xA)7yeBam? zZ>k$3GPpJ&rbSAm)NGG@$o;LiLmgX^0X6r%tXExkS@mw)_4RAQ0w4G6-d5K>+V^M} zL7v90oQMKZ0by-%5cdwC6tG4JQCvHs57$KQz%HrYZMHVAv-PH!m{eDe$yT-0WZ5#T zy5E+elM9ClTjYThrNmxcj3?JrCG77Ne?jx;!Rp)z~pvKbq5?van5STjo$1D~)?%Ak8^2V~aiT>Hr0?**fzH{3o%W z*d|2OJ3TAU_j*g`q9L96<}lA!~p zj?8K$$RhwVjddR^vo-pInz2r9mYV@rqZ$m=m_|kN>izHJpWCBwYw{$@GF3i_QwTzd zBy<&w+(=Iv){vxHSdhaT_AN7=)asmofnS2C`rVVA1JFBmFt7>l!y~N*F6C!2 z&gghGWl_tl(jfrW-K0LcDr0;9pEK%&;f#AfOVb9dV;;}Okuj8`PW+0d%Pq>?HE#cX zwd)MOaWSo~9F#51Y;)5{aa^Iv)#5cdyozO0m8CcQgZ!+qbyN=|s*zMoXjR7D*`Gsd z=%zgCp82wNn6;&1ILBzD%Z`78gTs`wh0`%zstDV`r;qKjyhj~el%cI0c1Cn2-!xqK zLxD)xMj z-|H+tPs;Mq7X={meo>a6VK{<3`Ha)R9DL?@TRu^c+Wq25;obYUe5}U%L|P9ts9Oi{ z8udq`)#V1ApnLpCK)rq%$9zYVb<=yW&APUxVYF0SD8_5;vYgP)7Z8N6J7z$|b#GT5 z|29^#uptb;>p3CK$l{qESH(dqqIw6d!5$-0bGC6pjHA|ZO&KZF=a_r;dgptCReU*k}ld4@T4&;n@fzJ|H0kUX3w+|CycLZR3M6o0*G-5tHj<4c58J)kXn zg((M@88s&Mve>m?lTh?68O_edetk5LS7p#J?iIF5Z>9RFFJsj{HL;08@{t>+G!Oc` zTSOW-{cJMzRp-qRZJbhDw+*P5){<}Pr!NkyRGldbRyp{?BBxHzB}4r^O1_E_988KK zX+_#pW`sQ2vc|EDBdA_s$(yzz7e#~WuTk=s4pg_v?G=uFz_CxjI)!UUb&OKIw*W1A zL|$DFQp;>}2G78KTANoD&E=JIONzGYsdH3YGeZe&UgWWH#qyb)vmz(X6Y^LIl{$aov6mW$0#) zt{r5m+&EU>Op>RC{SBhgQ23zJi&io<2OG?Xv5G^g?suq>`XqfGR$W`jCvQ9xl6L|N zODHunP}sgnT{=#|W^P}`cC*i^i)&5#kc6a&%fN?qb#d*N`WN>f{;vUVc{urjv*a-RM zW6=g#dp072VUKL>42|>nvNe}DRA6=48!KK+Al8MMp^3SjGXOiDW?6XQ8Ic!Hh~3EX z648a?=M`Vuxv;elwswU}={7+I#!)5wA_)?9AuSFe)|U&FtBMKc@WeJru}4FeTy*0( z1&gwP^$AP6pAR(QO{R3kaS% z`s;+XtVOlJ^BX!NLhvGYg z&IfUNpueo@HEoBVf<b^>Kv*6eKFL;d1(^u*EKhQQ?EC2ui delta 381 zcmXYsPbh(RNK zWT!Yqsh0|6Tt-k69``(I>64vO{N-VeU9}lb714MgC~U3Ne^5 z2y3NYF&+eM;=a@%=ANrW_%$c;#~L$o?vzE!n*mk8S@)Wli~kyvDx@*dgo-6pqgZ@sjI>m-m(7}N;x?PzxVwoc zQY$UJKp{)UcC0NDq!j5x4Hdf5YSs8s+Ik6C=|j<|#RsvXMJg20&YZmkA4(}d|G#JF z%;lSJX3qKX6WcrA+9nUSjkncc=MOIYEpFh!73)eus*fID0PlQLrBqy*Y8QV;R6?0} zX3Bjm-B#zIgZscm%{}16mW~?x1~sHYYCwfmQXMhuu>)YW9I+ zeilq0M~U*AQenF4g?jwFeDRYy3Eqz}qM^Kwq?bPHD(KPgOTYvD7&=>(jpphoIw|2m zR51Zwy)|cuvmeEK?=?`951jL|D6~+8GaG%-0Qt=9RQD?wnL~cKWW~e@7p{EJM$Qkw zPG5Myp*P9(J?FMMU5R&JwBhXzdwnvOPcLU=GG>!G3^UoI`rEAdp@k2jI`eKFr1qEW zR3W<841w`-pT>h*%IuiFUz&OJHFPg1?`O+6=gpolw9ZT0&q4RxoDgBkojHFF-tgeZ zfl=>HwOvItl|j|Z>kyj`;HG5@g(YlRRq7L(&Eg(w$GFIt4ww+fp_bZl!-|&zKD-~O zTsX+OVKuCR48NB>$-fx?3n@md1`%vtbe3DGaLmf?C z24}sg`nZ)PQnrr5cQ1R1seYy+!s`@W-Or_e`2aZmN!AKUr?FO?HN-EUg&U zS!Rvj~}V4M0mmrOP_GTnh~v;%teye9Fum8fgci? z*6VQ8z|rp(=MUHarzrQUO z2Ts&DvGkgcW*&o;PHzS)Iy&t^slFJ-nSfqB>q2iIONw10M2S>DWji592W$1SWt8=$ zaaXBVIoSranp;;^{L((V#l1qR71e!RG>mZAWUeAv2%;ryNna#sItA{%1wU#HJBaeu ztOPY~>0O~$TqQYq={-Rs8I15+5R08IAO8yRakY(QVLdDY!_+>CAp3Io7h!#upAW}I zyxVOt3Pc%7Ugq9`2PcZ^xeA-sMSzE{0R!)c}_9pe0-(B>=N^ZcmB)Fgn zPo1o8H91DM)Y~{2;YfzEaH#WI>>_&H^vy#*y$RK2g=>h`Oo6*hbcErj>Es^pmxj4m zXg=^&1Uc1yeq@+=y5Iwc9`JntEd*G2f|4$M@S4e2pra_E&Qs9h7RAcAXiSmcJ9$!mDICiBU%v6eFAFyu^cjApc) zyx~00^aUpwS*Bn3!N`}9$WX+P%#gv5$xz0S%AmlYrl7#!%aF)W31pW5>1?oQB9NEE zkk63EkPa5lWXJ`I>jBv=45dI5GT|~HlZt`j1whe!pvkE~UNM6X!*qe4j265K3@Hfp z(|3Pml;uwZD=!Bs1iFwxZ@ObYqvYg2${d?zu0645O#>QIH2tk4qx5tGNk*RS_aqp3 z7$KtDfucXzCQnNe*?vipv1H2jIolXRr1+D-PIm)3BL`&pbk>WE(ab=>?dca8YelvT zy<%L!HC^c|quBI&Ul;`@$A$7ucJSkxp3uX{HJ#}zHNiw}-oZckE)C2&L({nli diff --git a/iOSClient/Supporting Files/ka.lproj/Localizable.strings b/iOSClient/Supporting Files/ka.lproj/Localizable.strings index 6017aaaec74825b8b506cd3a52d914537ade36c2..e4c7cddc8529e5f9177f0791c23a6dad9ab9467a 100644 GIT binary patch delta 2143 zcmb_dZ)}rg5Pxor@~&&Uu78%jY=aj!V9J=lh=MpNL=#O6AQ3}Qfo|)z%G#B7Y_Jdf z3lIY;jEkH`9AF?(6A;o#AtOTgGrtg6jfwk2<}iE$LI^=(2zd9jv%?R5GE3j}eeRyS z-~H})*WMk*&{5-LX&o+0+HlqElEP*cRGq3_#Z?$5%vmu$sG3w01?Pg_*}=z_+QiQd zDvpOP`0?IPb~?8m+{x;?VJCgj1s>YE6lx0JQV|Z*qCzT>ghefWdMybKx?K-OvirS; zP05;ccRjv)$b*4j?6l7bE_%TUb{e??RrK}=Fleou^*sU~UH%AsxYc2&awkm09Xp<* zeJ&`$sx2N|y-S~#;`YKSeOiz~7*`?Ow#O?sN^#ZsjE#o>03S4C{dqHE*uNlS$d=W} zWyRDcDt1C8ee*K|kvv&rgpHjlBu0~ZonT81xxj$Y5>ii!;G7M2o$=7X9H^zh5Erv- zJ9{LssMy4Zr(HC-9wuSTY0$lkkWYb6z+RbPe%kocs5+R}Ab;c}E}8gQD}OZe7L=u~ zmB5ppaa}4%3#Y?O9RAWytDWG^n`WghehaTp3=Uu~@TRCrBwa}5X;Kjg?4yoEOM?I{Z2j7XR7F=LWp}t}+Wq6W- z%lh+im*vL8Rw+JSY4<1Coyq6Fs9M8;8L?VN>Elp#AO!X)W6e^pHnX$E-*iUmxe0z; z{=Nqtbq;j&JIFT|s%f~5#Sq`gCFO4hH(3Kvj(tfV)}&np?VPHas|jOzc_kf=fZ-Gb z>sg{KKGi{!2cUwQuY#x8VkOI|#dM31gV?juJ42F(`J`2!w`zN%2wYbZ)ve(;47Y>Q z<=_IM)G+o8n6&m7)H)*^Afzv`Nncp_FJCr>^}R$kBS#*S`x*2cDxSLY z8+>iY-6IJNGMBYZISvBk1}HZbEO`rbo~Fiu!sWNBPDd z>iG`5TM~S|O>0#M%@7ynu!$_mnH1tBBuT#|WsKj%C#)K@2LB1F7V&!UPeMdSvp7#a+6d;Jqw@`??xu!V3&ch zfcwS&Bu1wZB!VZ(xFY!B0kd!nL72Yocj`G|`Lirkoa)E1{01Lzr-tW)9Rq`gR~tBl zjJ@8oysnOHE|XT}cY^++5@`Yq%`wvHZ_0_$=|XhuxqCPn|8phB{vb zGtXp;!b4o4xQBWEvX|(12b4|7zTlC#zd#&>u9@_7BbdAz?{tER4_KYI`WZV5vf>S` zAyLTaAj$_N2ONbN=Wor-N}FhceG(c|W(~<;iDxt4 z5zGm0x}}zKjK(n$rfYx}3ns|t>)1o|${U5Bp;1C+F(gSfD!)u)gd^!ePv+ ba2GIo!y#{U`5>>>mmtsmKc%6)khk}5tfw=s delta 405 zcmcbygkxR{#|8=G&9{s{@NSlJN)Vm=A&hHtTXKFtV|hGUPDiOgCg= zl%0IRMQQSeJdWuO?->P@6B&vak{L1>G8xJkQW+E&)D#pLd>IlMDuL`0Af3&SIz8|) zqoj@=P{4(u6sSCtA&(&)EDh3D3}h7mMe~8iqyl-x3_1+ccfDh@;8kEqL8zZRyIxvQ z0capdeK}Ai&^Zix(;4qEN^bslA;*?A4QN8q0|mCrA7^Y7*?#Um<20`6|D1p! zQ1gmWW_r|QMv=)I1Xv~;?B<&Oj+2RP`leTmH@0(pW^9qRP6hfY1?sj;pvN8Q5T8DWpGjl87C+NF#_87tnVJBKnRJB! diff --git a/iOSClient/Supporting Files/kab.lproj/Localizable.strings b/iOSClient/Supporting Files/kab.lproj/Localizable.strings index 770f771165380c3da6618d9eaa49c4aaa83e9c52..b1858ba5f215af99acb34a7448556a4b74c77cad 100644 GIT binary patch delta 2282 zcmcIlTWpk75I$4svMk-7?e4Z^yIuNIY6O9j2E(OXET;NE01JtY4@lW|yY1TTw(Rzj z8imw=L86EbaJe2pb{0oIe(Y#qc7&q`7dYY z%zQKR&HR^ly7wP;kN6hzmD-BwaTQhls#~R1g74Hm#;0rDdQ+DCGkURUt3llRAhdbS zF9AMz&C+uVspz=9_^Owx^t>jT&AzP_Jo198uwAvO%<;eLJ45LB3Jt;r>-Yy}(Q5ynknv9=52IAK2mN z)N>x$ok?j;^%U0jXRwg+lCRjon1*|dRq*i2nY!^GvgTxfB8ke3Z$CDEek^3;{IhmzO<=A z?>mfRP_}gAd8*I{zaW3HtzuxdxOR#k<(D51aKo{nj_)O3Sq9rtEK%qN4pHks`9s@j zp0~%u*=q6-(}$WUj0Ab~>D%Pja}SY!DyWF7m;XEwSs|sCgvF4!T=Fs#_Qb=4WjrO4 zZ3dKhN95VaC3@GRj|>8N%{V&c%8y_}aCcB!<20tvFC#w}y;-WChK!H??&iibQ>af5 zZJ;@NxR5Nq{k4yK!WMt{Z7J`obMce){$?q^`0oa;R@2<#H-?@38aSmCe1-{mDC0Pp zguYa!PHjeH_o{l(R^G65aX~>~hZH1fI6WC&0&X)CId>q4mZV0gAdq_SB{~V!V%U1u zs9J;XBsBVL-PY0bE#>8RKk~bat50)*U?XyzHxpbHtfltMv9d`kcD?fgGaw9nI;e#2=+Mq$oS5HH@12d_F7F$PL%f<^fu@l#eS!gN zGBL_dzp#o?W4fI!BiG~}UJm6%h9ZV!h75*GhBAg!1_cH+1qB9QhD3%+AiD%eXEUTu zZ+y%ssjmkVZ~>ZA1XP;GkPemxX)6Y@3V@>dKx0yYykZ6&ph))gvzr*jrwhDil;c%k zNI~eEzVJ1pEPo|(dcAJ+&^|8a~lfEg&T-Tyda zqsaDu_Zb&)O<(tlQEjr1KHqeQB1VzvFJ3X;+wSt2u|?W473iH5sCzPje$D_oKM{zF pf!<04drAT5xgw}&0np8$AjoG(nZBQ&$#HucKhrzL=|VzGO#mU=Z(;xd diff --git a/iOSClient/Supporting Files/km.lproj/Localizable.strings b/iOSClient/Supporting Files/km.lproj/Localizable.strings index e8e0ad3e5b472ad6b86ea5223829e5c072ec068c..1e526d8d919be614763b0991648791d1f2acaf07 100644 GIT binary patch delta 2209 zcmb_deN5DK82>#d9>)pCc`uxBAiuzuW`LT`98!}%DtjYqt(VnA<$x}ayWOwv_^A^UDo z;8a8PI=|fWDLi|h{B}3@l;oYdFq*iVTmCMTExF{9zg*;GUvP;W@1s09d4qg>@pTu+ zr|f*R&nf#|Gd5GNE#a*P9Ma!MF8*kbhvOY{jw!)orKPh53tIr+1pIm<3UEzEz3g*Q zekz;Ry{O$YYh0Q#t}~v8Y1FF3G#aqpi699hY_!aEm*K;-LRRIHdmjAw>ka-iNJ%_I zX_PssxJ`(%b2`Fc1T~XYeftG+4feX}gk3t?KxN0|IvL5PY<|UEz)!qc#uIS|$IrP+ zlPDsBPaSZWKxKmGdf-A#d{^ir$Nr$;U_*dj=;pF#9n#)M4yjrWE+-z$v#|2VJF*8y z`lza4K?A&EpaijawZkuGz9XMh4pUX;^ofiOlXr0cFhzY_WA60t0x@9=LM=cjNeQEo zC(DbKLGR3&Ip?g_72u~$Hz!OVUw^~NM?bXl=C$@5r811<+LMUv^H5OEeL_xu6296i z?N%+$cHy&w4}BHnUmtOlYQgDqrNBCewTYFLU%JN0pQ*h<|M<4b0F;(}I06=WI?0rg z@1ded2l;X}OA-iF+I48k?`Nq>(Pfrd9(H|nK(;}CHf<|fg%Ji&6#Se zHlog|r2*W8v2Ky(hsiISy2zcGgw;0PDI%}j+(XN9<5qazcm5krXPTA zZ!dW=p;J69{MeU8cWJ|wDcH`kmb9{K2UH3zs9o`BP0bcIWR>PyXrAp7;dXSkA@IZ% z+^Y|+kXzo_i$;%Jq08K{5ZC8O9#0%`OU*dhIa=h%)!?m09L2+N6l3@pPeKoF}K7kjkxAN|K5q$&;1d`_nP2;UD>TO%`62uV&Fm zNz|*N3*lRlw_>dljqtI8{2~)8tx)aLctR=M+`J$k<8v9i-^Ea>p^OcB9rbEqXj-@A zzsb?S)_ejv`3vR{Bh@#^&ow!Y`P%-34E#x@yeG%wxBkLQhe2vM0h+d|h^t1|OQ;J< zhFfmP*p8bN2acFp$~W`LOC{W#Q=B!s$)h7)@!6?BU3vWRf_W%kx(-6aBVjbU|GZsi zM@Aq4kk-vI%21kiPJegH%2gPt-3Q4-Wn6wbD7N$Dt5zy?Tw!Q}QZ)=?n8F-LTJfv0 z(a|+CFH!?n$)A4Z@IbpaOZ}9caygWwz`S~+f@70`oLU5Fz@rx~Dr09TsB+A_*NPbB z(M8@&9pT8y$xBOR>(AtSS!rrSQmvuQN<8~+LnKw1LQbH=nsipLmSqEcw3sA%ZW@mw z28IS!sR+w0h$g7shW#DbS1nrDU}bS+#41p=Dq==xDI!&{+QZZ|RHf{)QboAy{o<@7 z5@@Ol{$(mCW(O@1_gUP(p`&D%pI*Y-circ>(D$vvO9=6JpxbYcS!E0!AE(Y>R+FV| z3<Kpb?)TYj@?G&jpCWXmC~ zoFyR*DcJ1tIBYZbaXiayRaXC2I-PRNGfdOmIl@^Eyi5}G@PR{g(ZUg%4B`JevM!;+ zD)W~e`92GITJTk~PudjSn^Dw?&rVvDP}1M6DB&N|2W(g~(6Aub^&1y%o$Kl~23*GV zu7^48a@0nvb+nVcgffL(Xrgi*2m82A*%I3Hi91Z$^~M{XnJCjlkM_7GQOM2jEG`r& zP~fA|#8=($jYYRV!d}C@A>+KG7?0c8PmVn+UB@yzS9>(WMmqaRLzs>%!ZfuGuwVCD LgioZ#pm6*HlrMMX diff --git a/iOSClient/Supporting Files/kn.lproj/Localizable.strings b/iOSClient/Supporting Files/kn.lproj/Localizable.strings index a7cfb623b8d95d02e013c0fe38cebaf55297185b..95c1539881de54424e39a88446475756e6668c40 100644 GIT binary patch delta 2134 zcmb_dZERCz6n;-BUDs~qt{-$;*R5;=1A)pQ4l)o*0x?7s{Dm5Htm`)F`n9&(gcw02 zFcT68hj3z|hN=mK7`M1!BVyPHAkm0`B!)ja5fY*VgCUp@70-Fwt&6|>VVZk;?|Z(U z^PG>nYsh-)xOHS`Dc)UDhiiW|u=yzi_b&;}>`)OE!Q4w8G9Lv4505)>*B#sF&{B(| ztWmYAsEX6Z9L&em1&4jTimMLQqqnMGX#U(pha8bC=9bjo{{l3|uv4qXiDAM)%5+ z8x7;p3Qs|DxS}MfECh&in1O=xn*0PO1Cmv~q6$t~s5Q;_TF*fproOMm3B!EoEh}UQ zs-TAUWMCnV_^mkcm7^)cFk^gmsw{)<;7d-@;+gM3`Q^`e;k!fB$|>WPdM6&WRO|dr z^8)#y6R@_{-p#5>&XgTsi`7G~odY*LJO?YwiW1752UmW>c}TeNcwoRYsJ3zCByKxL z-o|m$s)usBp+V^JZJrVbN@6xhs2KDf?Mxc@J_&}OsW*)k1~ja%Fr7l z;PGVGQfeSoXy*WfeDxxn@lo&-uq~S|mb|r?l>_`Vi7miB(=213s+(nLm8LKDf*?U_`4S>?424{H|HBgsPg0Gg$^+& z?^FHkHKVdLd=VNbcnOTA0Qb9`N^EH(smL^Ye$L`}+~e(JEylx|30*n%!n#?ist1>V zm{*z7(t>S-o_ijA7W*wg3!CaJyf7GeITIMnf_F47fQTC@+mkj{@@ zhhZDFrFk5}H(?$fxe341wKj0#5AV*#m)|p}{sXXJ@JB0d9XI2jV->V=6I9ddC&7Ua zYaMjCk00ELYP$R->)j7IpO6?Cj*(;(rJN#sGQAFkQVNt}5cjA& z$+b!Mlz0>dg`Et0j_Ga|Mz`tnD4%AEEEl4SmT(|^XRu|3YiG&l8n%zIq@Q~%##*wG z1hMq2L=jIJmPsWdd>8Ef{47|-ZZfLWBdNlC)}=>9Q2ZNXq?k)1{=C79zu0X!l&Hf{ zz={t-#^e8ut3prk;=zqnW1q$n=6s$?%^wehR69*A7Vy)j5gzRQl~%f+1!HEE;qeHu z06)629N+i3y(OD4Tc()BBxjWrYZo-qa0I+!Up^(lHCJ4;#{#w5#mA>>Y1b>fI0N74 z#cB4YnVy2vg-n5zQ2w{mqs6?QUOWI!UQivI{CI7t(=U_?Wm&!=S})4O$l45|pOORM zE1P`j=mk%?coGU0(fAIifglaP3N`j5+vovQ3F`ZrN95AmVAWkXXPxd2sr?{>l4`MI z{+9~NX%uf=pNrx7Zf}wu_vq88+QA`;aq-GJ*G>sr>Qa$)>-tLv2Kj9x`|2#&sK<)k z6J~#%s+TpS|9C_2y{kZn#*t>pWN#HH6I`;bi`$OnjN8nnG5qaVLwSaokx)T=`VSx7 z-pijh9yb1v&9~slJ7(&R^Yjkv=Pfn61&n&}DY2*M(HQ01BE;u+--5%w^0;nsriAW& z2L5FdU-A+pdOsBBD$J)nI)lmbif73{&jv!9_lU`~Ab$`{cL5!3HhE*fDVV?&4Wfyb~2hy zZV(cf?)QOFXu8QqMgdke1qBA*$^KJB?Q$6M8S)s?859^w7%~}h8B!VafNU3rQidWR zA10FlWETU)3xJ~e49P%M#XzXTFg@-gqXn-5LkdFublbO#viyl)<>f$yKo>CRZGL!h zy6toaK1PnoUoMGl_Yh)~VMK5ngc*0SO}F4?64|aM&A4OIc9V6CR#NOq43$7D-6lI4 z2~U?h!6-6)(FsNyW}xWyrzaR2MYd0Q$hd-Q`aTy%uIV9f7-c5!>*AU|L4=WK`i2%D z(ej4z-u4Gy82h9xQ-S_SfjTG?=+6wGv57!j4D?ST*f$D5Ull<`3xJLW`8}T@W%9uf WlGE!1nKZWZ2r|86oW4VtsR;m52yKu6 diff --git a/iOSClient/Supporting Files/ko.lproj/Localizable.strings b/iOSClient/Supporting Files/ko.lproj/Localizable.strings index 44bf08a055d14348fddcc7de0a4475d28abe0d2f..27180f90bcc3041aca2c3cb696a64d7931cefc02 100644 GIT binary patch delta 3072 zcmcImTWnNC7@k48ly*IV#g>-d4hs}5rIc6;fhcW?CR~C-F-Qc;cDF4XXt(V4Vo6|8 zh(RBOI!VW9)aZjG*eKG6YNAQK@nTROh%ah_K#b`F!GsunQNM3yPtUfx)fdxjcXsB# ze*gFXGdF$+eEVnM;!ms8Xid4g^>#>I-smiQShylA_KJk)$0~;J9v=B#RUB)xx=oh{4 zn=tXHgEzza+B0&w{v$2}>;G9HiaPGK!b!T26hgS+RTN*Lq zcM4_`r^g=n?DoWodJR7C(2y%k;?7Jhy3;W#HVi-mQNW`6o@WYJtumLYN)$v8%Tx^9sYF4Rs@S1L~XWRr=%!S?;vj4f8)G)YV%h zYUDLXIenF?>C1c-+~F*v4q=P}NK(r6tQe&_fj`}LL6(huJ-KT@Up^=o=#m3Anb6F{ ztKIS9h>R80J^_;vJ6jXb??c>EDK2Sh%FoNxr=mAE)=3yk)0VU#ZnN6m_qN&|s9dTq?>$Z##;q`?G zI15Y@EVY*cn<9yQI_kt33E}mehg(Xe8yD+qXVJkEtAqOFkj!63k{?AsvW3|foDSSe zA?SXrqM}2eACeV%a+7q`XZ0cV+wYF5KkXRxd#9;WuuA`YK{~qOxD2(E8c~ycC#oCA znuHm|iu1>B;8#B22gvhgKnL?-mPdRbzEuzq49u70T~Wf4q#di#0lY~x&H zMlR4vm>Ag7oCB_jjz~`T(`zu8vI)t!kxc34Q?h8`w5(w$IUadIw&fRQx`{jblVSOQ zzB?>0sixFi_3=DMxzAT`umk%sm}3Vqr0h}XV!sD_bf3TwvFTE(+Eph??pNKd zKz-2|N|UfWN&r_MLDnyu75?D#*x=88e|T;X``~zmXhQRH#SdGHBbQ0}oH$}{715VQ z33(8QK*|t^)MP-U4jJ$L?!_<4#6`O?>APsg?<0`Q#();siL_0-DWJjb;P7U0{aCXc z>#6AFVga+}IV~8}%$Q@D+*D2~19((n!B-|;bHC~hW*XO4w7?;=3P zwY{rJEdTA(i+vJy(A2|PVd+<6zaMy#_#j~rQAe@Gv010Cd{wIN%aaRK_>$>Sef^9K zq#2So@HkNIjmceROigBQN0A*j{AH`j9?C|7zNkr(beVNr9D{Ur=$$zAUPLKTo5rC( z0Vq0k7oZzl@i+`(!cUo43Df-54)am4%+5#7ZieLt!xy60mbP?03GHQP)4m7&trDlJ z3Q~ZVuIhv?$O8eK&3F)Cl5tAU%`@$)c7DD{fB&4!f5@BC5i=eV0MNERJ=sZZ?-Ryk z-wUYv_?s`wy`@ign~_17Ju4Pg-Y8a|1BKbyE@S!pIYJ(F899%V7H${&J>zRe<=Q#= akx|(&f69pd@m;xi|NrP!#B}R>^6X!C600o$ delta 400 zcmccipY73Iwhhm;r!Sbo$TIoBZJy0LbTYU%7g)FmO4a zJaFei%nk*HfGuwi-8rOk45)Lmqq+FzYa7IjCnr4TnEc|A*5n7zu1TvLi`c0HH1_Dx zm>qYvC@>hTd$MWGPLQ#JlMmKPZvOZD66@v_@6yV)r=&7QiB8^Nz$Q|7)aE$I@SB?% zj~zUqz_8}{-2>BKRWZ6u=1XG}K$qDbQq5Q;wEfRS#t4q-NwXN$Cf_^8H)$T%^hvWA zS8U(0h%rdgGL<2ZA%&qB=#FxROokGM3?M5Jh>IBtfOIlLDv+JePy`h%0P>4~>hc*< VrZetkblkpT592$==^^_Wn*ebip2;-ad>zPwmPuhCjFF7QU@*wVcp(OLT`yZlZ?+wCMnDL} zNW1_Ca+H7$ql7=2#iBKYm;nJ30|^rnFdDgNj4{M08ZST0Jm+h-5={JIny-EPp67ke zbI$v`@3(u0@$+|$Ben(jb!a-yAGPBfE3Ejz{4N{~8F5O`ZYijY($o-t52zdt{y3BV zTnXl)w&0otS}42*mNl$YDHT?|DyH(qzq)!|G_S~r{>P(3_)XA^ueF$p?PJ}`u>J4p z#H*~d)(R%NbOvV83JbVvIR$>%;-k@iFyV`46Ll6VAl$NJ7LL>zXq5$Qb!910ga#L` zj+AL|dqWjbUq<9qjIMjYQ3Xk1onHGS6@Bp?I4yBjy@?CUse~%v@`$f2?9_g^M^9L( zJ;IQz8lq4Uyk*3`&!C#Lp`0|us7*pr8nuEIq}bBUMmS-hcouB*@qCzvBW@#gZ3h$N z>3So0@zP5{%vy}p_6k^M=DB}Kej=*C4G;4pGdWf$-)-_K9d?6%Z|$H8Q>Yc@;;>=L zWJxu%wbHSp@W8Z&IKwl%B}{@Lql=E70w>Kr4V`tBNGWz2wmGpw+vFNleO$bxeTEbF zaoU{f#l!n&2(Ryn$*5M1GCs|i1zLdKnF`H#q1Axbelp+|%~s2e!aW%m2A?#!^1Ot1 zf9x78cv_ip{M9Eh6Dr_AZ-K6=>>N@YDKpV4td~ zs6MN4yT##Q46JuxOUl_A5vS|Y)B0BMI+j<^rDar{u>}?wqhB|}oW`sU6pN&pFoEHK zmpfv_?x2CX-vg~K#p_~hud(BPmuzkY$Q%iMoTu@N;F5Z5kSkri42MkApXCZ)xB;_i zI^MQEl2qhO|MXSfxGR1@y*FkrmP?w7sH zv(^mftX32)x0g;n1}@w^W=QF!@MN5UNv=S=)}c0VHFv8n_MD95S*x2wC}oGFsd;^c zxU~zS7omdO=Cavcq{CJLGt<%DJ35lN=e~K1=Tbx=b0xPIf)+Xe8|Rjz;9lK@w}&`5{o z(l5_}7uqOP!#CNnD>nL>_z<%F=;_N~@W_@AvQROYygSO5S{g52nFB!@I}VPma`X0b zqtgs~o+&3^Gd#N8k9l2I`L>uih;d#cvh%#xnp(pgk=qzrD-P;m_K4cZ2=QQ&*Q6Px zcS2>_SXGWg69$`TxGb4Gog9}c*r+&uWJZ0S--ME?jjk^OA2sdd7lD*KvWnkmJKpD( zcb+%WypO=>2+JbQJIEQuxlE6ty)HwB|HV}4;J2{LSD(@?&b=Z3C*WTwsih`41JbaG z)7itby}Eko6`PV{(mjpm)1{}zAyh-jacF0KS-s>K1)#=r2mSs3i(W-(8`Ql14+Eqc Ai~s-t delta 434 zcmYL_y-UMj5XSE!)EJsJtyO5PlBqg~OL0)@pop7LaOh~OiMCp8XsgntqEy61#DMRn z4vr2&h8LxRgI_rL7pS-h6{?6+Col19dGCFGclU62cSqt*MLbcWtg(TL^&EYYE&oWe z%{;6~Rym=SSnLwrEt4<WsAl^rGl)Uq#e>1jIfw)05N6tE_0`wBGopj>1ZyI!VUdL`( z1?}H}C0?;pPC5N6R7*3P-fGaQ-_H#(RYuYL^tZF{DSB93Mi-CyP{h9kPx`UyVqyaW zTnwSG%ag}A7;2t!b{RAArInpsW4ws~_ub%HfL}b~p2V~oM$GbrhZzz|X4jU)_G|dS z8y{Hd7|>}m?PsElGr4^;m54#n-cO}H^Yq%(AZ=eG2by{C7G&FAp@)aPLLJ$yPgwr~ Djp20& diff --git a/iOSClient/Supporting Files/lb.lproj/Localizable.strings b/iOSClient/Supporting Files/lb.lproj/Localizable.strings index f3e764294954fb882beb94499011609890ea4963..0fd771d40bfb44f919d3fe71bfb3a4d2b66e6fbb 100644 GIT binary patch delta 2193 zcmb_dYiv|i5I(al+hx1mz1>G&yIXFdl-NKUsR9-dijZJJVlgJfs9D->3k%&xcMDh{ zKuu#4F>URbOpF8?`5{4nlz?tD2E_0%MpPOgVx);lB_v{lgb<=eojG?G8h-f8H23!G zIdf*d`R1Fs(}RwqM;+snP59F}2X1KcspB8)0GbvnsCI)E3pLGAAy!^aSYkNj6%)>FYiW z8LktFiTx|_^;bi5&;u3p&IPEYA3RV-1Al;jRykk_+-4ujy?3D@}`Nlmj!!&jC)1i8q;5L3_C(XFz-5`II zyamCmzXG<67VTma0ei`_lr|p&m#xms_8x<^RmELwo@D0P3HCn|psi=YPdCoOilW>} zcK$i&_v2l2XJ8l85Yu5J99Sqxt1awvj_~R5frmfr+R7w;(|GhruY%uvHsBVxMFSxy6d4nh^Nt z)IKQlq;!&&hp39-8DHh7(@w2_F~Eioz*`jO%bi@J($B*IsjGxyLv|F?>kOT~0)bMY zsz)P}m|X1a;cIZjNzJ<%{+cPMqqY5<X$PM<3TsA-wiIhZSd>+4PeLS7H?QeoY5(DFd}JT2_NMhCZCd>X2d{zp*OhX>!yPxxbUb)8Wy5v=KCO)vpt^5jqI2FbSn0`CWB#~GFk7L%@j z3gM-KM$!~qLSnvc+Ie@2PAD%5@hti1hQt1NIZcDDKD!re(||*9r~WxxT2lL@e{8n$ M|0x9oSe`h zGx-9q&~%ZHi~_7`3JMIqlk2C5+T}3hGvqO(Gbk{WFk~|1GNdx-0og7`4hp)%Yh1k_A}^B78DSl?4!)J z+2&H2Eo&OkjH2lag&C!%@iX#lpCH7@#|RPJ4ivo1Hn}ZPWP5@%W7eeY3hNlHq}Y=f zDuI@}O*S+Zo?dl=F@PB;u>Il*#zv9t4G$UTa7~}(!pJq<Mx8{-v|?+Eqa9S6wQuLVDkNpT4-%QSmaKdz7t$_>Za@)s1zh zv1?PdKEKYFZ)2|uy8+k)^`%|Dg4FW?;~9rl)%_xFI!xsQo@Z|6^RWaK^3YbYbaUtI zycbjio*k-9B?i84Sm>j*1GN3qUVW{=t>2pK8t~lktl`ZU$eIaM?#d@WFTPCfoL+c^ zRJR^4uiuwi_=InYo3yfmh2 zQE_gpr|Kuo<1mdPZp4`9PMYib!TFQ6{LYW$rFLC0SjN%*De>2=%EZr4QPc{GV(I`_ zU8b@O9It(zGI)6@(+EuWo4CnsZYn0BBPn#`@VjBMcv~ISah->peE3yz%}6NSZ8Vbnt8Fz+44`GQ6GzMCcd>kU>@N9}=e%_D6njBBUz&l^&IP!@|*EL0bymN@W{M`_(&PksnOTM5}Uj6VBhiWTpI zq#=9t-A12j#t=-FPc;#V8WT9!SWToP0*>P>3O?Em7jfQllq$5l!OfdrA*X(-!Zwhn zW8|!oi2E=TP%IqVpnBV6kx1*km4}CE4tsw9CDAva%+s#{;=SdTzG@Xu!sJhb;kUnp zOZio@7RjgycxJ4G7a6{G5E#N*MzB}JuR8hb6HtKU5rBx#Y^S0E!JSM6b3hFxvtlN} z6iQG$`O$5v;#ePL@rK{Y!}iBim>+>BW&!AkX244R3r1yY0FB! z%|?ZQHP#bkh0PFTP(nCQq}w32_7VJaN$Ch-HZZqc#weur7|2YBMm&e5KG=j|B^RUA z8!%mvsq4Wvh6JJ}Q4vrN)>4f4O_UUs#RGE&Lb z2o@U}tSJPpnogh;ZmzQbATTwkQ^@1SE9BG7`Oa(+Pn`e0NELkgA*6kBklfmH-QhC_ zz!Y_w7YNn~O?UjbGxVYBD>(cSx%A_5m%vm9QB?~V=!C7<%UWvfFboOxbNTf_Dii43 zNi#j`dXdh$Q#uXu%2sP8$m6l&7@+7Pa{6}|qNC};YQdN&K>iQ#H?>s8KOLt+T#Lg= zKSwW8X_H~##UQ^*E$8E#-wb6^8ohuD!U26qL7-*PFso@lZ=F#kmbZ&iHitRJVbp` z66CG@@6X9ifF=pGSdaYb=YvOSo+%%2*^rfD}lMfn3>f>T3F;*d)PML{$K%?(Z0v>%WZt)ww&Ec&_O2jX5s zg>j)NUTo z2K>g?wQ9=wQjTW#P`H+01Co%WXMsb;97+u>p@@u>EZ<2os!eiv>nCXt8P?W6mlv) zs~*Jaz7;sdu~1hGwm5x;3nrUYC{e+MHWsb>3hw)?omVWVl(#WJ6E=1mYqp?r1Kq~N zjfY}3zFD_*JkmxJ%oqPFy4s?YpmT7RIrSE$WZDcXO*PF=HUA_9aeDM9Q-v*bSl_Br JLn?Ze{4ZGsc(?!n diff --git a/iOSClient/Supporting Files/lt_LT.lproj/Localizable.strings b/iOSClient/Supporting Files/lt_LT.lproj/Localizable.strings index 72066e6f8768fe1966cada5a297ac6a044f4c8a2..68e6101c56fb42a1ae633c350175de14b61e4196 100644 GIT binary patch delta 5272 zcmb_gdvH|c6~AY*yV-0u%VrJoHCP_a}LDz6tEun&Bo}2aKkp4Pd2yS^9L64S$?pE&C3K4+2WP z(xS8}A*EF@0aoRYi}vL|>&1#UEEpW)POn#dN;~hiC;_az)q`8cn6Z59c#}`5Q|jm+ z72u(&7r>D*Ku&3mt{VHbnab9J5gO=F9pvHDUs$7CCR6~H^*M28Uv~6j#WyL~b(;~p zYpvkJ%Tvy#l|sAHp)@KXn)V1x!yiqp!|u#TUZ%8nJ&D)7|;BhKRQycXRstrS=wAA8T*SztzdGh2JEo<4*3>=43j)9=B~24i|e zQ5gfD5>T3G#RD+>=2Bjm!H=>LWQ$99vx%QoWwFu-t=hLBLl$Ak{JgW6KYdChdRKKb z;DSdT*f-4)J#otw`=+f%42J9}ER1{lTx56#hM{v_rzUjkuWg1Lz@|rY@#WRo*f(z& zezn>SVZ6WJg}mv?0aXc$VuqxH@i)O_I#dm=X#Me z?tkY;4GXFbSXN>*1!WcQS@b;hKL^&)$rwbumDNk~8~9Jew-R+epus$45GY5xni{A}P! zHV{uWYm)p%*mxsH5@6ku=YVoAe+wu+=AzgVWJIzEeFt)I${&tTsANA>-Vs^mD)0OG z*#|dh*!g+4omW~Ec@`uExkmnLR@$Nq5B$t-@0_aQrlL+0Ho>O1 z>IciHNtt@hV2C9}HS5t5ha0|o#FISIq~w5*g!vlvz8$mRiE}pGIoui@d+ZZSQ3ET~ zdG2G026jcF*GZI2$J|p_+)_npdT1O{^u9|ETV-jp2Q1ii&Jw+N$utob?Oj9b!gzV>AtdNewKFFs|57?>S#l6bV0mW44hHShv z+#yZNU;^sqXW6!)W$iHP&1bpMZtiLCuFIwU{6#woE!0y5!;{;YVJ#t&&^j%^R@&`` z98DN^{;3$dt>!WFG{+=aLIjoF4e|fA%2HmJ6-n}-9;f(|T4={sxQ>e5;KaRs&Z}-( z@vyBp=?B;LWm}HsXl#oIY#<@y{!Dx9SQdO?ruMbqz^pIFVecFxhF`H71uE>$%*Mr! zmEh4kE%e|+4ED&S4p$@|kxsNyh5ylo%a9lAEr6FzbUqUtba(~0Gle87yDu$1%v_Je z-WU%5N*N-(mV2qjOlB}0TmfD>*#Z`$3AlNLH@RcyN5Kn|w4M~8?S2@}?Wd=XrRtY} zlaAd3BdIqAw%E8OU;#Y3!(o%$u7nO|j~B3?A)ZW?^)P}umqHnrsq-2EIuZxK^FNRiQyXB;PzYdi z;ZW*a&+cwn2UAk;=xIAPG-uJX%Q%+)pMo=1@;Ho4A;$_ZQ+^)U)xlKNi^R{~G_l22 z4v?;Nf=W+!LdDfX&OE(@bzv#h7Oj%B4nmhZ;l89Oe*AMs+Eo!+wH^wHU9-ftY=k)w zzc}dbHgMdOXjma-JV!cyYD$zVW!|u zz+8~L(Irx;0-=vwK^z}dt_2tFy@~ItHRjZKBPB=$m$aa9FG3fNo9MJjU_!e9!?=<)YAPuFqGD$7~C@KWYjV=LB6~NxmsLZlXtBGYYK;stAj4GF;l4h7~h}O?{g28 znPt#n>eybj!(&%sH|ayoI=Gg%bmE!_1N8?;f;JQ$8g%I!2` zDxYz5aBW{Ym5o!jS@2Ayqd?{%@gu~r)M{``+;zOd8KbQWz_ThbKrPi0DVLnkMVi%k zZMq(zOyPk==4|<^(TFKZCG80i<^4mRC0zNGT8OYY`K~(sZTJXv#}r;JZ{UR&@HkEKW6x6zXZgWIbg6^)BoZ34{Ui*+GYH z=0V&|+^I@%-|T|f3CtFARFWK3V~!vtov=6BsDa$qUvKr6D3_^7*7F&OEy5I1l{;BS?EnpH2OqCD zD(X}=9pnQ7q+bV6b4E-hxSU9y92wX*BrK_d8@CQki!d}|suVYVlSd~uK(Sze>ixz| z)n>Z%d*(~UyB2!jNxtOwAGDi<_d%S!*<+Tsb-xn(@En{m$xx$;32FNhg*YZJO;R$# z%pku5)5*_Zrby;Scuvw^4X=@@z63Kf?z+0q^-aIL1b299ScyK<$_tGAYQQC~zvoy# zhvjab6ZKzVRQkbTD7aZ-7BD4gT8HE`3d}+#{X$ESQ2Ljkrqw^*IKfLntKmAjl4ck= zs!$br=BtuOu__dlPnqjv@{a#{K|`lQ4wg^g=4mz5tA%Q*M(gi8hh1rIuS6~_MMeRc z5n42yB1s1pq#H`>4w{=y9(^(@_R;t!7}MIaqjdUA^(T@S?ZyYS4+A6Cn#3sztdE1m;e9( delta 3116 zcma)8du$Zv5&vd=kM;U|KHtNSJKH%s2?0VbmYY!I5Wx6BTnadqOG%8355KT{e)R4f zd}$Rqq>54$TH7CJXwya}NefaDa=26rp|~$4r4mL}0ymaSf&<6;!GgniP#{~1 zDv8O7hu?ub{Bb6D<-vfH*`3L`qSx~A?O|}?c^@sKkDj*SDvx5rKaCRG$vXXp?`ex{71Bz@61cIc2c+JTv3$ zDCEh?TBi)v-E{]|Ofaj?kpL5uuB-D+mfY!_{0;8u~4*Xo+&c+`R|hrlLhHfV)i zB1WW?Lcxq7TGxv>UONT(o<8vmZIacZi%4zs-qh;WL~Cc|I5SH8Guc>YkPsW2=e@*Cg&g6w#z3@69Q=ZB;4}& z9mX3{Ie=j@%*@ng(iWt|43Mr?+KbWCVYm?y2~jE^nkgJTejyz)Et@^)B_7UEJO1T) zD9(wKumReN8zNWIky2c|6BfvpJu0^B1TEP=QRc+{^{{1WvF4%r=a>c z1RUNj19m~NAQ%+uB<%IA;K-UOh!d49VVu^Gj||N;g-^2Xe%j>{GDEjS;CKE3%d&$M zp4FSki233?Zg?OCnTR=oiE4l)si2azpN7aIS&IQuG4UwqHE;+vkyP_V#7hu zlm$V#>PCUQvdNJQecY^ZN?aJM1P#Y5U`vkw>r0u~pP^{@E0wxT@Mm=o~jFYN*OyB&G*ite^|8F=iGH(v1I`bz^A)ed?sy?>| z8Ucq~pjMk@%|-_4?I&9pSm}^e(tj;slf5=)iRsh;adp$%NBHd3e~2*RK>zFDvZqE3 z?)wouh|j$N{s*g##>P%Eq>FTHC_9cdONf;t5hU#< zI&L>P!8B^p^vqk!^}>CWyDQH07-~4@tDv=8ScA>y2DtZ%Cpp>FaHTt*s%E!s2h8Tl_v~$OP)?_V36R- zq5F>}Ml^00W*vuV0y5-y6r^44C-eGf)hF=6n=odKuPhecZyeo5qpK8;940h}_CX%R z^y`P=R)$g1zIlzTmtjm}Ms8{L;bI*c^|Lyx2SXqv@OmSZ;>M#;>DwUIlK6Cv(wSts zd^^{ve{>Y)j4W%OG(6Hx>VC0G<_>w#^&2=7s5abY95Dtt4{4FrI2D6Jz32oC*|VyM zkdh;>Tl4Lyg2(W$!IRZ41`UYBX@czw^b-PG;+coSR~6m zUfG~qteuqMI9e}3El#We8+7BvHQ>RHOHe^K0AGx}HK$|`Y39F#h~RfWg<_n&1dHWx zg$rVM<9p=W=@C`#j#S8@XI=QM%P^_GconVyw@f#+OTHr!0`-w=uwTWoo7C<%-vpO@ zdV?2-J>bNln=qqKe-0m64fy2pNa2AUBoKFz_1iILHBH>NPLxPjmEYuv5QS~Jn+Ny4 zM3%YAo$O!+(eap7S(jN)L13iEjC+i}%%(i+{i2BwbH;Ih*+#OsU&RcBbJVz+P5dzG z*%2xj%nm4Bi#Ah833YRkrKl4a!_eho8HCA0tOZ(r9df^Uzv61aR0o+RJ>KJ)En2uEH2Cx0U+3R3R? diff --git a/iOSClient/Supporting Files/lv.lproj/Localizable.strings b/iOSClient/Supporting Files/lv.lproj/Localizable.strings index 17f7b7ce0c64eea906cbef60ba6a191b17422d01..36b21857c512c51bfb33a303b0312324462775a5 100644 GIT binary patch delta 2313 zcmbVOZETZO6uzfpZRxgtyleMCTlRJxMZVpb8rXY@F^gBXb)$tY{t>bkc}+igN5 zfEWcLG47(r%&&zQ6LiKw4J8N>O%M`M7a+t$f?yPgiN^3l6Jo$~?z?W|k3UHBw)egF zp68tNoadg~{rl{ne`+6J)hGj}?Q(3nSH`~5WaIL7`DcS&MwV!5}(eD%1RK z4|sty%OBI=Y_Z(vwHC(+IgTGx!FTfJF%5UyARu=vnj`hS4*bSN`RnJvfpd>iPW`JW zLw%n^j<%GBGm5S_(Uw&N>Vjv|6fJ)*^!)8HSU;y^GhwAfAA68@!ajWdG<4V|C-Wyx!!EB} z9UAlv5ormkUR-oiKulm-bjz!*8fMmBU0Bqo7il|jESo|hNJ-EAL8-v>JWb(>h;y9c~w>?AX(@-D1D3pM!tF5<zBh+*^@ z@G}w0(Zn`#bo3x-wmy|R;hYHixZsS=vdPlJ;|#hkk^&=_zzymAic9c`0|(jq*J}{L z!@IzXm$!ix8?M8Tc(EBYX}nW`BiBhyFDC89847s-+tx!^CL5f1>lB^0OMdDA?kaiZ zfPK3`>#QkO0-b(UUbab5C|#-}FmExMnU(>Qz_Wa?74fv1B}*>LX0lAue#TrWo}&cj ziB{4pkND|OLW(Lq>mvOiZRDaPm9(c}5;U8c)uR~;fP*ZmyCcU!<6EYQgcU<_^mW`iQ0aOWAQw&^0G z>!KH1a!`q*7r-ss&pYwuVem4bD@F~VlXCANJF+F{a_Zzi1S&%X?5Q=%pBdvbonZd| z2Vd5`R)v?75RvU+tAB&CGtAt?V#47f^Y0e?q5-P$!>>Vu#WHx@Uq3A=ZcNMypDMl$ z;uIg(hV2q0oXJZFzkC7wl%=|nWHjrW%^R4EC3t8MDvRP`DY}ZGx-3OT=0P+#_&ZQ3 zCz|Y`wMt9uXNYiFn0&RFjg)5-^68)EZCBB%6_zt!3M_i+Y-*ekTgl;!Yw<2&XH?A7 zhrkrR*&{QgAzeH{czov-5az1{bgE61=6i*{nw%L#0v9E2Qm2C9GguIAp)edN# z(_+c5d#{EXsi+lA!a?7igfo+uradrUcFeX8w>zYsn zBhG$?8NKt}#+Ay?x&a^g>0baJDxwj)k3;YQ4vNn%YlyimGytx0w@Ry^50h7JGe-Vu wk)_@u{|%O6Ymw!?1$;gE^pD+)=F=zq zVicI(@R?B{BaxwqA(POu zuK=_Rq_G^R1?bMn_4_0@7hL8LpSj)pw%F2 za)3Bx^WsP zb>vU~FU5FvvRabJ_8l^eY+>A9K&R${-IObKQ-PMIKz*DEbVdf)K?*>T0-(b{K2iXA11bvfImp*3lLPa`r`L%xX>D&4W_rgs J-AIh72>`l+igN$} diff --git a/iOSClient/Supporting Files/mk.lproj/Localizable.strings b/iOSClient/Supporting Files/mk.lproj/Localizable.strings index 42ad09ce411b8da94a8d165532574befdd5d3287..f3c530bdc90c2aa7bb5c33a2b219c833a30e13dc 100644 GIT binary patch delta 2142 zcmbtVZERCz6n;ZM$ymy0W`w@`|-Z- zIp;a&IUo1kA^VT-+ed0zaeYe-wqEgKrPGc2Lv1+GVyAak zwWQHN5s&EO!{DEBmI^l%bzLSN+@c&-&oFt-M}sQWBKR9ZIcGKO zMf`K4ojz=Y`Iu^U7~Wgv>bZTVVMBF!4}&I|2eyK1$Nco$r_fQF-^{K04E9%GL*0OX zhuX?17068tx|Jc*su#f>5|jRQ#Z!I8=}~tTHq~j6#9tPM@yj|JUHb`axV28BRzC!4 zx3jfwl`>-QVtcac<5j2H!S{ANw#skNIWYx}x(pjGh)BeS2rKCPWw=JWr@;?tI<_Ce zwCr^T+7q!+#}LdX?G-MTz-v%3ODtsa)!M~rxqKDEI8{|aseRzY4<7O2N>61`-c93I zVK%i*f)>$Llxtk@%)|=kU2Lh3Yd+4@a3%=A;wLMDR+?d{PL;*W7kqSbFU)nN4ChUS zDBaosA>7sIzB)O-^3QZ7WkviQ-3uusw|hNIF%eDgOgUkp_ef(Gf_7dPQO2RZvW zsHxjfNkg|mqf`be=+bTYfo#*jC496Jn};=8HqAu16Lz#-(P-jnuA?Vg+_bY7+&CO^ zqeTl~TbT{-wtFLz)C@hUU z!Ma1i@)=a%o$G!aa@k8HdufEJFpjwbeq+Tt(y-=u#7YF_wunz6UKzsqLDSMo^f$y0D65M-4MEuK)YSM8nLrIV1$s z%t`(qK`+b&Uzq?2gAZb&Dmbr6)vL#pgP)t&Sd!B$Flk;Td2O~=A2;hQizO)4TDro( z=)<)^Uumb|R}9N;&Y_m?prT6H?PWJ4=ACRO?;PG)>Dw*1<{07TG|qZhN~T(d%RyI9 zu@;Yof{vPJc&T$zI(dIP?@N(NyR$ztSd?F1p#)8)g;&Y7(Y;2&?ocr+tH$xGBei82 zreC0%aPapKP3(sT35=Rw0xND_W24p-cxl@K{+X@5=q`~8?n8LfZ%Z1~`59<-iKUhH c9Rn@6lx6DG8n!3nN$02Z+W!VTWV0Oj2O%#ZX8-^I delta 383 zcmaE~j$=h1#|9ab&G$@-_%<(a-X}VlFN|w*R^+m(>33N?SIg}F_iWrg^G8i%$${11^6d2SL6c~IN5*aFi>=GcI&5%0X@CBo! zjvi3J1!ztYP-z}RI#?Q{tr*BE0E*@VjY$RaiWzhmrmy(SXd$Y=kb%8{i%k<`*vJ?l#h(OrryI~2IUviYPd&{T%?uRW{{1v#r^xnoPZ*bQO;32ws5W_CBH#3e zOh&G0?LeQ;de3-o`?v3m3#83cfu2c$x+W9o;|!p85`nlF=&3}omlS|rD}ss^0Br#U SK>qfR!c2xt(;tX2H30zgn{EC8 diff --git a/iOSClient/Supporting Files/mn.lproj/Localizable.strings b/iOSClient/Supporting Files/mn.lproj/Localizable.strings index c796df4e974cb1a84b9ee918325f205cde71734c..02abe1f5075abc587038a1fc0587605eeac5d264 100644 GIT binary patch delta 2212 zcmb_dYiyHc6n;-PyVA8?*RFJ3H`Z<3Bm*))E|c4ciI;fcVj>b8uF0yH2 zLW~5+Av?q$!XhyS5JzU{0>s#45|_-eEyfsr42*)t;53V(iHM%_e!CQY`NK5d*SGI` z&U4Or&hwsLdE0XLjHP>UDO#5l;^~x&4rPM_!%NrW!6g>FSnk5=h?$=00xOQ(apCiK z9CYd}u=R{C{ml-g*#3J3?%ZOtJ*^^YmugUTDxrD?s#-m;uBYmydHe9d6UB}2xi*m1SgIB1$lHj0(sbR#yVwX!ynsi_>*-eIs+Plq`Dvz968stv;`GW|D4Vj(GA!eCWIcFjFgY2QE5vg?*OVO{FXgD#RKc_j zeR2X8=x}xeB*;OV)Lwd{7koNcMnuK1q`71YCEGCMZPluCN_INI0lMaWL*Pgb<-$cX z{@Lrq&hh2c@g~@4uAK`%IK*7+?sZUiGt9z`-&xSP#!;4-s4u9Rx!PU)$WC0f>h~hW zA4=T?fAZaYSc%li%ux7I2;i^zR-L$MZXo%^S=dmJUC(YY?hV6WD(0a}pEJ`dufpoA ziIZg8RcOz{Zvw5JHnm%YG_gBWkk`A|w*i5QrB2+r%Dh5I4@{r2b(DKvsvcKXe(zw9 zTKOtR?9t!iF}{iA*GnC)+()aU&T#R_PVZ;nAY`^jVDCzWESW{Cx*gG`k-PY4bhq zw-|i2)kT(TFjHIEC5Jt#h2II5)zAGHDA*_8odRh^mQe;@TvD^(zCbu z@W56Ho`Ot#EoH-|NG2`V#F^ZyaL}uDJOY~h4$QlhkJV*nTu|kDO!Sb@$px8#$$D#f zXIeMHjWEfz8||R32zXzvRNI-bIchyCS61?D)It+TAy=FdTz6P&T*%(7_d~p!m}r0z zUaW+EX+;7nPP(6*#;juhFrT#Wbq%wX$Wc-jAxjiNO7NM8O=gHB-@Rew8Hjah^clq)dNYQl z5Goxt2Bc_B%3|H(jt`4@*6r1y7Nrf<2UwwuLokmV{rtCa^J6nzdL3L;*~`~_paPt^ z5<=>pqLFfMgFAWUHgx)DM6~*uI9hxeidIM(QlBIwY))Re8s2Ttg+%qcK_f~bx(xj1 mhL6@}nC2%3Or|kF+fAGPIYTa%#-!&mP2T@08hw;$I`S`b?@7G? delta 399 zcmeyiiepa~$A)vpnkgb10c=^HW``PkDKN*Qt(a;AUeXH=iu zAR#w>!U;y6>3ts=*+df=iWrg^G8i%$${11^6d2SL6c~IbZ=51(m&1_HkjIb?6e|JB z<^pB)fNU3_QAI#LOa`R07${x<6wL>klnUe(Gw3i(KlYK)f>(hd1)+X=;X6iI{zS0y za-c$>ix~7a7hc+H%bEt%TQuEJfKh$=znzS1)3rhvHKso>X5^agBFuOVAuT;UW+Nld zb{7%GZEVwL{9qK>{!E(DV)FKq^^6u$>`4rjK$o~pRx}o#e&!@&05ecvyYeZHT$e7NkJbYGgN{WHh{+zH=fCf-KIFCx)@kYLt_eObck|nSTT=k3&F*K z<8S(kUM{L#)op(1st3>^FOx2RUU7O2L< zmU4|SZ?=&-d=#GYbEy&xB`Qv^&g!AN-@tPPnM+K6A`HMFZ3s_?kag03YN+-A7)q%&xI6If~MTVT+uNp6j` zyP=du{ZK+{&T=QT1xiH=N0*l16<67mjx(R3<1gKAy80d4ul@mCRC*3v#gg6-XDOyS znSO+o;8QQneg|9yNzQ+(>cjLYw?uH!jqOlY)T`N;Cha@}t$TdT-AZ5gfQ8n53`Rjz zv+cvvrve_SqBv(w_7>6SBz<&=`{(3ka6uyV{be|4qpktwa(;rld)ov@8jUfl;RzV0 z_v@hqogb7@<#i~-LpN4B6gwCD1wb8OazS(@c!DG!#Gbh2d+>r2lqSFbRKhg2i?o}Boh&mhmeIH{GS zF>{6y4x8kFa%VyiElGm@jp<{ari>G{ibM49eZT7EZ=ChpHE3x+u@^r{mPzvn6oKE* zXMx*;+x@}+&B_TMri4r2@`YGwv6ZE)Kk z$O8?g!E2W_(ZZ-AyjAx7#WiVO2DX5n492AI*^LacUYLTB);EJsYB!B5*-h7m!Hb73 zR?)E|V57ZhaLmm*ZDZHHT#dLkm$Qs>I72n5g0yNGcx$rJa;lQ&1V@$e;oAILL~NMv zq9tQcPEWioJ9f1dcN94b7chD$w|oaEun>Iie%%=+jjc?iT`jjTecv)RUXk};ue z?yo*mi*F)4T6QexT?v$ zQ}lDwA+k>PEPFZ0&1I>-r1Q4j077^;o^g;Y0dNXyGgVVGtjb#(s6IMvYE(uu{? zc3?Skn8v0(c=JH@j3lQ;Le*mNtqQug3uZB499n0i&$se!xH0U)b*G)vCCf1!yyd1} zC%~E7dmVOF%!umw;WSXe7Z6w`aRk3Kh~y?a>r%V0MjF%aFv}!22H(rz)neVGm+sz& wIjKGO;Sa!uNr(P9SBBH{+MS6@NO1xN)Vl_5W=;&CfuWPx>p(_AA1@@DMJoJ&h)@QM$5?o zIy}=8jxcgfk9o()p`6H2#E{I8!H~&N#*oUOz@Vm}z~IY}$WRGnmjLN(hScf0j~ONP z^?(8{Ky!+KO7j@f!O|dY#Xwd8P&6NCOe&C9%%B4l$)4W0iBWv|hIfo|yb25{2z}EX zUo*<`CxW$=12q7h#h^F&qK^3H3m39%S<`^#6iqJtBe|VRkWq#a!Ji%@$;h`oK!|Z2 z+w_dDj3V1#Niyrc|_dN?XU2)<4-|Unw z2HWZS<+saeax1uKa1%Ju^oFcYt6mjVohq)fr~QqcUi$Pj8}8lo(E!caDXWpbC-BSV zE=oIKHl4MDg*K<5hJLj}1y%h89;)gF4<0ybp_6u)jgJl5Y4u{hbDn?-N1sY5Q?)aC z4}X0ajRiFZ4q9upMOn@wqvAO7h8vSHd+wf-qDdD7=&qfK7A$l*Ox3|ew=iHvC1yfx zUbRSD6*3e>4|4LF$S1ZyrrFFS6L_`{1FV26B4HrjRrY{TtNI9Xz?Qmrb>JTq+B zII#*}J!`|)odIgLihlEo?(l1+?BOq_KWM-=~P);$Og*Z9CQL_Zw|C*7IP zRT&LbXle!X&Ed!icrsw^VDJ=|+*DDlGq_Kep`~={aQNrTu+xLh<`(Z()x}9llva+@ z#ffKBCq*k@u2jwN=Wopiw``wOow&xV#I)%_XPpH{%MAK307ZDfbW)RxyVLxl#hYax zt~dXZj2=~6`Rk^gE(qeqXN_u;OYp#edFBk*+~k!GOfU{!$Y5)I7@HexxNc);C~Cu& zQsZC}O6b~d@Dxf%Ki}Y{AHQKOcy@q^?>z3pGqoXV*an8!*y(5*=FshKS$zvGh+^aO z?lOtO{Z(CbWIqIbSzbo@lkh2)fGJlhPOXFRh$F2NiSb2}MV7#^tA1=bZztyj6w%FN z5Gw7}iS}XmT-Yluo3dn(gkEQ9>G!OdwKuqBflW*sf>2BlLh*!_k3-#({&S{=%U~$Pxz7{PA0g+-z3$qN?!!sd!T^9aN z@M)Ie7e0`_cbUnkl>_bL(Y%k^0*`ZtILX;nMPO zF{?mjsPa#!rgfKiV0V-oZhgk=d0?5PydH5J^9M^!HQzK@`n{il4?`n!sQwsxtS)ys z+Buh2&by24*#~F+;f5+|yu@}4=%2n|Qc^0J(1f%>+8-|b7be}g&hol)ffbQi;R-&d zoszjMR4s!@pXGmmj?{slCg0>~sKwvchwJi{X=DD(q$(RtvK5&y77?K$>7%~~k7*ZJ&7}OLL7Dq+N>3NyW8~SsK#-A- z5hA)BD0-P~dIm3($o2v$Mv+O|Z>?dplHyMSJK7ECrW}wH*^{uIZ0nF{(|T7tA;PLo1`mbeY$T_qJDjW^9tSOa=NX1?rqkpw}~i4od{$VxaF5 l!G2Q!ISDFS0JH%V68Q`%(?9YvIc}f9&-9LQx{DA~69A7jZFm3x diff --git a/iOSClient/Supporting Files/my.lproj/Localizable.strings b/iOSClient/Supporting Files/my.lproj/Localizable.strings index dfea08985001904f63d1d974a5589d740aec92f0..845a2e161d33d30741dbd2266471e33c0689d7f6 100644 GIT binary patch delta 2239 zcmbVNZ){Ul6u+k%UFq8HtzA31b?mM~Tm`Fc3ayOolNL44CK$8ZhXT4@M$-&V6*P-;8PA>wWK@ zd(Q9t&hMP}^#R-PaogC+6^E7wDqseCKAP3e5t?cCdMP|IQrPHf^9tVfIdKPTs zZ$G=eUv=-LV^3d7twr}+ZeX}IW?b%9-tT!eY5*9krhuBk#-qa8;Ml&WkOSaHx{ zSErod(uR0^|2(zzB-y2xyHO4mZ|y5Lx5`Y&X*Jyp-lC(~33K0T?ste632#CpmLi7X z34f7s`(W+Jt zfT12KhZ=0!8Ne~46xV-ZgA7(w`mrYGQpY|38}6K*qncias>Td)*iBalkuM3O6#OR5 z?&%)lpK7}YwYi%<7(S!|C&9pPt3xX51czn5%q333rke5?*(Rwb{P-E?(fo8OCLWma`63igI#6V6(j z&;dy+9URYOy=5#@4jTTkS}O{h>lP?u2!a*tK4Z$1f<@IYDX3HJgCOz3qi(!x_}DC< zeoYZl-^Z1C zd=>0?Y;mc2{4S|-!Etaqdvt1CEi%)G*Zu39?yb1?ks+gAO)s1Cic@6pFH?tc3e<%n zojYfPOGj24F`f%i^q3%u5*6j&!K%c7I z0*&fK3Apg3+POHC@ZscjD~_ypFXgf_f+SJY!5yt5N9awV{9IwiLpxmIBg*Z-L%z5 zvgsx>9z}eJ)yQGkRfhIhND;a-^gdemYV8)nGW|)>PClZ#2N;G)a{*-JelkI9bof=u zVzHr-2w>An+d4+a&BjjBwEl0rF~kHx?IGU$q?!vcaCZn zh=6^TDeBQ@JSB0O!QcC9OQo1E@$<>mYT!MZk8@XneR07-JGn^_s3hf`)N2OXNl;lp z84*+!5vXo356B`@lSppwxSm!AOQ5b0CG-s#CB&v`8K%jZjni}xLd`fNWgWB1J56tvW zp4se@GkHi&r=hweDni)un;+l3WvDkkrO{7fuG^?(#_o6P=3*}TC$n)82s3eM6{Fp54K_^-^68m2(3UZ+Fy=Qzdt1mKO_>=F4{F4c6c?uV zT|#akcL{RHHHez5X^5<*ynOTZ;Ug?yKEX2Ol&B4G>5=-%4DrOFLLw;E2)hX4=q-=> z>HvLx&RwICF!U;YgBP7P@U^opDRnr`Ge*CBa7i7I)S2a;zYd3LXZPqPBC^z#kD%^h z4$pZpcf~`AOW3JBCiK0NEX;PG!hYCbS!Tf{t#+0D8x}3(o;3aJAfQsLr7I{l(>tDx UjDY(#A9B_I3Q6;Kqs4OYAAA!r0{{R3 delta 398 zcmY+Aze_?<6vxjo(1+;DXU_zMx-Cwjfl&lD7}3^`EpUh|e`HBMvZg(kA7Ix=t!dC1FxHZPloO1%s^Dg23wF9}81g+0hY8PXsiBm}I=s`Qcl z9len-qh&T1S#Sth?v94y5?2P(0-MuN;x*0WfYwd)Q?rd8+WJD(Egx z0#4Y*6G51?YxJArFUBIRxly5y3v@|!(n*FJb!%Tk1^-`I4KLo?=?QJRH5b6fr4_iv zSirFZ6@GU`PDRJ4+`^3!2G(;6tGadC!7C?;E{3ELQOV;%of2Jqv+lmJ9$ZLswAAlZ pxnaP;l!A%D=OCcqpheyX)Zl*b9R*0yqh^a+ol#p4sqVF%{{VgLZtMU6 diff --git a/iOSClient/Supporting Files/nb-NO.lproj/Localizable.strings b/iOSClient/Supporting Files/nb-NO.lproj/Localizable.strings index ed1915b41541c7062050fe055ddf07b22562f2bb..84ca709f703513675c87ee8efa62933a8d5f98cb 100644 GIT binary patch delta 3095 zcmcguTWnNS6g{WlcpX}botf4$1#T$>L$oOE52(=c5Qzz3Y($jMOlR6sJDn+a`mm@O zN&GNkjOmG-7~=zDjPZv`VPgTsv>87{JM1(JgY52R97Iwrn!W>|s}oVl!imTBRNY2Ou$me^;cjNzTu#3{LKjg9^K z@=0LS4NXJFg(09pS9)MNjrgbAV!ACN9bFlfVRbgVOr3W^jn~(d!pStg(4-&wMAn-# zbD&+Vf3oog=V(6z4+hH5scR>Ut(NQCN}8-OZtHDXwr6O0``(p{X48-bQe%Oq+B+kS z$yp8*^JEV^8sjo$QtTzTjjxj?^s zM22L}Y^(b}p06@5EFK?m?%rY0Oo*I`k-tPY34F;;*Sz-(bv)6`kG}Pqj2GT%kb7QH zU+;F*ug$Acv-OX!#fp+i3cV@G**kqS_u13WoF_Q*yJBubV}_ z@c0+4*j>jK<(7YWq=mavv%gC5;17}$?9`-W8 zvIl!ir(aD)7zFa-$vQnXf+BcvT)OJ~NoTt)61uPt6^deqqHehE8n<$Aei*AH-Z5f6 zp_s7FV9oS(nH?4x>l@5~3AOC88ujkY4fzhh#e*}|-$)Tn44Aq%)n`nljAMRRAYUkz zL7ZvCXjwFBu-=P3zwVP_xD|$NNzzkB<~N5d4hN!?DMhLeyf5pk2hBli2YFO-T|M(! ziijNc8EbnmRRpl01K>lm^pqIPH2E+6khDCZ4n{ynXF6!o;}rAb73#m0uZ7jru}XdP zlDu6%cSP3czb?t2mGfbZYJRI$k6f0uYTMP28h^c3KmCXd>kZ$_8kMPX^wcRV<}SER z-E~VS-wyv=6OC;Ec8bssws4?W1d&NE=UdQieV+Nq7dx)exmN6k{^g<#Rm~N@GrB?E z08RAM#o6Jj=|il!V(QsCn87SQbk{x);)ybSU?CeyV4@w*oIRxx0wPfZtC`85-W+Jw zEw+NR_+xZ-K(Y7(u;Qh9d0NPBnz1QqF83^R*eZsKMzcvJ{WW!;%WYra?bVuoUpQ&r#Xz4lx zRb1O4%T$-HK!AZ<%7NN+C1spu4(`;``(@o6kV+rO%X;49XoVucDlBE2ub1mbyB!>x%8L zWt+4OZ3Q$_stekaV28`wTL$!?bKPaBu|#1wWR<;s@kn_?Vwk3$bh{%X5wa`|2w6sB zIFc02>dfEqLob01=qS570Jg0Y&O743Zf5(NUqJy6s4EL3Z4J@|+beB%x4n}i9@#Ej zxGXQnlkBY=`z&Fi?(4GMnoZtnVSzIg+nHx}RI;eo$oZ_LRf1ul(8t3S?S&`872ipH dGF;J`4|H|n?(GVD58Vd+|ARN+;Ks@d^$$<#v<(0N delta 529 zcmcb&ljGVJ@kA_fJ9L?D^XkjYTSpr)X};0@&G0YyuIv;t5Y zh?frLXELNP=mA-wK#>#%h+Yt#2ZX6W@dBVZ`3%KCb}^XDW6+tt_9vq;w*t@wP%YCH ztr;b!UwFXC!k!N_wwU4Rbi+PI>CJvuZ`e%_n90b-TLLw?fZ-{F!sJGI`RNM6j3U#e z#2G(LUr@@(A+7_ostjx`hSJFs1o^gyNHCsZn_eZ(B(j}HfiZ6C_5)iPk4fnU1KpPi z4hWEs%YcTYf%&;WRxZ%E6d>&e76H0{`o;^4u8R1ylmV3{ZWq4DcvNJE-rT+?@b zVN{#EA(3x-f;^+p^iN+H-)-0W%lJ%sx}7u=S70&F>QZ2!!rTnAG8HHSaR4Ze@_|kP p1tQ2%sbI%Kf*GQ!0343lKpmwFxsx5gN^b9wWa43(9wx)o0RYtBiC_Q# diff --git a/iOSClient/Supporting Files/ne.lproj/Localizable.strings b/iOSClient/Supporting Files/ne.lproj/Localizable.strings index 616e31ebcfd47568c7338fb72a42b517742e88db..ba9ada0efb1273fb9ae7ebc7b10584337a77ffa8 100644 GIT binary patch delta 2175 zcmb_dZD^BM6uzg;(lj6SH6M*>TKZb+*U;FisI$sCR)zgowIEJ5p~fa{qfJs1ZHbK9 zxjJw#haT3$MyIPNb7N>n8QJETPB(wZP%FYFgW^%e#T-x|dSVdF>!~LHAI9MD>WvYxysy!-& zP4_E@OP7tg=y*4{sb(`6WFCZa$4e@%!m3S0ReJbIeVdOOhrv1Ax%t%$4fOG9-bi9C zK6IDUEfss*TL?oIW=iJl8XzVU{anpSR_Xfr-HPk(u_fg1J zXlDO}h3n1@$lgQZAwBp^*-QWd#*U=q&a&X{Isau97&iT*-42jeuw0j9V~^q zBZb{6Ottf2F@6-X=dl;eGqcxEK|>k7wXOu0+uU^N0(hz6BCIKDWdlhS&%SvP-tc0* z+2!k2?JB0-HM7HZc9&9ZG~$Iy2`2bT{@9#6W`%@m!{^M|7%{!LEonc}WToDFV8uSu zz@z3Y?0DJkOS49bF-KXtnGG{qwG*3SK{C#OefHF22~!`lu*oZpi3ab0uPBxCMu)@T z*Bl~aCv)YqgJ7cBZb#!A3%!1T1H4|wOk8>i%BbOQ{uH`7S~%K-YUTK1IAT;xRVbmF zOPuwtAHhA-WS;NQu5*PAW78@RR>i%v((8@T=M|BAW!&AC!<0(a!j0sw5UJm}5lDAzNlQ`$PXaD&LKD5&Z8E%_` zJ5WkPx4Gv|?Bhsp--gk`D%Fg?49umCe1>aw*wAv`z|Qw=_-T(py@lLZm#-T5c)gQO zWw?iW<4!d1S+S$uJx7Y3)}gg<#kjk5L=oP}lY?p(foI`(+E5|oi z)iW_=#SaaB0-k+oEhMa+RE)8w^mwjwUPVe0Boq)7o%|&-F|}ROGH+D9z~3D#v4>?k zSW2doXb5G+huBT?nedIFHSkM>6FRe^sH?{RlKJGg`8a|NCI0-qDiw0*qgLo@m zjlDHCOw=1o{zJ0HZ6z339AE1XIHqI>&t3Jobo+2iFkN_*j@+!ETMv0=Tv-8j3|+H% z^gg4J0gz-|F%`vQJwYj>1&>}^MWZKpZuT4kyMNNub~f0_v`mT;8}&dHeYXPqVlj|E zU~G~u>GYTk6dddcOt*`d&l>c7J5=NEey?j0D@bYOKSOm(AVA080XHndGhYR9+cNL- zLIRmq!qSMAi|H20-4rG}3hT)=F+X^6Xvy)r1P5>Wip9F5e=!ZFz+V_uA*wB4SC*B` z@keKw(ahC~b=rgtf8H1k3DYoB2D7S5s-5)>v~B zYNEMYSw~_?bMZ~Jfo-%*u`pj_G?_SI-rva*!do6K{Ep@_q>>?ql;Wca8s-WlS(j?+ z4%cjx5SY#gA6FKq`C1iMAzbn(NHqsI_tKla2@4scOpLpGd9tnlf^%{7B-o23&qCS+ zexC8znrdTD+Ipn)LuCjBv^A2||o}*ru!f zVieiVA;qXLX?xuoMk^`yB(P)MCNH!So_^yPV*oQyV0vRGqwsd8BGVI^82P6A)iSb8KlF<6#&(I%j4jfZsX$MqKpmF}^mzu*nTbGL4D?(g l*lP+vFBU;X3xFN~1w=kW%H%*R@$CZqOcG4fp9wNG0RZbbZlwSK diff --git a/iOSClient/Supporting Files/nl.lproj/Localizable.strings b/iOSClient/Supporting Files/nl.lproj/Localizable.strings index fe44631a08361a82a8174a69f124b9db4e45f269..58deed43c048223ba15317054fee0d66d403e116 100644 GIT binary patch delta 2729 zcmcImZD^BM6uu|x-lq9znc+sqM(HngjKQ9B-+b90^VbN;d*6G` zJ?A;k`M9}y(K)@~ymW7)oDLcC^oB`|3|2dfizULL4R!b%bl67U3n>V6A#S5M*`1k&Bw zE>C-${;d#ph`si%t*-WX9iitB0raSvYNcj%qJ^5|wv#L6lG~8S->qs}W*H_%040Sl zaI2{n>XftY|5CT0nugEuqPpuwNxi0IUA_4ih3b~kqRp#gpV4ZDvOGvRzcpy%UGS7F zHkT~gOyGAM{2zw%_7=ovglRD{hy+^LwL~xE1)7%g7tZ&!(p^I?Bn;U*=TX<|sZ~xJ zO>*c14`o#ZY{_$1Yt_vgY;FtzHUa&U?utql-lq(xe^~k01FGM}^~`3=eaeh*uw$S&i8Uv25-aW!)HtQUX>eiRk;6;Pr!?xgE+X*c@17T7N@9&_H z@2QAz(#m`Pq(5A8HH!7oBkECAXDOhzKc=PUIz&{S|8$j_drXaL^N-}AoZ9jhbyk_8 z%aoVGUGk;5Cb<+a*J__hbVX1XKYEzjShoKg- zWJD@cq7`6}t5{!KSZ7t+9csqK)Mc9D05(QNM!x@3XH872CBLC#njM>$}g z)?7;bJOTuK^%gUl*68>>r;}$#`!*1ch+es8KCBwVSy}JH0)vCqs zG5gVW{e!yrxnPQuhNH8u58mbY*${J~XN0_VEvPcTBO=C-&`6Vl!!5?bNGm%rtD`M< zEo+vBEfAVw7mUX%EOf&%YYoGC9F?L66WWQvj24ry9yo1M>|U}n#d+YG)8h(M*>UNf zlrua1a^L5!nk;h24*S%HgA`Uz&Jk{LjGzZ=t7PpJqk2O3rzJ0333ub-(S1A#$K0#z zh2f0hQv`&YUJV$IVE%R&9?gEjyF delta 409 zcmX|7ze_?<6h7yv^$;-$O|6Uz!rDt2R^neE-Qi+GG(qP0vuE-CT5P}jS>0pg^G&pL+RMjQ_LLAi zCn3Sf>JR45W%TZV)uwHx5~Lx`KUx@I&7hS_H54}$NI@L-KmrBw5Q8M_5Kj=7B?&^r ziiA_t)irS_57jD^NUuA6R1ZUjxCAQC8E9(v&^V`1Sn)qhN4ua73BHr1~{>w^`H{3YRZ)ecO+d8`N;1}DniKYc|Rrrw`9U66iM!4TB*tMpN z;g;s>;??BXHdINoK@-z7rNF;mF>0Wq7I{N%*Cu}P)6B&_%B;I+Wl~k-Yc90#^*;8r w_NZ_^70}mGL4Rju@Bv9e3Y`LE>5fsVL<{GK)hzKm=`z}lS6CAGsVp@903lUx@c;k- diff --git a/iOSClient/Supporting Files/nn_NO.lproj/Localizable.strings b/iOSClient/Supporting Files/nn_NO.lproj/Localizable.strings index eb32f3cbf12e9d081d7514d0ae218fab605dd302..5c0c1cce4ff9fa515547315948191d4daf027dbd 100644 GIT binary patch delta 2238 zcmbVNduWqa6u+m|^fOJWs{RI;Du)L}7n8kTUUsh{znG`$Kf+d|%R(!C;JXZ*T5B zkKg&7$M@BMHvYACv|%BBR8xU_&Ny*i+=>kgR^rndjc(S06VF5~1shejTBnkD>ux1# z4Ytwu7G5iYARSl>0jytMQTUpQsTS3t+Ei+E;fgK~EFay!_S22jupMlpgIA)w$pewM=^h2DIWdFUYTODqP#+?hq1TtlAEMjZ|v2I|0wVjd}-Ya3wG0^ zLrhe~bdJVSY5>MVT{axPpPycP80ME3wzFzCXMs&&jpm`3&ViebpM#eRGB0#&8@SSz z^ALAqr@r2^L3OH#fuNZ&bTWdZ>Y(eh!H@k#n(wWNXFd1K3USqeEqVaDG4LWw9<*Rn ztp!JP2fgBiQhMVESUoA$NOBl$EZxk8H}LAhp@@&}9|db*^0P#4JhQOQCj%3{dBp>q z%>nR}{S&T|VK;9(xBh?jmH1cx`pA}zc`xSO9vrBk(0 zzy~hc*9#@Z2_s5VJ**-)6etr$sObVH{>gWs?fnMa1u?eJrh2gMl)qBaEzVe)twoG; zDq~rEu@8DM0A5I@V^`sT74LD`*lxgV?D^hJy_=YZcW%H{IRkpuOVx9A%1Ks}QA1);h!UL?TaFraoS}^GBYd06FmHMnDWO6t zK+q)km&im^t6?$cVFmw=vc$St7K*Z13-c!`QnKPy45#=_>MhK>gtCs$0zHbS%FFyF zfPcaZn_*y$%}4clc^Yo3E5Uw`h4eX~trBR`kkVI4KL0n`1W}xk#WJ)(+*LSzxkoaq=udwgc(qU#Z7)%JqjhuWZp?bg$>z=(NtLtp;u;KcH9V1 zGnLto&(H_1Y2rgDeu4Zc@J?w{)wuc3vLfkEvw`EP4bLtvlVT67G7L%{H!xJ`Uipfd z<5q4Xxb(MK7@X_w6N)<+fP}Qk0FiOZQthl2Wyn$sDFb+}H3V)u>quZJt|nbIvW?Ir z3(p&HbuQ`_zVBiQDS8<-j5JGGHN%n;%a*XotFS2f&y5KQgL?s+znw8H#axT4Y8-mx zqmcp5jWl@ThkU#|$AYWxIBChp{LQ#<%05-fKaTL&rOEe8LP=NMghAi5n9(`R2pu~K z{zc+X0uj%~Fk;Fi#8%!r49MA>OBjNfi#`3|8Pw$A$YUEdJ%c%Or4vm<%__(u%XkG< Z1K-Ic)OnX~&HAkSUp`fsJb8IL{{}keM&AGc delta 402 zcmcb&lw(ON#|A&+%>pKGcsKhvrHD>W2;thiC%mh2x(){uAA1@@DMJoJ&g8<2n$rW^ z8D*w7Ok(8H&Sc1ANM}%BNMtBtNM^`j$YdyENCooM6ciYIfufZ_b_tNqW=NfW@iC*M zjvi3J1!zG0B!S4QNQw^n-ki>eCyxGqO!*3u4rmd_kOR`V~RO zYY18C$v%QS+ph>Qu3_6AA;su6Y5SKoj3!d-Nnl62O=9Ks<6GlH53fxuEi)kHxSTttE!ATh>>7&C|w1fz*0Mm^`g9UY1BpIP4by}kF` z=RD^*AKSIVaqfuY*yVa$TwjI7Hw-+n+`w-hXqeQdqAH5bEq+Rz0z35`04FXR@?_7| zSGr4Ps1_AhopkaYm`(e`;4Xbab*eViu3{>az0x@9ho#x0&y4orzIiTesBvb;E}mL~ z7cN!N&jz?lHn446b>XH^Ev+;lVCzyze7z<_-zUIHBU8XdHw|#zwHtQU(wpTFKwrIw zelTEaX&!-jq;bp48hu*AD9Ti6n*SL1?{x$>yy!aZ(`MzCSwyX4775{ujYD3%`KeRi zDJwun7%zpROQ-95nq=eRdy(mvFTmn9#e(hRJ3T5!R}HA3{W-AbzAgs`NbI@4Jn+7q zc5eg&w;B(Vd5z;-=H_3iawop^p_^JW;HK09sN8klRg+;cJNSvHE{>{=AK6I@I=}1W zFE>yD8Ke`3!Jxyl!H4FB0NvtWMYGJD;|MfWmab*nBqxUvu;1Y)`!M)u+b}FH$)Dtg zhGB~jubVyo4J=7q`)pyaB2QYi)9qJ53*e|2KQ#-dY|j$nRkNCo`(PTj-f>XF7!>2& zYGV?s$4M^V;U!lVoc;`7OEdWxUuj`*5qdoWlWNF7&Z1PHb#(|6elC}t|EAHFjyO5o3sufp)-=t-`q^m*`AV4z8qtTdOr>?l0=J3He{f)oVVgy9VFkXIT%949uc8-UB=Cf7*e@ zP%$1p?V^`g!qhz_7Ndh4X3F!<&b?Evn1QX}oOkRDz8nM=i9AxKQEe30ozlxJyZMSpEAwo8qUdp~lcp`O zzso;aphcB=G|&S~7*t6PP?(s1Ry z!8w|8o=1dvh@Gh3uGewZr+H}}bo(RlZjuYu1RHPrwG1rbWw?4w^&~??3YS!^rfsS< z3>e|`34DU>g-py+OuJ9>$#}(%=Spr&oB|wHF<&Zu7Pstzmegs5&i%+#HW zE3$4HdxiV?(4c|NPu!Dq;COVmpVsmJnw;x8>ax zn6%c!;j@ORw`;rc60lS=c|Vc6Vk=ruL^o|k4>HL-2=&{Ff-n4!UR9KA_M)Bt03O;w AJ^%m! delta 414 zcmYL_KS%;$7{=e%$c}@qlhpnyH#vy}3DFQ#NJJ3S=FoC^l809(+7-3*XJ`we8@`BW za1V?NzS$)tLxP|wBBH@bp@xc$cowK+sIp4E)0b4#2 zom%pC*IpTwyDc!lu*<#)`^1viLj^_l{DDGNg*>Dn13GL$8We~q3M_&OJ2Y?7n{}y5 zn*kSj|YW4=vuWWCjd2^!lAcxL!zzo&t0A?P{$144pxH~;_u diff --git a/iOSClient/Supporting Files/pl.lproj/Localizable.strings b/iOSClient/Supporting Files/pl.lproj/Localizable.strings index 0c996679d14cc5f2d76f03126467ade7d617cd25..fc58b2bfadef622461dfb180d1bbdfc83d8670b7 100644 GIT binary patch delta 2664 zcmcImZETZO6uzgKwRENQyX!i(w;K$CP+2lCB19mWx@d5TLIQzc*V56jt)<;YAaOu2 z$`52)j^>ys7z{s*i2>c9;fDfI2tNc77N$l-NJu~p0Wf-gD1$ zp67hr_vpK}ug}?zo^8c>t#vqd)`QQt`ev?GY1OX`mEiAQHAnUF@69Tv2Gk~HP|KU( zpKkDCnwMj2lpyd~{Iu#kSf_2*N3+Vndp~*U!U)ukUTJkYp%Fj$tp$f4cT~5l9zNB@ zu5zPSSDp{hGoONEbZo8fMO^A~jL!ekxg0x2t7fJcA;s`rjACN-tBmSq|BOj*2cU_3 zgD?x%ud&UEvE}{Nag}9^7_W2C{JB|eB6lr>=)C~c;xG0_>~Px}raGD66W#pglNtxI z)Y1-3|7b5by^PdTu-7_%d&VONnx+DXYc++MgqFl3XLM9G=vp>UxCf_VlU5lz5P;cu z$uUbjho|z~!}4&J@Q-9DzpDnS;NdowAH-J$$vA-H9wy@TMw z!`B0JY5)xS?ig69cLc1E6B~TuBPZ?J!yAUn;%mwkB#X0XQWS=gm8`CqtcuDD-SKe# zoB@a5Ao~e$(f9RS6s{vSA?&P-soBVPo`A{P>ZA_5M;wxS0{mnh2OqsO4oe>_Y~?>0 zhgW@=jSTp=v8E}VwvEcvH>Nu`$gDgsDXCt0hMisQ0l!AIk4BS8sM$zP5?i;a_J%R>9` zx7d_o;5yqK4NaFd(`Q1Z@cQ`>D`_R;E2eSKVX?Q%6p>Q~drxcs+$j7KB#A`C`YTFC zBnHjeHEJH0#!S7>n?c<|yo0s3(g z0<`ZUII1hFeCwW2m7$sqTvaz0kKYL3sf?8d*MSu~Ryuadtdn@t%&V+Tf>$b&S{^XP zWa=TqR!KhD&B{xvm#Fp3akg5{Js`j2-J$DLFob(i({6LVs=l2vt9T>D2Dw7|%z#9C zkxVL00?hC~u}P^;?XnE9g8z53MFnYnBBtRa#r^!wFix)yRLJ%7Z;|>DIauT(xkwk> zODjrH_s5(txzUQJAcE_+*3)p6#g401Iv4#nCC$o{0jiah>%=>)BpjVQ06{!B-Sz<2 zm&(!?4wy-;m%$SGT2c zi`N+mX19^0*vzNdgZTGCBQ3ZLp#~G7w8zXvK$$Pv3346%3l5%K9j3#dLk+FF0}hNn z$t5;weW=%@s)w=^hYV-)?ld7J|Als;kpb|-eEj`n7+s5e8!E0RPMsV~=^iZ}FWuBS zP8mCL&q#txmou}K0?Khci(;l1$-)9U@(P3?iuZmE;n+17_ASx9(blGmQz~y8!%1<- zBZ4{XWmOUj&j)icv33;V(%79L)uF!TxfC&VZ)&YcaD`4 zsHkdGk14z2hnZ_$vcJYpA2K(1@K)jQnDs%vq8TWQk#R3BJZAI8bQ7l8m#IHv;SdF@ zB!l@iKf~Sdh92FAqGNG5n!3pVme8bAC0W8#!jiFEZq80#?b3F|dJ@h;Pxm#Rz8Ru# hT$Z`{Q*O&0zza7W`tP)oPg>!%1pnV4=#bYk^e28Id~E;# delta 470 zcmYjNJuE|E6utNPX(RPiQDhA$g4HApN zB9U}>i=~SgSr7{ngLIGxF(J+q5zF^|_ndR@`R=(rcb*(MPtH1}S7&#AuS?hS@BL>! zGg7F?pO1J~#WldXW16xQqnH`;bjx@RyUmMAPivG?G)c2GM@yjN6jq7`DM2f2kfLRZ z(ma*1gB4{cA;)j*?^ZMf!4%qTIt^Kd44MI^D2Z_iFb41}>MXbvbUBKfckBLkg^mSa zIoK^?umbz~^PWS`3hLzl+2+H6pWE`6*Uf#hamsZL1#OPfvJ>P+dG>L+6a)Mv9UJVd zkHF9VlL3b)R#9+>;{5BVH5;m3R!1E_mCJBY^ID}%e7I+QopLcyHG#Q`1`tHd{=Ix& z^Job`YwU)%y;k6bA1lQ5!QtIW-E^-x<$jBOGW5Y$)?uku4E7kXoBD}k2}3f-)LvEr mN#eF6QCKbYizZR0ptFN)lY+rDvLDh~tVl?EX1Qt9HopNoR(?1D diff --git a/iOSClient/Supporting Files/ps.lproj/Localizable.strings b/iOSClient/Supporting Files/ps.lproj/Localizable.strings index f45b1c4de68c0c61e940de8ed79f584a20be6df3..f6a1e686cd122b74ac671d8407b8cb86811d9a38 100644 GIT binary patch delta 2235 zcmcIlZERCz6n@V>y3*aZO2_VMqEa92u47Xji6D2Zoy>!0a@2|qglV~?KVe( z(*$B9h_EyALevWzT&U4;7xz~E_1M4Z}Q9SUa6}#Hpc;mDaFDGrNtlo^Dw^`BB>cmXK;vE%fF(k4= z6a6A0hG~`+agm$8x4KG&kX+Rb4dVv_)z63|Z4Zc;(5LUU$2{`sX;7vU-CIUw_&6wd zC}f)seAl%Rdq)CtnF>p>%-sI_E5 z0Q$&#f}Wg+NxKS-kd=2n1s7f&ax9uIF-@~%GUo-C>5y~V>T@m{HJV0Iv5PRL4Ac*# zSBQXnH#9AtGJvKxcub!YN^z&g^p_-*!y=X+Q(+}=qNs$_{P%YF*dixJ!6E;;0Ik?n zYa5?YIk@yM35&{b7f%vGQ2x3H%b_sE(^!nIlh_GoT-u$Dkyt+%3MO$ENHIpnEYSh{?WkjF$u z>=ZhkB>w4_qFF?Y&@+gK10Fn8WkKz(ZDXkgfi*42UZYVlevZ6wd2~`8y2TVA;aKg!T`aN?zR>g1N|)Ew%Zg4>R{v8AnE&djln&Ra0!SMj914j)-! zk@L?2Ewn3Rta6>wNfW%N!3{(0dw_1UXq1#mGwtT67+jQoI+-xKFDurI?L^56(N4nT z5kFmqm^cPoFVrBxy}(|Y3vS|Mpcgnc0dc9AVb*|-$Je{;MW-F)G=Yb|a;=CME;Zw< zP6}gP@m0VH4AGbYC-ALhzMw`UPV{~VeI``w-0G93H;`uDx7XwRF)Jq89c%v^H|#J) zAd@CHIF)o&moR}HHNi?My2xVskD&Z*9rfs;8mq@xqNAi8%9Hg=^2CO=Anu=k95>Yj z8v8^q^+<*oN*gvj<|ZB}xOzTq_b{~xi?hwyDEd-GohUM!ga delta 379 zcmaE~gkwbu#|9qb&F74N@NO1!iW8kI=g+mdA>6BSvJWrU^gKRBHr7&x9EP0f69XA7 zrwdpyicOxt%b}deP{feTkin42P{xqTpunJ}puphEkjPL8WS0QxY=+e7(T^D=b@YG& zE)1nW<(Uk54C!ELkhWqVs{kmP4>Tqf$SY>hVVFMQJ);G$0z(Qy{q+3TjI#WRVCCgN zg+P}u=uH>wW)z=1L5XYgg9`z+(*@QrvQ1YIVw9SmAjrtGollUFj}a`m9VmE(ZSsl) zk?jv88J#9=uUNxqCB>fvcBLE85jh~MryoDY7|je6oIde5qwsc{_DG}@=1)$H0 YprQppYe4?b-~N%0$&hLK13{)H0Cn7EdH?_b diff --git a/iOSClient/Supporting Files/pt-BR.lproj/Localizable.strings b/iOSClient/Supporting Files/pt-BR.lproj/Localizable.strings index d101249bafce75ffd87556f4a11a307a98457100..e264e01611046dfc8ee2b56ef34ea231757acfc9 100644 GIT binary patch delta 3477 zcmcguZETa*6@E`Xj4{SZoY=;(Lu?Z01R=y-S+@cuDXiUCT3{*^wOzm@E|@uvaR_V- z0cHF^Q-x>`&<^zf8V<8r-%%8zWHy2U5AK1TkZ$0){!y1epvl6*&kvx6a;d1TT~Rb=b2wO zhmuYnA!5!WxUNNrJnnm(oJ@{TT=qxE!{eg$a`hy+Xq~zIC6zLrs zB_GMum2p7izJS3~Dr^2a(V;mmQL{NpZaM(geWGKk@YLIZZBl;@hr~U$#=-jDh_Abs+OE{WO8* za$;e)tOem#0!tb!K{|5yF{%moKtD;R!{lhkwG+8|a7}VzQ$(TWFb(kMYZ&jIgGU(-uGgK=BXJ~?%6#5U7 zdT)7~W{&3OttD|Q>SanXzFF{<#+~Kx5mc7t4G)DzYsD!;2JU$D`dVt>#|I_1i4=sb+r`fb6;cMWSDn9+?-m(!s6aS;=LBp@ERNK*Z#LVHl79$mjy{^JtI}jr)uiP6h)vm`lv8VD|N>7);q9STHakM_$ zH*^b6dvMJFiH^`nigvyne~Bh@>su3x0ZLk~P(Yezk!MCBtqr0^l0ym$#sby;Bu!B< z?S%=O)Z(~nZ^VF`!sq$}mCFpAo09Xj{_$9sF+kP$dY9meOyvxvMsujdv*u2(DsX$v zI`8WmMm~L_^OoYO<>Z~T9KS^INd@lb;g#*TPQd`Mlr|}~YbZiieoVF8y3jqvv>vb0 z$d$#k;jAk8JU%Hy`Cm%dT8Vy$6qU3fLb_0#-(IpNpBH+*%5{}#)-QqD+F zg`n9w3^uJ$nbQh&R2F%4-Rc^Ofe&@d<+#(U#oJQ_@YacSh!zKGq@|N8%k=RL^(pev zIWySC?vTT2ifB`$KTs5`Rl(|vLhULa6_UD5)&!k-Qvd_VDp02>ENx5y;J?$J@16T0 zjT(iG$}5XLqp!*3LF}Qsx5C$d^%-5_;88y}?hEpPedYX{7k#{9!5BXFof6q`lIq31 z2Kz%()F+ps*sz!6U9$0;*ktCd8aG*mlSb|#b2CUcvp9nkBulWG0JMe z?8-traZ1VEMy-q>CKH%Yo^&Xi%Kwb@0)&as6qm<<+1JXsvO4&09BijaoYBIcbzel#8r07=$ z^76~C`Q#7%=>FApi1@*2m+YQ}md1YS8DZPjz|R~E$Q!d^TyyqP)dsa&<*%?pRPc6v zrtzC{euUMM!HD<4yfJr-*R`TYgm&jW+^Ml!1GWd5;dnYOYb2~$l8B+B?2c=_V{#Ep zi=Lx6KlpjMZ0^C~aK}XmxUG-eD#(g^F`*msv2E5fQ+%+tENz0Qp=a@am3lGGP;gHq z$)Xi&@oNm8SN=Xws>$0WF^9spciTPB)H`hOg*vRJ)CD!PS)-^7$rNH}Qh_Qfup!gM zz#I1}EwNi#ifMdt(&^^Ey$~PS=!|owIUZ-6JlaE*^6*o7q|3W-GIa5w&JyX{K;;nW z@F4)QH4*cuTMbHOsx8U#!B6Tqmn!Lf`k&;hSvFUnff+TG!ff3;wSZy6vyIV4>Jmv} ztFQ}1XU4QRG}i8VTCko&?Hk=Fbsx;|{OKXt6)Bo1OSWQLyERhORHqR240s~|yWZ0f zuwBzb(^iLv2Wt%~Co6((XOLUwg|S24JTY0ww>4VSD1DDY?8wC*1_Ls76Zv=46h*mh ry2oPLe1yt7XCU%8=l{fql+_g7Bgb0FTa0rlUziq>%^S(@vOWG62B>w` delta 2191 zcmb7F-)j_C6ux&7ZA{xG8k00lqhkUt$hxK__QBR4!M0QqVrUgb6q((b)qw79_eUgB z43s|C;(+&|C8aNI@x?+41Nvg1^bhbs{1MtbNRjqM5V81B{JwK{va@3PgJpJR?mhRM z?|kPwXP*ArF@L?|$Io|~rS-bYTprM=h05FKQww|E@9Hzz3EjDH?D#Ji!cX7mda16? z;CC|o_M81?WL$Tc<#kR+_-?MN%~exo%yEVXvdUHOsk*8tPZd>71$Ylsl~1AfUAN8{ zeL{DeZJo|m^Y;@yVun7`y$hLpSN56t*Wqz=SZ_2J03v$0NvE`Zf8|fSXI#C!HhPfO zx4w#A`&kz^?)6j|-b;w%BATlTY8vnuv4&MwoyEV8wSt<(TM3w4TmkGo>;>kx+j@N9 zkU9;MDz>Jv=&4iUKvv9wJ9_YN9tO!=sP=R;&`oJieED7fYPEp*tDkov;0>E#; zsv#)tEiniua6H@t<(p5g=$>$NzSrEmr!y!1e*{J(1{*V`!*WQvtJup)A$)W7cik6# zxulP_g*W!K50}9|Yf@L|VaR&enxPtaE@Q6}Zuxr9Y+Ba4{_BCLzO47YY979&+sCQ3 zA^=c-$$HqhWvtnlby!(isGhj4C7_{pq=x3=lJ47?!GbNf&94X-d0bV=vLeE=VzaW6 zyu*|E{<*P6wLXG%LEu#3nC)?}5uzdzqHq|6p3mW4UJ(pyZbdr-3_kWLv!w7^H9!ki zn280wI6zb8LK!5p>S0b+@rkW)G2e4AtB%8**BBhIn$!+S@@yrqW6UouvfbVBFU96y;(1P1&Yfm7e-6tl zl^1dX89!E^Z>kYMuvyo|t}38NHd}&pHd`Q}2&~i3v?#vcrDAf-S$tUE9Je!SE&!gU zbPgdC)`wW*=}Y~!NCR(4iVLXJi+VCItgxFYYi3P8XR%tx-Ht+c2rsPznDld0uY3~B z;;imYQz1-{;+_ElM#MHaLevKGnsX^nTd=uI&9TuKV|Iv z9N@4EZ41k1D;%r#+Y|RQ&P4j3Pc*aF+nw%kXm_tY`qOqNn}AP_`K|UOd!|8VELUXd zjKCntt+LJ(snEDwy!IH#2{f|5e0#PsL_b97eq9S(e6*B>$cVn5trej zQD3U#;*R1-z!)EVFu<17#2A@TA0QAR%Or|`MtShTFie2N#Q8h-Ygy3;U(E7#_q+Gp z-#O=Ze&>8QjygX2(s68}K_6KWp4FyWRG;cnX_eq}Uh?Y;D;i5%R9waN_@Q}oE%gEp zEwvZFy)d$L(^vCGPc?+yJop-UIr}W#Pnprn8%F(nX`Ec6BioYwdgP>QH2&Skjr?ei zJa++s*W92WzZ{?{eJ!+r8*>zpZ9N?g>78%h&A$c7Bggagi*MAKW9@D5aymeDl;+Kk zQPgbCwS!5kR+U2BovM|40uC`jnQddlkWjbUYwq*l zRtA*9*OYA5Y^~o^UHYo4Jy+IBOdC~KP|7U-Hmu#L7U_r1ERd8@YbUC z;MvVh4HV*$&&e4nK9`KmIjk+LjBjqbPX2PJ&Nv-kT`8i`yQY0?ts~#?B)R!}O}^RU zB$F3kj89J>ftoDnS+L#97gms4?{9UpXOJ9X?&h--6lTv4p;7z^w7C8^>^~r2nGj3fw2mtI)vM&8bdImmsFR0ZM}8-!D8M_XAdG7%J|(Wy1)|;lUH9l8W_F~tKZL%i+XwAb~Ml1ZZP#kxJEDX;QBTc^~nw1 z0g*+2=z(%zC}96K}e5z=H=NHMi4qNELxCSfmauu46L{1&Q4h*?(hY%u~9ACh;0 zz@)U6pbX@hO~4?NuazB;6kO;M8bxFPe9G6 z(w`j)&MG2F9}oNWvDuDN=tiaWrYqI_*I87w<}f*Ra?;^9`?xJL!;ELFGu-8(YJG9? z0evFulk3qqC}~E59WW9@SxboR_?6Dds5+i|m?{Oc;iK)893Lgjfx-ds)os5p?g?}PhZ?HO#6HsrV%Y{?GSo9r zilsIKFII%G*yLfMIxX1Wt*i}jmcj*Lw6%yyaaGV~p~2)zeh?UGN?OplE`fPAbJWq{ z55huw2qNnf6iS`y8%U?nplEh6y1ehq^Ugf)^Q%1AKd7Nr zJN4R@TBO!9x*Xe(ghNn4rDz%Fw6IsCa06ZCAGDV z-m_GNI~B5!fn7^a!#?XW*Q@ND=k322)2YCEj@2wXC%7xXG*Ipv=iL&-|K}LHAgRTrzW1+sMJ9ZE%KpxW<>hX&_}Tjz8QBYTm%;qOc3uz oNi%th8BFQ|lPDI4Wd7Y%a~#*C&O)4` z&pG$}Q2wi5FKJd}-R>&=nDO0_uVyagq z_^w@rRUbbkR79mzn@ZBp282cr){Gf2pFUX+D+-=c-3;oG!_g;Jf9t2}ad3^AFKpgH zLkKQ>v(h;_b-8~9HW!!D;Sw4o@?{s*j@wI+_96#1x`8_$bc^H zslq9jo#G|nnUmr?Ox4PPy7=O3Xd(bX?F|z6OI76z)L}sr47pVS#j_kr;kScDGf1C> zY*CxIhN!NBi^jEI2eU?bEgU$G6JzBwW;twz1CRN#AR7K2vbnk~x7}>mH=SqtBQ70; z1Z*5M(!T}aguP(CYEnrqCZYOhUu6JPO-Tzom!~K{h1C|m zI$29TTzbb-k>miGQ{h3MGS=ifogx*khX8%k52f@afgP`2w$n%-BHSyzaxTi3#3$fg^stERU263aGv$fm&wCNc4 zqMHp_?0EWQS+TTU0y2`FX!hv-m0T*<KuW9u$P^s+#@7{g0TTZjt9r}#BFY;fBi((SghOGi?yoaKYOZ?6tQzHEuy z$^>U?g<~kt=sv330fC%0RZWF1-uVZ*d3LX#21}>WYhVvW8M=j|$aG02 z){d075j^zkf}PonWo~8J%RKT5)Atdzjl;}D=eu@3?b1?8u&~71+B%t8Zs%CCjgu@L zQ>|rxEwel<(rKHjS##5=%*WYXlnsR4wppwSeHqnS%v*G3wxdjil*Q`DhPTTdNhU}_ zRnx6H2+?2t{MFI3`XM+S{F!3s3Eu2G4>C*Jj)Akv8U=A}?H_AfNfIVYH>N90pSTG} zLym4eLrffnPC?nj(p9NX5)wA`-s0b8e%ql7$%w^b(&YCteAQIpU}@F6YwrpE>=wW`t$rwgvtZaoPWjNaJqXzJCBSO-aN6 delta 410 zcmeC!%W-ZF$A&i3$p_+Crf=Y5tVAHr zVaR95V@L;!XENjh#r1$}7odSf44H753?RE0C|&>*&1XmkswxIT9fs-ieluF|DlnuV z)K9Pd&M3>D2v%MWR0wnzgWlxCkK&tG-I`@Poktw#rUEubvFQ`U8Tqy=Ni*^>A_TYl z$S_`Do2-#0vYkVP@z3P#Q9BrYq}Y?d4tJY;@q_U6W0x2Mn1KS@WiKVdAjklE2IT!> tpqCQC9#Q~$tOzPv0CYFV`}qthlN*0ZPCp~fq_I6pn&}P#Bm&OK5jk=r6rCNVh9OwY@Cu-P@pYpx0VS4fi#08kc3Z7 zr0u3@t3*XPRZq9714f7Fk9D+}>Z+?I(7N?w-PCQY+XQ25+A4L@Sczu!n2F+1cR79*e-1I4KR+0!ySaW3{~k$&ZnEY+_{>8hyT z@4r(FLT*24#KgMpCquGJzj(I{OnmgZe5$ z@kigQRVSAhsrad~swSvxaoXT;%!xa$Xm;p)%2|Qys5*W;qz<2O8kZs*i=714mzbce z!Y)FC-xmDdf&V1d6HW_%)g!0s)uj^^6Yo6NGFzQ#cGUW=uQuQSHUW}(_<{@L1@~Y2xJC9+krdl@^bul zg2KH8e+jTMhSqRb+=Z(+zS9g-$=l|RBMmT$Wku@Mpoqa##(k;DKoVV8&tL% zGieNYV&JCfabrvjJT$^enC=DxO}Wz?@u8ZN56YnGyQ8=;f&DH7GYL!D^zqrgfI57t z{H2%Liqy+rERUq%R@@}C(-_LsvPfvgS_obO)&%Z&PNL4lJ!hVtq2HY^^HgGvqa*jq z{FzB`$DK9m=;vzGx9%xWkG)Z*Upp@=)TUc2^b0e61^UUbES?y7y*r=|KI&gcsT)L{ zXaHBI(H)0ARzY*--k!)d-Ta0O)vR?$Y-800Q)14Y#=NLWSB*Lw4(ThsvQ)>FyU~X^2}?&Fn70m-#|rDt+IAIb3Tt z%(f8wzulzXZFDAze|NyA4y61l(oit*^9%RQ(wm-<`PH6jtS)+#gwEwAIvrSnj`Lo!GAv*XGy)KuaB!6|xTR0?KCKW`#ghgdg1-^o3tuAQ!pXe^Hj!X7SPBjHy`SX95t?hVu|pgCA8D=B}jl z@$bmM3h%H?M;LlZ@FI5D!Y9%%LshdNZGpOib-mO{|F%bV@0)?3sNR|ybu6!<^y;|I zchjY3^DK44vJQg|EvRc32pl&J!zxMXv%6)WHj8;gL)%A)K@<=p z*s_U*^@W`%(!ozhB6|ndig`HFF`8ps~UmXF$4w>&f6L2;oR}kpf zE>vPcS(qjhD3_3VAM*TB~LT3W!g5al_VaR6}9+G!y{^>!^kmVpqx zX*Cs?0c+DtqqG7JHHoKI%d&CGY6oX~CbC{~ivxq|O za;Tv{D~v2Q8Fq5c1{U(sx0hg(2hk1)UYG4OB1xRU6&>R!#u#!X=Cmn!G`N_p&CA~c zCIr>SbCxod%bLseJDV~b&T0J$ELWFx9P>$7#?^~I2{l;%JOGNDnPYmpUnJC~kIU!M zL5fad1Z*>QX40W{{W@S8Zu-jKa5gFUy@Y|rwR$%44L(XCS=MklcALQeI%^%P8O+i zwL61*F?!pfdn0g|t!G5^>nE2{Zrd+c01FJ)tj}hx>8%Fjy(flpOqI#nsMHC z437T2VuxB-7p~b3P}+*N#3o@IG27xs?6bpcSH1Z`D!X@8hK_w8OUKS&=nS-F?PVJu z!QK$4w_%T+%Nr9s$QxC|sgPdx6`8M2R=e8$K<2Ntj~Z;vyYQI8#{{->K8A2@6u`L~ zS6gsRmwCq#!G)e-)2B9@oo!}kNc2&|Y|;(EiXCD=B}=M|qOI3bbR&@zGH50nJ7G|1 zpE1#!d)Oj#RN;C|_lA7I_ASO{J8rO-(kE}&8HQcik>_e3_2eV6qL8ara34JAJu;X1 z)PD{7%E$Q>$FO7rz-fjSy2fdSX}XG?CXiF&KsjJ2DT7aXG;F#-xrrFT4A&tF@M*%w zO*efg@0~q+YPt|3di)c)F#Yl;G9uOcGfUOv@p5(QWO2J4|5j$InaXmLSFr?n&+MtHJ8W@d5=F*{v)1(7%eIKst?0T7I4O2Hb$I%UoAbkM+^xFgMg z*la{*RU~o00~EQ@AO}x6WmE1b_<1Hd;L^F>xMteg$mscELubWvwxCg0+uWD&O>Oee zQ%lA$t8AXB$2ZHH7|r$nTQq5woti%zO$$}Ikwo&!)akBq&!}; z42c{v?fU~;&D_6sIk+MNSbF;hvbKnkBJ+Sril?r=4}so@ussB+l+5etrB$bzD?>e= z9a%~ndV;MLd85+2u`QRMsL=1cC+F#dCoo&T@SFT*PNXC7%xYy*XT9;?zDvobJp*U@ z7w^fST%`W~qwu(WO~a%TqaXNtATw@9B-ZSB-aX5RrNjcS8V52M6LWXS> z*4+h=OsdV0z6Et;F%oUT*p$gb1ZMN4mUrg767lMc=bBldn>f2_)#;j9qlhO}MRbXS z=YYy*@Eic7Dp_yJh}3-WgY)Y7SHcl=UVMfj!JcPourO_UlrhFWGBCgCi=WEX!B7<@ zC%&8D?8~aLV??X2?UDig!(y42UKj9v)2B`kI{NX~WWjBpoig~EiW3W83Gt-}-~F`X zdYK-4UWR4>+4a$&uQ|Q1+&AC(!XVal)8_gRM+b`BYJ{uaN@qe@idHYwT@6dk$(wb1J delta 3088 zcmb_eTWnNS6g}s1X$x%!(rKqHwe-@KR(bRReL#gWZDn1{J<$+HaT{t{_=tQt6zEs!-rwOX^seqIDWhrx}l2@9CGuH-Rf=&tj>VIHV&d$|m~4pcuIU zhfA|!yie&TCZhAgl@&_q_lP)-84&=@aLihS+0-x!9|>5K5q)A8vs@^459XM5=((Sd zT%e>_f~;iT8N7wvPcF7)quC+fif8dk?>jMJ5_i^yY>bQSy)WNcw0MNzs-`Nd$?dro zaD(4esQq)YK_vq+aQWKH{>A%6s~C0fhW*`+7(psYEJ9k7lo4P*B{;(?H|dc;9Ed^5 zbJ9P{*APPVIKfz$x1-HWbQ%3+5{sFoSP+9ETaS%K01~*D@|Ft!le6CI|y^B6HY z;xl1Tg5f-QRjATxb60*=R&8RNuaWyxxX$gm)t+^Jj9MzbSNh7Mn)z|HcT#S4d#b9s zSK8a`emiLQ*yHvldyfs2>|uM*?!%X@^Iv)}K8EKFc8@xFPd2KzE$gwm%{DpN3~NUW zV)hVbJ*{qEmu-vs>|wRG+-g+Augm4`&1(&ASNqcJySMi(k>l#fI?JyX_0OC z`Q~8tN_zmS(__Cq>fZ1-t1Ye4m%Ft}R!Dd5c9lvlleQdIA2-YO%J;Rb%01pDx0mO( zua!ZkdWR+mVTM?#BM)W0+R|+;jhPnkSXKlKEK(>dtIr&s0?M9#c=XQC@J1*FM%41n zqJ_`BxV22k$dHqe7{`H&B(GRwxK4&z;+O|>$gYjQ9L_hgGRd_ zBaYyhBih}!`s!40P7Z0i)&I#($-B~@yD%qx)nm_T2-z)Z$}f^H&6!&exlRTqG=*cZ zVY4N4X9E?|c-3Y$i%Hw7AQ{hIO#_ycmT{x?#m%X^e0HHOXPx%|*#D_95jv&7XjegB z`Vc6^{@)vONf+cI!M{5BhpelQBQd7VO(CMmNemO}w@$Q6xYBZFsU~I@^hj;o=k>|J zQV)QsEday%hc_!ERVAD6q3wggYM>pd1j} zyt_gaO-;?tV+hzQdUSwHYDR;Q)X29V$6 z9&tpN1~cb1mdR9t+c$am_lPglYCKt_M5+~$a4YecSE@xgny5F|SanMt$(1X=^{??Z zKyJN9brsm1Bt12lJGeD0qNgHdgLJ~Jgu@by)H7eqE;P24Jk15WX_#^C_|NQ z;&hqvm06B^{)Ep>1}ky{WmbpG{ou34>jJxBz_wZ~g4!8(lS3jGY_~30?yH~LD)3O& zG#lm@yYOfcQdc^y&PA=_N%dZb)tgInS{?SFp^kTMXAya8QH7A^ OeqC$zTIx=hb?Q%H?|>5k diff --git a/iOSClient/Supporting Files/sc.lproj/Localizable.strings b/iOSClient/Supporting Files/sc.lproj/Localizable.strings index 0de456ef6ff10bc5ce57e6b25ec889ccc5102e0c..efb7320830c2079d83f8262cf0f65914a5845804 100644 GIT binary patch delta 2845 zcmcImdu$YC5dUU-y|&x-dffHeUfXN8cP$M`De`JeF$EGD1ql=(5-LLa?#cDRU4eko zf(A6v@cNL6540G9B)*E*x~Xrr{kDzzp`C`hM` z@=2pC+P?ki75G;_IOITSYP58uS4UerxS|6${-~6r0UOrWfG(F+XUqK8^J7E#U`xaN zv*1iKb6zL9@jxwjurdG{*dHL>QeUCmH8&Nr&q7WTX~{dK1=84Q!=q*3#EJkoOuecd zrLr#FuDr?jUUs2B0C`FTVO;wyxc6i@#}rb`aAE9PPQemR4xdXn@t8WCUjvn-jk2az z$XCyK#~EChoiNC>47xRaJ3SPPIb0>cM8nt1Io+mdo zIsy@rh3MBrE}3iw^_sLz8&S!Bc)+a{#Cj{>InAO-ia|XlI^d~taL1fepx^ay(a>D@ z@G(myvyZxBZ}-B2GU+^R!*Bp{@F|^WG(1zslcQk6UHiZln|Kg%CMHqFc%TL*a~`UB z+C(EZ?JRWX(@2G>N*#<>-dgX$<3B-NO8hXE^)p;aemHX{#pc|APKSKNzuvP!(UUV@ zO}TR`!!#+UQj3`j1`IP@y96jnIT}4T^E^J7CC9XSw9nX)by4m;>v+0b3?xT1#DP;bF#O zSHOqHFW_3tONdV`PB8_V$YCeN4pDTI7#rxWhXArxyV7nVtFQ{+si?y%qJcbvU2|t~ zurX8wf9l;_wMm#~?7CS5<<>)996Ak^nH{9uqI{4@*?W2GjMF7yP}xPW?{^wZ%H0X! z*!y?jBOOEgY1Eh5w24?W40haM({AF$CE$`nhjUO*)^g;cw2VDzx}sXL$>_P4X%P<< zy5yiU4~IViyZm~dE16jkkt6fmkB@bOP;faI4JAh?%(hrXXeCr3%7!PVTC5~#nV3T} z!*}#U_!8WGT!@ews6_R6rAFd)r=S#swYW4)3V!aUC)NpyMrJ_5shCU8A=04fr)wAK zH!BOKFvDfP5t1wP$O3Pbe~mb zB_m(VLG=cu`-t2+!yxgrN#hJ5a zTtlANX`F|0 zl?WH8fzH>E1`iA`DMk^mb0xv%P!|8K{<6u~8X-BSNpmFcMT;jys6ia6QuxIzp$k7A z*$i$hItZoab{KtvzE{rEPH8>v#QVfFIWXT|#I6}bYkXBQ3WG@Br5YFON!IGK=uMRQ zz&8~3WozD;-J>11p^>fGWZj$gbdxEH7dU%9cw*PR+QO7)I@Ba{6XJv+jGQuWS~ED$ zIcLV}mg&X1)_60I+LWF}Ne0WcA$G+&O0=mOzEGmgzzd%d6&?*}&S(F}fbIr@}Q3uS7IN-{FRgd|0)jfBXUqiN&JFh7YdqLhe0 zO1jXc$S{c7m>3Vbi;P^lYSqG3xrmYqtq`+_?7Wv&1GRYXa=-VT^L_6-XKgWKVlHEP zbf4P1|Fc~kYgn-e$2c0yF^EC}k}7ZgYE`eczc!molrHFk6c75gs-vOXW}TP0!GoY$ z+n?p(g$<7EUg(G4FvR(*k-a=Qtlp?yPLu$vVt6zY^4DKR`aUP0-R+Xu2^vwN23G6W>5% cbl3JKN~!yKu+}4H0&ItD46r2>t9#h+PZA63Z2$lO diff --git a/iOSClient/Supporting Files/si.lproj/Localizable.strings b/iOSClient/Supporting Files/si.lproj/Localizable.strings index b86cea862584d736bc09bea48562109d934f661c..543fb618188b85ccd3d04fe6b3e2ccaf20d4b926 100644 GIT binary patch delta 2195 zcmbtVTWC~g5dJ5Zb+fs6_LgLmO>#DgR&9(lMyxf`q}YejVyzUlAWhcIZnC)9jag$* zM2!ztRGOp%8H!+6UwV^LiF6+n+eL|(2d|~HC>q6Dkk(rtT3Ut9{3m8(ANo?3v*-M0 z{{NeAX1Z-F?Rpzhb!>tAr}ohp?uT;2cGcz@luVQB2FkJ^9gvcqTWHRaW<_w>YxR@4@Y;G zU}|qEn#DGXIl;)?q7vA?&u3Y`FRY)oIN731rW#gpX4Ip?RO5s?9o%HbHNB5gb>M*r z9?w`HWPA(@ve=JWyc(MxPCypC*aGii7h6?0J>-OiAi<XOVd##cyfVa`FmG@7iR1=k{SQHX{(vF^8PrN7CQna4SS#(drNJ&bBPVV2pD)a zP+qf%>4sIWYG?W(#*v&m1I7A;x>Wpw)k@KbyU zY}7HxfA21Zd6b%f5~o0KW+Af5IAdW}Xr>Ch)O#K(D8=UIK;__s|W`fRUtew zD?q6Muopjfn0n4b6`i=q`rUg03<%=oWo}Fz_fYc)7-a$<)Gka0i!i}0OfW3&+_ZWS zN@w@#I4xJ5T$(`0N2wd&podQx^YmxnYG>{7Pbw>XB+403y!sae?~Ll5$tib2V9GJy zpQF!ced*=b;8O?P9b|dk{u$=djSMI2$347Ua2IaiT8Dw%hstr{6NA<-0~glE4Z8jg z_t&YW61vv{CAive;Ao;0je9oyv&~f{auDwcE@MbHym;;8H(VcD^d5L~SqZq}k23cw zY6Ax~N42s@<%ut9%g=EyPV(uPR&bVklu?tqZc=seAiE?hdX2YEcy~>Ks5U+whw+Q{$1x;2D@mBlQ>&MES>YB`0@ zfOqs&u%kawjjgBb_*a{;?fl5sqAZ9aCq=-HKEoGxZ0 z?6))Gv?G3s@QIja*T;nypVnl0*3}U{BfOI=0aLxr7@-*mFKC7)%O}Y1H}MV;s|*xr zWSt$AXq1@P^h}FZ3Pq_h&$GQOA+V+~_<4;p$+-|$jfyp=9I6&?p__{KK|OU$fB?jG1n0f4@gSk2L(iOQm_W>_FF4D{S(r*@KJM%Wx3chO9Dt;$3&g6gT N$^Yw~{?5x8`Ui1yG(G?T delta 365 zcmX@Inxm_mV?&L}W;;_czRgRVJ47e{)8*Q15t&joJs^vbk3Ef{lp%*9XR@M@`6M2` z=?gL$xu!4p#K2499mhDrtnh7us1&5$~M{xe2N9X+6c3s7qjLncEW zLpoSG1IR80vI>Br`3%VnsX$&agAT*=d!HCBcoi5@5bCGzea9%vp9of74pazq0fXM= ziI>;gvZev`7EQkYUvhe$2qWKiIT1z?MugyYKT*a#Y}4CVm_)XJlVMDmw0*;RMmH(; zB(M|Rrf+<}C_G*46k`B0P-J`VDaJ;T?J|!U7jR8K^_Ed>@;L*(=@Z%+d8Yq)%lK@2 z!Z*el(w3<}52Qd{k_q%>2G9$MKwJ#;NFvx93LqyzMGJtg1$jE3A!Yh|AtuM|(}bAb LF-~_9WoiNd4SQ&Fv5Cy4QLtWKkn`lJKq-|ob(mm9niBug8bTBk+RTaUu@7(K0pwT8( znk?J@{qH@G`<-*Xd+txqSWo}TI`FYW<~+t-B0l61&caoCu%mE|dPv+TykeJwUEC=` z6*hUxW)#ZCUA#g#tF83fCGPoM;>t;z81VZO_5PK)^9p&G&F5j>&LYfY+gK-ijz{s^ z&3afn-t^;n3yZTbo+2!Ycj3e@gI68$ZUwi?P#?D>-kG!C5VL06#P#o$9(|dciM?}e zhOpN7^I9O*$2!I8F0Xj3#x`*Sl=VVsGybD&DomKHO|5}(dG;9hI!wIm#Op8?HH+d# zZ%~hSD$zJ*QcB@dJJv>6cVbV?ZM)IPMXdIubXIWNf)0qAtO>ez!uc?pg;5Lww5{9N zq>2BnY(#C6wyjO{!>3O88x|WDEfE6;a@=98Qg2vHa1zzxnC{oCASwWhoUfR zRUW*)=(9o*v*m~vziUsNS{=*d3liQ9zT5?)Z8`w;R){>K+)n${t~?{0dNC78c{c*T zJ6T*7`M57P#PAWHeJ3CeZmy93I>zlaU)Dt-Y{4GK&_d$)i2CvVgteh`27zRm)d6G8 zYWWDDA{e${)U2NR)!zEyY?R$(UuVL(*OQ39uw^cZx&k8#%RVe!#!c1%Pg_$s=>)m` zv$f-n7I2HKpTe!VeQXGr?pf0%WoCK!`%l^sY?t9k2SXKxWf(z-Utc;7Zh&c3VkOaJA z3B={072GTLm+(@t@?r(8D<5ker6^%cSp=WNoQpmsB;Pp2XOunQerC%}@H{4;h0pS> zZ=<%MyKF4oRLcmFy!<{7$R~aLPSp~GjS@1h3S#^J0+fmK#ihyJ6S$ctD$cvaw2>NV z9_DtVTg`~0KS$2LI7yMqfYT@}?@qzg1fZybsmvmCsRBT$`3KK~w{AO~DVPV!DAKU{IR&nf?0#clG!I6gjk zY6ZXcgz&E~m0!F9+PhwBk;f&sCI?>T&l$CyFcH%@;3pn!X!-$84^(5 zhvnsc+$;9hOh}$M%vVj7N6vAl+%%7y;-JSSVwZEqZDm7(&-W(poa2LDG2sFj_jT?` zet4Om&Y4a`v=f?iEHuYbk#?#wCxlu60a8nz{g5v$|L@D!vTw1K$$7uy-**+P10A;! za>)B%f6A8>Jgrdffv)>vPPJEthIv)~8db)9?7r+;#@%wqFrO>x=jVvQGd6Ln!c|O3 z*9`}W>~CUiuc%%dki)~glIq+pLVvgBo8Ufb!Xwp3~6^1F=?J_+ou9DE01ylyB2|^JR)YG zQw2d5=YnN{t;nnhg5QAMH9-N5q8No?8ayoayjeDTMQRP9kX=KmtJYxF%sMKo|H9pF zI(0Mwr6R|=#g7I{Wb9dP6|*Lp;!2T4`W=S#aWdJ39K#7k!~rKw5&FfW9(m;th?{4T zE0>5E7B_MC(osQ&pE@zH{Y>P?I74toZUR_S*g|j$J>e_6JcfH0LDiv(lcJ#UZ3iUX zsmQ=8jL<5I3*{b-iL_``Z1k<_TjWE2S6XujUX) zwoKxDD3u3)01~sGvF=*0R8#Xr+(NA(WfJ}CzE_*3@TNH;j0B)aYJ8|SZyvRt`ecGY z^Mfv8mEd>}cEN{`%(OLYGUC)jW{qZgbG7Sqqe4`bZO3CUL}A#jT+vQY63mw2ecZ=G zqG+U~r~#8LDbk5jPjZ_3WR(Sl`kR~FGLxK1vk(!QZXf6^0Yc;%$G@TtQGMOJYcw!T zuum%qh*tI7uC6MheH_*7!BdjX7BzvIp>ov_)IpynXT8g*D%A)$f^;i07r|H*wM5QE z9zs1`csi2VL?j8yRoW&BSd^UzKhB+Ptm~O%S?H zvw1*t&j4Jt(15M9BzA;Ij#!%e#+IVW(YBj378zH(AE>nBenLgs>&@+6dXsq+|b)wki zm9JTi#rf+10Xhk^uYa=|b!cMlXI?DNH|k0js~A%=+X1IXls??y$dw=as3LU!(}jWv zkj%lX?6MhkqVZB;envucmHFfsHscZ1?(D9!$e;X)TLRP`h0)F2h8J|RGucdh_4#A6 zKG}MePZY;5S!92Ku`sBKOka)3pKi#SxE|vQ3^1c?r`R)Cmi(x|_&m1(y_4EXwm_-$ zD0gW$6cXZO${$T-6woXcQ%ULuCrtr5pG~|c77vNvF7Zm!Wo#DDZ}61p%SaUHk+~st zNAJrnnjt@N87r~UA0(V;d$;jR?$8Fh0j#=Q!ey;^x7*k#|GJ#p=Fp{!w%LX`x{Q*3 zYN9AB=olk+$SQToXrg49$CxOtEwPbrGjP3lAP@^mdf3) ea$8Y4sAM;aw)Wjluef&4F3<1fh1O6iNB;vBEFEkB delta 2019 zcmah~ZETZO6u#%B-AY|&*RpnH-P*0d+$3WxkRg&t86#1UEqo|Z11q$!N!M;|x5`IA zH2Q;R__*jHksoOMp(Mh{YpgK>gBY^-gO89H-%RDyc zZKE-&HsP06gDRzRDh%r^+C0Afz{SzutTI^dx09ERHd&{OQRADo}H{TjqjTLd& z3T!K4WWOP?X4+0(v-Q-HDK$~Z5f^T=9GpQfHtBA2-^fM#)5|b5gynJfn0@DetI$V+ zZu=k@lU&h84Ln?COkA=jA`aLD5B>pIST$R}uzFcNP`jw;-L#G#_Rnv|Y8QxIB<>Wi zr-72T=*%=&5?C3;cn~Txzz_1LV^lLIrnbURHb_~^G4-Yenj|aUMhavg&==1J^l-#! z_FNrqi*$iCgXuKDF`PjHsCZ4HLS~bNHaDGd@Q9yEN`Jv*R>kzdh5&D@rYb%@N+o== zn(lD7CTE4*y&Peydff8!U&&Xft)cqTLe)%KZ<7@9P%rOkBtKtzhMM_YBehB|$Yo9B zwo<6j)I{IR;L%#6%r&7&H1_Gai@gP3C;ikuC9QJ&`aOzp`6uKk)N9&hxFqnhh!)Wv z=fAIzzd?>DS&@PJ#m$cgo5Z061VW2n=pesdFyCEpe@rVI3SXb1k;>||!ULnQ>O&ic z;bugL6^Xoi2Sp0Uuh4#*?j9?NWFRj=6E5rnL}Uo8n6qKETF2h-bLJ{75>H!s3-8;F!)}*2a4Y_4REQMs^BIl2#%Ii`SZyVS zR4qv!d}Xb(@#l^YA`iovYB5?d1&}XKH@ti&U@Qu)gx*K^HzQl)Bb21Fi+xTvTpY_B%1 z+1sR$#;xt;wUB+Jyh$KPm6qZn>0T^)61PIttI~X;&dApkpBXV1?hK^KZn7Wlig#Z| zwQU)2a&yFJ;{El;is$|l@TkOuLGTu|2u)UY!%0up8BtyrFpAzF z-}#;2Io~EuJ1<;zp4!qOS57#kcTGSh%3j%Lx-l6ByG*WeM4uN!VxKTXmj33&a#5sn zpGb=l(Ty9D5MP)kQ&x_qgL3;v-i97!8WIJOBiC+Wo>||q%7eR)fE_Qq37+?Nf`Q2) z@YD{HXRq?;-gF>@eP4n5%&ncaLwNi{Iwh}{llbWvczs1mY!HNDF+eBIhOsC_YWCBa za$tNCmgCtm2w}F~7Let)Jb3#F@ZpLmxGg8y{PuEeKMGDfHwK8JEOSDt3nO&UvgB|`fAn;6*w$6 zJ{hmZ@mO)lA`WPrwR1P*bgcD0-$9B(-BG?Vd~XTNtlCWdDA3mhv68s7uIH$jUV6Td zVVO!amdZ_V%OM@(K76PX+At}>2PRw4^EHIz;gb=0s?I6TrQAzR%EnA)lseKBlD{b^ z6a~@7KbUgbqNSMx{Bdjn<(F|IE{{7Rie1|>qx9_<6c;b*Q78u~KpJ5Zf>^!^0W4mF zbu|@J>C81)?~+|R+%+cU%!z)26fPMzA?A`tT1JBViSe95lOjB<)Pm@hN4=WUM$2ea zjpR-|$TwP=<=K{iyi;_^Q%w%+x(yC_rNxU^;t;_fI|StJIH_m?JeZgQU%jF7nbf*{ zbjskaNr>Z3(m4)GaLd42A9jqu66~4;XB|V^OO7;kG#9B!Xu^+w1>X=4FMn%wnj6GY z%ab6PLfy9gbSe@~_UcpK8I@Fvp2wb|QdK-I2HozN=gB@*kkc%F^C5h1FEc}G!t>BaGCzj#ZEPbE<4&1O0*Y1X;c0(*PESeGd zMbIJ7cls8xhD>?6)Bp5bBNbe&1%_eu(;}{ifDRDpJOhu57pS~ek)$c$6Mb;>bMPNx zz6R8YR2k0^i)N))l$7dYFXo5!#LvU@#6CH(M~QqcsMtu)1LQG(Q}PWS0PYyaHR(Q! zm!NH;U+kP$`1zuRo{1l-gy&IYP*ojj$XQ(tjy?m;vRsJa;S|a7VyC;xR@1#~X&LeE z)Z;dq_SepFBgdD7<()d`!Yo+}vi@!pzVbcwXT?=;NA;{I3xQ{`>emZN4sZMjjWygk zL-gbN8EX2G-@#X#R&F^lg3}(GirkQ;ic(a5AKm5%Dcp7){203mk+{xKwQ16Ldr$~^ z&pkNHqPZ-hTW)KM;-!O-z-Q~hv3iGMJ+DhuJC_B(yY~R4=GUp!mAryu;5P?J!z<;+ z^U>A%{mPv0p?bMpJiyg=RaOPcsR2~s@W$&9ZM9+-$bDF;#iYH%ROt)cfJboeYY<-0 zE!yO(cN*pPV_tc4ZHSfLyje++b6Jakf%1r@6@r|M|JM5O{1P%13#aH+=bU(nl2m}m z=`H?4>|dP|w$1H4Qjj5@qu8119s2)5i**mh%ZPsRVDa;k;uTTNu8Y)Cwx-W&2j5`! zs17^Ng#7Ddyw)VG8h*jjz{=(y{2i695D1>BAqv&4 z#)VVq-KRIF-dp^_!n;s7n0IX zDLe>Mxl`Qm#ULJj^Pj9^uB&wh-m&mmP}+eKt?l4s6N7%@4?dE5b;62zd_j}KtLo`w zTvt!Resv!^q`!5BE0T`JQGWg*dbD@f(y==cn!3fnme}?0HH(ezho)wRYbtolF=cjB m!L}tnpA9d0dvpC_1NC?=GS7t!oeYS_{?`F(V-JQ4XyuI&k+wC@eZ__0@hfR%IqFZ8Mf7k^JON=@1Zu4}z)18|+ zMO}#*_KQ8j1CixKqR4b>#pSZv1R>XotU(Ye3x=@%7!s9{AU)6b-Rp)B^iLe`kNZBq zKhNiR-Zy(ZBOiPErdHsRno=AaHgKY5BfeAP!HFtk-X4`wmfFYn4%MzKY??B0Xv#;| z=0fhk-4!K1D96UX9>U0u+?)-nO*N?&6<6tj{KsDkz*-t!2j;+gn=bChEfs|WUylv0 z!5*`ePMP4NH70oJ^e8N(#jfIrVMOQ~II-z3HxDf{Qjcz+#LPd+DYL*93;lcmWK$@R83A zeD;r(6gdpJIPNdO_b>RUZa-M)KjfjhKFF;|b2Y7eML8*pQ?!(vv`YZKSIduH%gzdA z&V^tEsq{Sf>C2^1fXhE9)LG+^-~5>a=i$kcoMyI2F)r)}ht~zD^9mHu?^j^8ds>oN zF#<0apfB7R*vt4QbmT^kEkL!a7VM=3f}khp2(1|*t23794u`QOT!6QZd9iP?i>w9U zG6m8E$Ckdda(qS*!>k=~^Sp_wuz9`_G2R%78aKTPy%*F)LgArnA8A zv4C5;#?fiG1&T1&pG$SS!Gk-VDWI*8Rf6N2vgy`wZlMH9b9kY2l&+5g*K?HdKJlD^ zmBB(9nqX%3bwRdc;-&FXD5c6D!3bL_#)dXKWj3T6QC(FtSCvpv{I)ENHAtYX5a=4cyaZhkG`9PLOL+XT-bOSTsU&kN9QKt296CFcFe3DEM$9RgB{WE<9Lo9FpFrbyiWXR)CYpF+Q2$h~OZnGQGMH zEY%at&t|TtmGyxnYSj#9FO{+gSdd_a^fdf-LefW} zlr$nuS5Ja@XvS+jdlja2@T|77S`%j=RS1v8ow!P~OjswmNj)9>F8Pa{lfAScrMB@| zTu+XqaetPv>1xU;S`P~_nVOH|O&&a4?_c%bz>}2hm1WP+aD|yO3o7i+D|>)BquO!l z?J^p=!z-n4EqL+!Jddf**F+=2O>)Z1QTJkssrF z8oBoEY#>IBboCpi>6x`qC@zCD5*sg>|Aby?y$!ri|HbqCDnE@Sd8gN{0#|60MmouM zvS6ziGwHfK_tD9f>}BBzFrW%=N6P5P1^x&Kz(V7gR!T5!rawM(rwc-E=TC1j1m=nx z!E70IzW_yZ;;I^Z{|w?M=%+K6xCo}Md%Y%55_bnpw`_T45Yn|pOLYodbO)bAIZ2TS zZY}yfp}N?w6RhbA=9C!Buv>)9nj^AEs`=kj>>@g2q#m4R*fw?;RXbTjNGkgw$yzhv z`I~x=f6mN^g|jJzXM0t`#ip(N`OV*8;>MHS{4{@(C1ABGSC1*LD#v~_>C|yps3&&o zdM`C}gMpv*`>FOUcieiyHHZCEe+&2c<|Ozsul@>eg`RKI6CnF0$r;MiUQV*OBp;cP zUAhLTRG1(-&RlHcT3WQ8(_5iWm zW%(1q+RA|%fNo;Yn;fXDxVh)bE<4sVpgBd;H~wXmoZctI$hTcZj8TLUNp$iX0lw`? z;*4w9rf2+N6xseuj`7sw?NXZ={iN8Fz>apC>}V`Jz2hun05ecv`~9lg4%#F{XEn(@#h+H30xK5qE(A diff --git a/iOSClient/Supporting Files/sr.lproj/Localizable.strings b/iOSClient/Supporting Files/sr.lproj/Localizable.strings index d25ba5199e9f66a15dc68152d69a2d325048cef5..45a6ac569ad33245b3a1cd15d9c5994b2aad5e59 100644 GIT binary patch delta 3487 zcmbtWZETy>6~5Pfle&%*9M^Un+lig;u5`w6?8GFtXhlg^APr+pTH3B#m!xS& z(I~VCw2yzZM{q^Oq-^RcU=phqDV{(b?JBJZSm_c((Ew@F3gg$Lr5Zv=3p>xfag$b> z_G7Z%*YEq@d(U~!bDneV{q&diXD-{%J)fW_yF%3Z2REHQY_bdOWP&yu=H~z0#pP$7HK|e^v%O5z%Yq z*ky|EF>6FSr|DPxHzzx({in@zVRfbYXjXbuwp)hOTbHC$8y*{AVlfP*tex6e*6Lbe;y3B_z~fre z%ij8dUS0Icu!*kqnrtl_cgcPlt6r<#^vcGPzfQWP`phRA)#As}QQF~`UsjUmyi0xV zl#O&E8&K0T(jimopC8LQweWsh^&IrXn2vPGTwqim<`$9&bTJQFg_a_lMUOoz#L)^+M}t2ZJ5qrfhB zKuajY|7nH=AuQ>a6IA{R$WVHkfn1RT|J`^6!pf=R_sJmr?zlt!_5Y>^8||le9|L=VWWO$~kQAzAUqlLTAG91t*o|Dy}^YpX129d~-~Z3ED_2Sg+U(#>j+oqPvj29og%BDV8)gt43<59T)u#DWfu zsfeYZSuz4cgvd^i+IRRnZkL*YX4b%kVI@g#yw=nDj9)2DZQ(`}Nq_7%&RNB2+4g^{R-lA^z zBUX&Q+0>+31L%OEN}9Z3r|BjSH9zd}9%dU;Y-=woa~In}hx;Bi{W_@54amy=ZU%%D z46{Ax1*K%bmdgU$KtvSS=dxeKs}rGKcghB};fK;g2fuQvT$L>lY}Q5~6+{>jm<)mD ztUL`p_;orJ^-;XfO@p;QdiJP;uH0v*c)D>9nu?JqRuy?oAFo7);W)$n4$Ty$V5%4Q z0$VlvR6%#M=W9x=guJR%OzU$@qSGv)e`ta-rH3dCl>WVtmM zeoT`_MSu+>ymBH9RDcI0tlp>6@RHEzFZ63Y`n3z7s3c<6$b?a#p zIo{Nk8j-qKk`4P&b!Za?FyI%MM|4nRs4RZ$`kk8d^euIQ#2W#Ww|LVn%WF{xx{mpT zvX=hxwpC|p9whZKtZvS|2Be1G<@3V{x6MJ(L!nbQ92`}1{q4$vwvo~#HT|l-YDG+R zvbv#yQzoX=_p4+BlNPE*c5j}0DEwfM{k2DE!?i3GLY_MhaFX}4>>m-k`98_TG$D@g zh36^$->*yKusAH5>F4p5b+?#X;@40YV}>`fD;yk_9PGY7@ca+>m@+w6Nw4|c8Snt| z@egjwZN;zw?$IV8Cvz;oJV<1a9c3sP4{yX5^eNNQSLVe~Qa(-~b3JSY`&?1?NVFjo zB=1Sgh7)a)UEp-6Z#$&FdP3~u-$CYdTlvdlmE9<**d}KkDV8_vW){?+hYBi}r-_$cb+*P+m z??${bK|`}^w{-H_^6m#Z;WEw6k}M~)(-`Ui2w<_@n`4xjyP0RA;g9giD?HsspZ?ol zf%v1;xfoQBp5(ltw5Dh5cVJ&K1*DD8R~MRUO}8$)!}0B|ec{skH)JSO;fFxg*DlMp z45|*NT_6Aw;~U9Y=+P&FtkFt|K;Sm1;ujZl@ delta 2300 zcmb7FO=w(I7`=D$lroGZnoMe9jIU$NBE)eT9Vls?{Fr8;A(og%kq~&9pEfX=jPoW* z1Hx43LU+OwFS;xWMY>S3c;HIi6uMAbX$27x7X^2^s|Kv++;3iHf(a_a+;`vo{?B*r z_by*Qa`)pSpT7T`xtGwLJ9F9e(VZJFyxkKFKN{aze*LdEgW3Mh2R2k$mCa{?&IWF} z(~RBLiN{0}*OWt<7;jC0Z%buxGxa$VIQ{9zs+PAgZXoSZX;|7l2a(8xK@VmA(p zoHGOKN!W}ilfJKWPme%<3h&gPx1LWzcM5Njhbh=)oabRRZ65wXcLmqJOawQcjPKOG z9~d`FNgX#I_344|Qm-D=rj*ofFklkl*M0ilgXV5L)*XJ8(jS`e+UGjgJ++`bRYlBo zRl(}vUs9{6vxr?Cr>b&(J zC!R9yRoyq5hmHrk^U&c+-8$ASJb^5BW%&1-u`kd!?0Sy6*p=`rsyy_=p>Opgy8j$p z6#kK*I5vwHb;@ks)I+b#Sl>mP87pP2d7DKA#*DsV<89cug=RK&WJ)vy6GmNCn|QB7 zw_x-YoeVM`^qA>edSv zkAiBaos8MNt)DRO-O;lN(Kx5()vOBtyrTynYAjfI*eM^e*faK&xVpfh25A+0_Ok?e z#iF~4(+YBs{;WgNvJw2skTQE>LR~Ue`Y07tY;{1~wT09yZG=if8YvB=4t~GV)xZU6 zwq+iMC7Y&?BzbXOZl+ta6d^Gfi~+XrtU`+|BxNFEZX%TeowSD#AgI$ocqP`H*e~*7i zQH?#)&;>7UTa`A{L8y`9EPp?4yzKj%$}3#~Nup91gVb3=nH&Ull_idB+N+jfZRkKO z_wF#{*wlh`!ueB&TL$xygs_vrIeX-$`gTfeSs5*(5)P(?NS^^9_Oa#xO&*_&RbM!69~Z($EpeFHjX#&lriaI-#(IMNOO4a zeJIRZZ|JUJMq`0wKW8Q9i2z1()+IprDqQ|m4|jx%zv-)Sli1Zm=96XJZL05JncdYt zh3|I6-2O9jaKaA^$3Jl-L~L0!oH<{r&|#S9{*yp5)Hs|#$UxseW-@MUF1w^s>M1n= zBvVilF3R_g+*rX0Z>`LL=-VddU%0WtOEgRWX9s)Oz~Mz?Ot(HlaA1nlOUGl2Ct4-4 z7ZoUaP@-z|_2C=Qh*Gvy M)=*4Jol1A72VBanv;Zo#cgdxdhAknBm0>OB}*{<8D>)O(8Or~ZC zxEK>7*_(L$voQU^<&VsODWZhM%!H6AFpWWDq6jetBO(bTCM5d2=UcahU;Z)8*M8@m z=e*DJUVCB4dGM%nEWVnftLE|cb3T4K?dE%{HkQUzf?Ly};T~uCUX@XOD#ssBg!sAN zz54tCa-Ynt{>4KLdgoT!Q1OK7Q8Cq}I#mAT)2*+D^y8<1nB4KjUXCtzpS*Q>&j!AJ z*sljFsYc)Mkc*olO}fTQRlIe5keeG_!$W1v2@+%Em2hibXz%?2JTcjyIP@|E_g z48JiHmXjLZU(qyuG8ZSxQaLcEp`b@~=*u2zG>%x~!TWLTY8OyxQ3_`QdJ-P+q4i6q zS#+hQkWEuEe}=|m)AWFBk4+KQFM7#Qc)yaIRCLW<828a>2e0|m&&U2+!{bku>6;mH z>dCJmJNH(UvmPgpJ?d@DLvIRCyXuAd7#`WliE(^Kl0i3lX|-m9q} zIo|Ou`O5P^#boDM3TXErEvn2KckCMpmE_hyn61Ug+A@cJ^~Fgr{d$Fblq>vwg^s#-?P;$by+(`lx7X;lu3ATaer=?R?~VAl<)VYV0p|mG z*zVKMZ$TL!T8SVula~)ge7bWpRq2=Rl7r8*_#c)u3Nizi+NC><@C0^d(k-q_cHrVv zLRZ|W4m7J55RQea1!X30cr=xk1P1dkmNGRc(c5LBm;_0FDxx-_pd_A>P7#pBN-#+k zH#IhGRJCHA0-_I?6i}Eb=7!XtWJv7haVEu+P#s=;2{>)krual{c)kU*LLwJtI^Cp4Hbi$09OyG?w8aiU zZbB{Nwlk4!7AWlG#jz{FkhZZ$Qm>7}P7*BxQ;^(;U%4UUsW>Fe5H%Sy2t632&>Z2G z=q}fX^Hg2hp`!ff?XYf&laD99v8%U!ld&V5{os&769#(_%5IWJzF)}uj|B%sc^7!a zMbTeWlfzplZl#Rs94bSIXKG!Lowy?-Hji4d)E3xipJm}eBhy03#<1TFgp{-lh%`_p zgent-ETJk^s2yj?lGMOdkKwy{!}FRh4Je&8rj(cQW;AY%t+xB$CnH$5g=`oH4?-(5hIvlyGlO0 z46axf;+Z&Mnv74}3hSq+Wbr3Il-wl#{Fd9S(D_A^ZK?s_4R{*VW*C=oXW{#Q6s4m2 JY)Q#G{{md|C|m#l delta 400 zcmZo!&T*=ZV?&bhW)+i9yqnXUN<=3IgmZ0v7JjI5ddx9KuIU$S8ClrV7)lv(7;+{* z6jGm7%BV0oL7HQF+y_Pu zo2-#2vi*h>qt~SE6>Ax-q}Y?d?sS`eFqKhw`laKH0n9*==@U;d3U9YL!B`-&{ni7< z1zgh~y=D}fKJPW7z~p)QeA5$J8F?lj=;WAw=r!ZL?Q&ljXGvS80{xT%bzCOU;~79- tB?56V&~J%gpD6%+SOgU<0D1rv4*3iz)7J|yIc^UVV0y_R4bHo4u diff --git a/iOSClient/Supporting Files/sv.lproj/Localizable.strings b/iOSClient/Supporting Files/sv.lproj/Localizable.strings index 3c23ad51d7a5282e0407214e696d0ad41e8e2c9c..045616a42ce5c71243aef3f05bac8a643967e1a5 100644 GIT binary patch delta 2304 zcmbtVZERCz6n;;))yrKeUAMN|I@Y@{)JY~AiHZ;y8WI9XAVK{BGCI1ALf39>H$s9L zU%?QFZXD&rL>K&{#-I#GEI}e&VoZcY7e4%)E|DF^WKdOewmn?+xy=4 zJ@=gFJm)#5XO9{CGR8n;Ar3c`;fAaiBMqz2(@>7{$;^wYxC-Ofw?lOE5O@aD3(fqT z78M)pS=!^E-aU|u>EG*c_%#<^|G_)>cjK!*ST=b0g^lZI-E}al+ErL}@YTX@Qp!>h zUZr_2rnd7=RCTLy>RSQ9!P@t}t)lV`s`SZhKH&#Qf~p6dD;T?t~%pB zwhPM9^MoN$O+5al6OD7OygJ1fb^Z#$OhXVxUZ=As!GpKwdhxik_&$KSR9yj{jCTP1 zK`vb!`;0ws^k+QwChah00XR$em@)kgJRsL}NV3&Um* zpRxSdY!%_~D<*b)@5J3!F&4F!)22_r^riU2Bs-9jH?f1AeD9!6KLj6Jsm9rNjM-1A z9cqlt+O?U*I$)!F>!^xQr~m@Appymh*0W&Xg+*>WQt2Xh5)7JH1Qs=Z048~EKyFQo zYUPr)u)9{i$5k6U6QNleM@n_^Gez&b43FZz%3^%?Ne_Or#E-pM1GkjsJ~a6e4ee&V zynh9})O;CApBMZVgG%tTl|e^&Ey9W3?^lBsX~#K^q#@N4*+~4fCNswArWqIS;@--@ zoDPj^tM(jGQS38Ightx-C%Af&>_k$LUP5)4hkY~y{4`%H+x z=a~1S+ERRojVN;3T5LNRw8XIKt5l0K`6KY7i5Jee>6e=@j}Cqaep+%1CTKwgK`qJ;IEX+TvnQ4+C2Q z@^05ErJh!s*s?}7va)3*_ZoFq6CjBtxMf9{LV~1`Vn*ec=M$gWI|)9OICpuxNX6Pz z+I`R2a^8wz!IcnR>o-<}nPD3_jIg2PC-e)l85z>hmk`L8$W0tO%I)6w7tgQFm(0+Vb_Ag4Pl_f3 zGr`6z4BT7qtFXA3Q>`?UQ)wbeqFej-P5E!;C>{KXs3;+k3ZW9$UkW@BtJOB(Xi4_N zUjADwJpWuUr;BIFORqx+*HoG~JjXR>K7*HH$}^(6sD3pRR)kq@f-u1mqRfj3UFvTe zMR_211`3x;eCb_V8S+fCt=QQ0Wp})`hm)lXsE0ILNht9$y5E<>_B6B$e1%fk4lS0r z%zVD;@ceHjv!!?J#*V8N8g)8KM7UL->jsqG?_yKwW$F1iuLUievj0=x`B>Ld8#%S{ zNKP_&%GaHX6bXxs(b_qRRXLfPGWNBGy?PS;3-fij0ED~Ka3a5tEhM_+~ zkBh)0aXV1Aro@JBkD+k}-=^Kkdy>|#gp!gv#qF+4{%vAjXHv282s|uQ;J`^2UEj+C zv0*ioqUS{;Xpg`9%o)&_NHbG00f92go`-phrAKAZ3mYl@Y}0V%Z}%?qw0)8}+e&y9 zCSt-kb}kBL`VB|91LwD!#H-p_Dcwd}xk~>$0zTSoI-EMrvi}iB>PRO|$KihgsEAVH delta 447 zcmZqL&arD6$A%Q+$qyn~HtU#(aWf|}m zde(VHzv&L=8AY~VI?q@jvc2I2;}fpwMV}efCLf6Bo4g>9Yx=$xMxn`jvN@*j`OJ84 z`-I<&0y6%^K-ZUo{Q>f8E-<`OfOINQqy);(0f$*014v~e*q5n5Rt_{|A>JzhdkW~q U$%YRlx64T|$uLc4k!Gp`0P(ARS^xk5 diff --git a/iOSClient/Supporting Files/sw.lproj/Localizable.strings b/iOSClient/Supporting Files/sw.lproj/Localizable.strings index 77062d3115ec80761ef261ffaadb219d7bc48069..9977ce2b14c4bc672eee8a8030e157d35b624905 100644 GIT binary patch delta 2061 zcmbVNZETZO6uxI4UDuX(FYUT+>k9A62%vRjKoADbjvz*Ye1#Z82&1&!*3pmIw=hSP zuRtIuC>+UA0}2TeGz8K8sPQW!BoYzHY6$Q{$sGPrOqM8y;17f6-nQHL;UBZy_q~13 zInO!gInTX&cdzyOY3l`jF8{tm8oKZRs`0MhhQoWy=voh0aF@eID>QceAplKu@+{O; zWVj4NL=fz5Tp8B#5PR&8Sku%Pgb8NZ)8aa(0-#R~&a(Hej!VOZ0@@ZmMu#=fC*`rj zoS|3rQ@aM!=)KE@#P+-~!NzUa9`PBj5lN zt0vds_5A_ton*z~Z?$PA6BOqa78!0O#7llji*4%JEclP!zX#szH#Jall;USW$5EdX zPg-hKoj7k)&t5$X^Xu)M9F}6%83tRdZfdv$t1Aj0v#Cq4ySm&jIz)($iX zB3F4l^S%QeLfH7C3;l^|T7DRGkICZ*@sb6kIGMrwZV{z_UWHjzy{Z!_jiiW(7={|_ zaA*d1-4E6D@(19oNN`|OWH9%+&n0JD@MMsf+|!zQ;Gr8gId3}q+f6uOqu2XEqtOvo z{_zp6X~`ajx;6s0srn8ds$Y-S(BeDX(CT$o7E#AL9(rXZE1|7Lqva1kJL=Uilc>cv zTP@fbbe79englDs<$ILCJjt>%HAwoZ2RyD;v4K08E`qE!x#LASC~?g-x!AapVxfQL zA;#pJeE20ZoP?j!OE#!}WA23O*)bT=&hc@!Y~@oFXRi+l_tD}ZEB&$p^zs;EnDkO9`0(7&0=jYcdZ*7Xk4}6jw-D#zy!$o z$antkEl^1N55ZIj;PpbsEbf* zluN3fp!PI)N~59~8~>=orT6(5f5F;garvH6YLM7{%#=J6@*hF|>Pq5Iw`T2}SYnt- zm%b@xp-b&!VmoITOUk=Ww#HSZX`Zu;9@nV!?BqPM2a_jmh*gXio`5i;prg%<9u`}K zv4r8093bT#oxoR`gNriD-(iwLdF!eZsU#*_V!7hOGgB)~=2@m{rr+m-m;C#nNu91; z$ueBM)IvA5gOfJoz)A-Xu@pAW2CbBFvE``Sl2Ywt&)k87-pYhhI5R{uJ_FxEnNv0+ zvrF34wZd;Xv@TV3Qa!u)o21R)@A7Cg&j_{RMIa4fh(i%WL916<~^$6rJDVFxA^QW|1NG?<3_C zM{P>iw#NpJ1%CalUYbXh0u>yjs1wapwV>GuInatd*2k@;m+z@Lj@|i%EX1_&7dOccVbbu~gf`N%Lpb^a1A20I diff --git a/iOSClient/Supporting Files/ta.lproj/Localizable.strings b/iOSClient/Supporting Files/ta.lproj/Localizable.strings index 4407f53eff2552f8514fe01271f61d71c82f1fed..3f1a2dc8690871319fa11232f94e0fbad87094da 100644 GIT binary patch delta 2138 zcmb_dZERCz6n;-PyRN3`k5wf^qDl8)f}k+d)7S zrf~^IWpIdxXf$XH4viq$NOduh=_1C&u=y1s0b(TnP(#MlK^CH(^LDixzfE&*?|t9% z@to&8=e@rV*iOA``|zdJ_+?WSe)p+|tOXVq4z6m!=2bSFiBuHrQ*qU&k{J4}et2;8 z9VgUL(>ACj^Drzbj;c1*twJg_+`pmQM~6nh7@mIovwc+cJh+BE-=rVLjovCsxWGkr z7uc~gxR}Kp9>*X{WnRsKiY#)KQ`Y%5nNx4_+A1s}(rtSgcoz(+tE^ zNfn~W3h;_cg&@XVKX|l3eb%5u?P5?d4a-zr{Je)9lPavz?0U-y_2_BNhIHmU;wqu` zW%u4O7VJgYcMtpZk3dc4qY|)z6qLENMDk?Fs$?(yg&zZ$N6t|2y!O0z7Y8(KAsv=xs z!x|=JV!4Zs56Ig=W~KciRMB&yqNjSmaCB&BU0iDzw?0-+6KSwH1z9hvmj$ZQ)OHc7 zY3OV4l$xqjubNzwh^h@sw|jB=Y?;49w5}_P=~l5H!%9=>4wf9}Z750T-E6u}PWICm z0pUm|B;#oo`r5a_o3DGJH6J+ktTkI z1}eM((>Qa^gP*)nj?Hhnapxr~{yk>W=GvNWW7w)&t&y`Jfv(K!lC@cV*j$^Ah0Xi$S^;X8o)@ z0@fwlv~P?(1^IH0C5@Kxt!Bkr~M#sDe zOEf%_g(ThSVGB9r_#EMTC`Zvsh9s$Ss?743=UZGveNuu}H}-Gx;?(Q*;uJG12^+EY zc91p=z+yd~ht9e1MAU^>&bn#$O0An19=+|g$rnddpP9M=$AbKYm0@5uDEcu}KPV8T z5XmeQs}9jK7K{IXN=8>L HmLvZHKTjm& delta 394 zcmZ3soa0R!$A%u`&3-0-csI{-N)Vk~5YDxkE#gq+WFBcg_B4i4h8%{R>4hDPrjsXB z3QRwDjFD&hgb$2tqKOPe49N@`44DjN45BG zDg?TSL2r6v6QlU%D;LXbr`xS%WSiXdS7Lh3E=IoX8bXYGj0mxw^+2%%Va7FV(>?f@ zM7IBsVvLxyec4(@7b*56hDx9d+$L{y5uVO81Y@Jfc7cbCv$&=Uy^bM3 znQvykxt#Mm^LHK2Kk>&BwSIABVW$x^HW>*cX+(^T9Hg9Lb#RHp{}PV+49f`N9yjJ1 z8}Zz&Rl_{7h^ljgMu?lvk+;@@ECJ~jZIQ&&Ly$(;Nb#W=67 z0jx`eLy5-YCA4BFuy%1juX&4HL!Vw8T&`MsJZj*clYK?x=3y5(`9y;1cyNduJl{b*euYTjv+2R2ZZ8mP@@^Br>6rT`no)uF2q z1k-Yo)b5f-z5KZ4Px5EpE~V>x*maalRbS;%2OZ^_vYa_q=GCLr=qu=iRRSfjjG&qo z^YWM9QghzqA)kDciZfN;QM^Q@txev|sCz{FZG&g&fuzx;PUcq&o%z13(0U+*dK|S? z+bkOEr*_lAC zlvmt93n&r&oSH-wAHe3@X)5Eo`($R`|CRPR)#zzA?;QhL;}}Zb`c(-h$LOkh;ekDzy@O-B7WRxL<`E3T+O;=1X5U$`lhsn%*G@?xJPiBnk3qG zFItknR63<5KJ#wRBCZ5t6z6^DnkaOzfnY0of-b|*hpQLQ!d@7c=*BgRVwa6vYLPJ7 zb$l`c62qerh9)zaZ7fr(R(pAH8CV*PR`JM6o31DSlP+M&pxUT$L>R*fW`bMJQE|aE zx>T?*@R*c3$-z4)P5%fv1NQhx2jUzsGVk_+*1Pn3s>rj9IhKVf`S=(*Vf-gl{rQV- z)m~Ux6hdZgV4)i(vaB{-DaO)`jZj%l7I#XB0EIl6>WFL+KJ>5H?7obt8SJJyHN4c} ze^DF8?W~Li0Fvss69F?*?H z*Fq>zIZxS0`klbU~c=Kto1A_KbuvNn^e62vN4fv*i?%>6=3G#H6U2 zPpX|Me3r!(C>r_8$DgNBKd_>%A9wS*epv&@OrC!PUsv-qIjRKGUSU&2;$1#zGEkZ4 zZqSGRnK8s68_(izG8AYK_mYy#{&9llW!6p5 eIH{KdF8#`_`^o&^W{&xYtEc{_D_%}H@BIT-s712? delta 445 zcmZXQy-Px26vof76uaO@r9^3RjmRY|4T6T~gJ`m`mZn>3iZ`|F)5$)N#_A4dYif{a zh=lGY1>z751rgCu&LS551)Y158k*j7eus13=Xvty=7$6GRXs@M5Za5Sk*uY7G*xgE zgA2o1MYafv-EA^%f($aOC9JU|=A`FNk%z*^ETyOB3 z(5b|o3@J#$iq7M(%G}TO3~cg+`0qr!lwqD`n&Qh5?lLgQBta9`*hQ0{=y8e={w&@%2S`Wxbv7FH%9s1I-wPPbbNy*DqFB$srCwFZ^!E2)TLTG@XbW6 zZ3c(>+>O-}Rk*~_9@~=cX_ocLnkt&F;9?6yB~a>A?<<&esL>BR6R1AT;kaw)R&qmD zK>C&4d2zmH=er2B5XI@)EVcPVr)-~D8sfT-L$BQZPGMOeDfQKA LXdoT78}@$yPG5J@ diff --git a/iOSClient/Supporting Files/tk.lproj/Localizable.strings b/iOSClient/Supporting Files/tk.lproj/Localizable.strings index 5a78947a75b468ba89effb4c2a61f3240cf8dc60..8eb8dc66b58e0139f2bb332ab4f0cf155dace7b2 100644 GIT binary patch delta 2115 zcmbtVZETZO6uzg7uIsw4tRJlF>fUWIkRT35khnowGbL!`!x$k3GD^E{3v0Wzg9$+y zM%grRZXCif7{1~U7a_XwRRd(%h&aLzWyTmk5+O#6Mni&v8bgTZ+_J8J{AHTk_vSt4 zp68tBJm#MM#9Pfqgw8;)uZ1l~cd&6L%O((#PuhqM7Wwn(&Ht^v6uuXhy*!O}B`}cY9#w;@p z*ugQQOC@k|uQwY7k~Fdss z27UBS&T)F~D$L2AEy_T|7-)xT#@k^pU9*EZZFPVJq{MXXEVyW<(iAx8tEEs&J)KZO z2W;TL9*>3gpMz4Y9W2Eg4X)}G6V}cvthzYu5HER?l*s(QNPg6L2YlyBVK?H*1`95G zs0@#pOLeNI8Y7)N0gYvaQT9%73pfTIPq-<18C>+^WoXEMU`bbh3VU7HZ|rb)t5(&f zQJWaIl~I$bg@VN}*S8^SGNw)J99J#aYgFPX!-cgSR&or18OI9q+z~c*@k+2uh+`VO z>%huBH~#ai)t%xXNv@=stxat2=CzIX4ucN|?piBz%~HHJv$MfFog#MbadU@{_JW`K z`x(;liy4#t`55@nLXJKtqoE!q`PvmI$4h5i6n_XUgY^4 zN@f`x;oD(XlkF=_0sw@X5ecS_{;tpM5 z29(*rti#^OyqV3tAojd)49(%lqQCc(KX8%G5hMHI7Lb?`I6E)ji5^eEvZ zREllV3&|#;eWfp~UYsoR`ZJI`?rfb-D3ZyQ#xH>vm&R)_aL9sp0*+}a_~7&qWR8n` zV;L%NWV5p{r*K?bU!_=_4aB z`{0+{v!5qz^ve5SrO|q@dNYJKaY0*oEIOFIgpScf;}hV-DxXIH3bF=VKk20BC&2UH zf$`}z4%kkIx3QG00}M1g02Vw}Z1*l?)}(*(%v|Hr3{IRL9e@g0h!@WL@KT-2FBHpF zBw~+f_ckqfeTdd~gRfvJLXvJ*gR?+F3ImVe;@MtUM1woQTPRbK8NN6Tj>@FF%=u61 z3oVY|b@GQLj(5L#cu#IZVNOX@B0RR}eVf|Fk+Xoj%avtH=Qk~3@r;Tr_)Lo1Z>Y5# zFPsx$p3c-@#zXvmn=+nYus9!(W;Exp!ymb<=B6e?j!fNhBI|Aq)sA=Httdz_s{*wM zV}JSRYA=5&xMCW+0(mri1I*MI=i7hdTRf;Mhqy8J3NX*$z*vE9b3zL{z2zt9_Z7sn z-uXK~-<=2lV*)DGNW0|YFg0ac`E83%JFfp**e1Lg{GJDe#!aSRy47U51@xK8w3NDz bg2TIl%~fg*hmzaK;D4E||Bbae&-C`c$cZX; delta 388 zcmbQVisMEn$A${y>1#R|1vYz`u<&i3=Tsm%d0zf` z3}&>PWY07GfgK~);)2EDh3D3}h7mMe~8iqyl-x3_1+c3qCSh@G3B*Akv=8qRw+p?wsH5X0aD8#6~y-J9Yj}ajtJ$c?_p6#228P~CGw~%I> zFnPP&dPXZL_9U?T+$I|u2~V#$$r!*46xe?LBx9q<_PR%mbGWA4xH580?|I9}Gu`ej zqrl{KU0jpId8X^}0tN0>GO|t2c*}TW`@XM?9n$8hK<}hLos|jna|Y0Xi9lQo^j0F+ dQwkvWK}8FIP6q`+{`Q4}OomL;Cx|dL0RXOKYM%fA diff --git a/iOSClient/Supporting Files/tr.lproj/Localizable.strings b/iOSClient/Supporting Files/tr.lproj/Localizable.strings index 343fe99a94cd985104171c25294e96695b255e91..cf618c24af4171d1f19b277dbd7f83b07c1283cc 100644 GIT binary patch delta 2387 zcmcIlUu=_A6u)PdQo4`sTlbHx?e=wJKol1yNaT+TE`*q9bU`3!WKFkDSJ!o0+l_P& zWAMTY0ptqD2eS+%F&ZM6nKT$RGXfe!G84csjb^-%5Q8%TLUfA1b2~5)qQ02sem(cz zbIQV_lTuEWxQ$=3hG)xP5sLJJ~mnA=~hvdR*l5xs;I%J=?zJgxgITC$SoUSay(5g?cDO{R?D^;DWM)6v`hBHt+G5Q~z->yuUR=qpU9?o& zKYI=|*>!P%cECixN@BEQ$gD$Mkx*2F{CMgX1#%;Gbp0SdaROe?gsE1}^Js=#Jb)!V z)N17poS>n=BP~iCWnrdU^{4@u>45wS-1K0@!Jf?G(CXl?&r=O&F3_U-e>=5VZB)=jZI0a=p zWCvfFCCqLTat3Qs{X(AXBvlMfD)oj{w%)oqxMLFtkllN&|C1f@(5-qU5qELK3@!)r z9@vspiB=Lm6QH`?px6#618<_0_lc@m3CH3(X;dVzD24#)!QsDE?+a9TD(W>S^NwOF zuBSRHB{z3Nl5$L;lH}`vkRC{BNOx$FnwJp7R}#|wKJT*+&A8DN8xn#by%(WLk7qVN zsa*Ohz3Ao1z2xJw*J;tA8#Jw59=~pRdl8@eklft#E%_;{4<~EbnJR37<{+zR zJ$-+Kik^f*ty+)yAVyNZHyJ`}mq?u1W>b8UN_g}Ka(mkWBWZx}^f(%HB4Df)#eg}2 z!tKO46e{eNpdUq@5KkA9YhFr4C@zf$Z$ZU>HXeM`=Eo*z0hczBUyq+~X`k(mE(I(} zmi!Ad84xIaQQ*r;59YEb_dpWYW>{+^*+usUc+u|0H$tMWpNJ4|2j_xk26MC}H zm*m~CoR=m2rZD)YT~uF~#j#%FqCrp1RO;Aen^WI_8WbA8IRN1MFWUiQ7|z5^{^^p7 z+y>K$gU6_1{+$5iL5=Y*D=2{H;HlLmc*OFZGC~>Nca}ohAMx|~3{}-jU#Ow{8!>1K zOQM;5je$|PN%Eyn(I}gb)&@FE3NrAY4`si>}d?83^@!r zlRpZYPc{(b<8)*wVu)wVWGI`S$jBsXtEQm9-~?2nz>oKfV?7xM20FL zy9!9>0&yOYod`545iFYnQZZT3LU{7BI1xz$MugsChCe{PsX&~~kUD+;5k_gp6rd)M z=4^%%AX&s<$Oy6ormz@jR31YzP>TW(mjG3P_zFN)DH!K5=uH>=z$m`?(ak1@>2`^X zY}1#>FzQU_JITnleUB_74?W&+&~vLc_M5@>uPL&zGS+hGLz@_GfGT97^l~%GA#fA;Z%Rw diff --git a/iOSClient/Supporting Files/ug.lproj/Localizable.strings b/iOSClient/Supporting Files/ug.lproj/Localizable.strings index 4447f2a9ed96e54b97c5b4565120fa8f8a4e33c9..978b53983dfb405ff15e51130db7592e39492617 100644 GIT binary patch delta 2247 zcmcIlYiv|i5I(ci_Ok5ba(A~&cUyW}UR7&(H3m^)n@EU{*loZP4W^XcZM(MJ+w5(N z^hc`*M2r&Ygr1NTBBa4oQ7~XOQ6oMGAp{6DCPHGQ5q^-UJZe(^I&Q>ZWDbtxi^66nPU84!tHp^>{*K6G-+~` zAAC4IY+&!coMrcdf~Ca4$;by!LC6WbcCKK?XKD=0oHyjJKB&gACm)e@e$eMpkUTY- z4;c3p0=y1h4^=lHlG$cItN_))wvWM=9oOMxv8*2iU5@?)wX(MeeDZ(=8XngpxaI9y zG-H|^a=Cm1wrPlF6Jan$WMqOce_jD5nqIdm5-)Xm zaO!SRc5nh7k1S}XD2wPOEIg=%vbai+VfD1=NpU)o|H{xT2xn@52LCvK|92( zv|1p9<`2cVeJ1$wda;cPUnUwzU%cWcrsTj7%ut@=Zl%Or@3fyHyPadvNyn358hDsC zHXjWP7cd(MvzsjaqL$OmUoIoZd{gC3etWQfZs3 z_-BoYTrbu5*$ZL0-Q{v)OS&9muWNXrLErqJ?5K44w$oH*5ABfN=akT%^O-{(K8S?8 zj$bRQfUdvhKPzzTB@L&d8jg9jfZC;^`FLB@L`o#FDiZaYV!3IGF0v#=ue^5+s&Lz+ z52qu+Z~<@!ou6LPoW;Qe9iY2uG8$&!ve|tqk=f*fn&Mf>g+)*?|6i~(nuXXEWbNcPb>!> zC=ZN}5P8u~N*$v-Hcy1Fx%@4|H8>K;e@vS92j+7ei{&x-v<=~TNl}OA9+ctj869u0 zD^=>R+Nf|i=T6GOqT!E`bD{EX3R5?hJvz6qIOS#w_tBx&DZ$<3I>%!h1Hxb=3-`xGha0SwT@W7|{}ew6s;XG!sJQ+CLYOsD_rncR1gm zsY1BJJGW$`IZHV-1WHDIfsU6T&hoy``JKagO1J9MrCQ0%*i*A#9y_<32#8wPO^q0; z_Q&RRfyPs)_3gtk9CB@oiTa{wuBoCFa?@uA)qn{W?7;z~;RKRkz@%Zox_A=-Qr=My zM;NW}w8eo8r1*E_g!BB$G0pOD67pPOpq&oRdkjeY=c&JqV_uWn2Ap!RdqhOUVk36( zOV diff --git a/iOSClient/Supporting Files/uk.lproj/Localizable.strings b/iOSClient/Supporting Files/uk.lproj/Localizable.strings index 885842b5306c8190fda6de835b6898a9a8820000..63c46517d1307fa50b0a84b30b15b0e45974b411 100644 GIT binary patch delta 2372 zcmbVOZERCz6n@X>x~}b-rfbK#cIZhB8rdI`lP=b>Sl#RSOzYF zXRao?$)<^&bl*mMG%;g8*&;%hcdg(XS+Z=YL+;oEE;+dY7TcZ`9faG$_K}xY?JbhW zM!`OEYvX{qu)(y*Ab zojgaUrt0OLVQ}L9xJxei4XpBl8ys>@A+0X$fC_nQ7>WvvWSc3`hzMcV0S}J zvb*iry3vl+2a2%IW;PH9_POM^8;TVeB;@cr;KlJgr?Tc5kO}!{HF&Zy^HMb6>m{Wb zB)R`*a6A7^P!>D4nOKZt|LL-f%PfM&ck5>$Pny@ULEsH7yNxfC)KB%MWY z#VnuR2~PRG73ScOxmX6bf(e6XEjhZt-oavAIO>o?yT}-HTV&tc;FzluQafGE)IW{7 zqzQhLP!STMn*Xq-*_V9i2iK9j?X)8*x-sB<+@Q;S&zW?chD{|TodhM;!64X>@W_Lc z5F}#M%j6G}fNnf>(v0^5anE+qN+=w)iQ={rWsKt4c+B# z96Myi;5swj7&FVFtKh)r%S+^I_rdDX$%#82A%`Y{=9Js8;Uk|MBXeM;nd?<%+N{w^ zWre$^Jb9#ljwgSK!fbi?2$qWa_khi@;Ve1-IzaK>ftVP zpfH8$ri<6QY`98VZkz@uB$Cl-_}q%4q>}18Fke=Na19$>E?jY9KAw*`aOext z)G@c5Y$T}_e@5iLyUKxP82r#oMXMNTsf~QvJ1?Z$KO}sz| z%~>C@K&7iuUzxEsg&rebSVxf=R5Pb(g{L*f9+}gD>{N$|T-))F<7N3e^@u}NMt7d;bl@lcS77>&XuU1xt-k?G@?{Y9v6u|C7z`{HiHM;?hYrqb#dPoazH@%}yXW%G`9V%&KcE-1cJne&ROnGg zHw}5wWlBRmE;B#5-Ju)(vRR$i94$FWz!Ib(1#!rkSF!g2v!GQcNNtl`O-a}Q6;z6J zVxKO$f+uGv(^mzZz8GYAYz0;!&j0}-Az((2ochMp8f=-hb#~X-?dSU>ld@c=pzhd{ zIo30*rkRxBDhFYpXa$E`1c&+W*V~FptAJRof0H{oksf&n=+Pe8JbHN=CE-Co?K!cV zgcqBw*-i{%W0JX9x{xqH)mK&z{rKdt1_g$WE#np!gRP6O#R;Z3iE6)3y(L^|V3Ec( z^jPO5-0@n|pLj1*ri>jFE~8t&*>#a|fL@v__*RQLs!oPE#$ diff --git a/iOSClient/Supporting Files/ur_PK.lproj/Localizable.strings b/iOSClient/Supporting Files/ur_PK.lproj/Localizable.strings index 41e1423ed9bbe586d2dee439a57033d0a93ee9d7..3fdc3fe0a28a8b91a4a5c385db114bbdfadda209 100644 GIT binary patch delta 2148 zcmbtVYiyHc6n@VbUHR5_rPpU6*znju^nW zAt5@HgC1kTMI&)0K#&?sBvFuMCP>JLL@^-<_=g}_W+ME+7(D0u3SHtK6SI8nxA#5g zInQ~{r56UQ14peV^;*2UFoe-NPV_9Q$FYT0^i;cWC~hh0S4q{YQh5AMWu~fj!VWWO zegjmX-WMufuM#Sv+EuGcXY6(DUTVsKEwiIxpdZJpZJAdtM^|F=-yyo;00-@GfJTL* zP(`D|;6vAP&;Cjc$M0!a|G7rT>^T!08LXoE-@!_khQWb_S6uXo9ZC&%NYS}{5WuFe zlP(T}v#5v3=_*CA0=$zTB#)N@o;(bV=-?@7W3qkhky0_$qgv@>I|K||+_L-$1JG^) zwy;sR>dza{$oI_qF{_+eg3bC&oN`*X3FeZ%n{%990zXOG*)k`5ZNXEgU9_YaN<}f! zcY+Pl`0aZEYDz)~cl}k0yMAy~rdh){KP^m5XPUZ_la#>oXN~;f;l^P0wht`(i?*=^ zr+_!RYYp1CZkjDSJ`Bssird&c$z@>>Jh#S6SHA-fHJpbPMY$WQ+YRpQ@OkL+V7=bw z-KIJiOJX;2^bST&sdlW;%8hgbk1qB6C7XA#$$Gs4dvp&Tc}v3^b1Zn|O$#2-op?eo z#qAq4Z<;+)4BN`qjSMTKJF)JYAUUpqRx$ZnGB&Q+Sm!q*C{|qda>1t$LKq#lZ4~MR zD?a~%2Tx|KGS{abo=5gBAC^6deMmKg~uFX(2XBvVk~t za3lD4z`T-fBWu$)s$%$t7QiDLG&=bJTy*FtxQY_&*vfctChV4yOBA+bkeIwv;wkXc z9!?5U*^;YpP{XgX4mx%VX5*qg9&&643q806S84H5aAD~|AAWd{b-!exn#G*$fw+@K zGd!nzhj@VWpo2Uf9-yXs>^8O$EVyg6?P)1=+MwUU3`8^$Fv=(Sh7bkhR8lo-e)FE+ zy-aOozO&S7Zc2IMhoGRqT`TIxjgHOf!C)~ClFkWh1{5nvSZba|IOKlrF^sBX|2W_D z^7O^oD#C<>r1Yd{El6p;la`9+==Sov(2L{YvOw63$SZ+w^L(NvRg7#1;N4;khY}%l zeq_bhS3Ccs5Q5jmJQ6t}rdPX*CkcV8?BS_W!W8}bCsg9rF_+i)m)&Hf&7H%P=v)TE zQ*_lyCD*`@6Js;z)1}}E81nL%L{&3?c5^o+4Yx*$jDQ>K$7Tfbho&GhtTAK%{0kG; zK3PUrI-m-#ziJ7-Xz15vGFR2CqZCiR`zf-3SKx_vzy;NKb90!+Pl0c_ECXros9_W{ z8r=LD&^e~;P$`DFEO-+bmJG&}d-02umnNQt!l|Oze0=B&Kn?ye;ioMT?$t#<`piBY z`WY-iX@YG`ONK`V*Ia#a8pGmWWB?7%Ap>e&L--j9B{@h?5y{v#{N2o_?FQRN)s+l{tT7=teIoGwhJv|t vwjT3tfu#z=bZQOd#dO1T~%(X5uAgE~BnEvl^6O6|#_nT}XopAytJr(4fNg zHoqG-)ws(;0k&Yx<~i789_D(65107&@1%XH%)H37z?ak9m0=XArFoz#fQ#7xjZ20{BR8%k^)_YOLKPGBEymC_XnF>joFQn27i-?gzx%GoHEV)WVRlJm#SuHwNh5!Y48EiM7F4njQ2`m>j!j k*`n=TW*5yiQQ4@}p+yed_AY=AL+A4GU;zyJUM diff --git a/iOSClient/Supporting Files/uz.lproj/Localizable.strings b/iOSClient/Supporting Files/uz.lproj/Localizable.strings index 77062d3115ec80761ef261ffaadb219d7bc48069..9977ce2b14c4bc672eee8a8030e157d35b624905 100644 GIT binary patch delta 2061 zcmbVNZETZO6uxI4UDuX(FYUT+>k9A62%vRjKoADbjvz*Ye1#Z82&1&!*3pmIw=hSP zuRtIuC>+UA0}2TeGz8K8sPQW!BoYzHY6$Q{$sGPrOqM8y;17f6-nQHL;UBZy_q~13 zInO!gInTX&cdzyOY3l`jF8{tm8oKZRs`0MhhQoWy=voh0aF@eID>QceAplKu@+{O; zWVj4NL=fz5Tp8B#5PR&8Sku%Pgb8NZ)8aa(0-#R~&a(Hej!VOZ0@@ZmMu#=fC*`rj zoS|3rQ@aM!=)KE@#P+-~!NzUa9`PBj5lN zt0vds_5A_ton*z~Z?$PA6BOqa78!0O#7llji*4%JEclP!zX#szH#Jall;USW$5EdX zPg-hKoj7k)&t5$X^Xu)M9F}6%83tRdZfdv$t1Aj0v#Cq4ySm&jIz)($iX zB3F4l^S%QeLfH7C3;l^|T7DRGkICZ*@sb6kIGMrwZV{z_UWHjzy{Z!_jiiW(7={|_ zaA*d1-4E6D@(19oNN`|OWH9%+&n0JD@MMsf+|!zQ;Gr8gId3}q+f6uOqu2XEqtOvo z{_zp6X~`ajx;6s0srn8ds$Y-S(BeDX(CT$o7E#AL9(rXZE1|7Lqva1kJL=Uilc>cv zTP@fbbe79englDs<$ILCJjt>%HAwoZ2RyD;v4K08E`qE!x#LASC~?g-x!AapVxfQL zA;#pJeE20ZoP?j!OE#!}WA23O*)bT=&hc@!Y~@oFXRi+l_tD}ZEB&$p^zs;EnDkO9`0(7&0=jYcdZ*7Xk4}6jw-D#zy!$o z$antkEl^1N55ZIj;PpbsEbf* zluN3fp!PI)N~59~8~>=orT6(5f5F;garvH6YLM7{%#=J6@*hF|>Pq5Iw`T2}SYnt- zm%b@xp-b&!VmoITOUk=Ww#HSZX`Zu;9@nV!?BqPM2a_jmh*gXio`5i;prg%<9u`}K zv4r8093bT#oxoR`gNriD-(iwLdF!eZsU#*_V!7hOGgB)~=2@m{rr+m-m;C#nNu91; z$ueBM)IvA5gOfJoz)A-Xu@pAW2CbBFvE``Sl2Ywt&)k87-pYhhI5R{uJ_FxEnNv0+ zvrF34wZd;Xv@TV3Qa!u)o21R)@A7Cg&j_{RMIa4fh(i%WL916<~^$6rJDVFxA^QW|1NG?<3_C zM{P>iw#NpJ1%CalUYbXh0u>yjs1wapwV>GuInatd*2k@;m+z@Lj@|i%EX1_&7dOccVbbu~gf`N%Lpb^a1A20I diff --git a/iOSClient/Supporting Files/vi.lproj/Localizable.strings b/iOSClient/Supporting Files/vi.lproj/Localizable.strings index a67471dcdd1a735b7a6267583b753ded3973c72c..50e325ec73c33ff887670f8937a768f8450ff8d9 100644 GIT binary patch delta 2220 zcmcIlZERCz6n@V>N-0~pyKBd~cI9n5`HWMjgiGDKM00s5IyI;Et~xEk7@4hz3=Tv&b2mNAzflWJ1GRvS+ zABGe)wt^q6ZEn3gZGfG!REkyHL12t0%-8!6rXaa7^60+EuP3|i%c z**M~;q>vkmaP~eYr1A2wkFKR5fGb>;c<-`%PMQUb@)co~Ef&;LvXT-rd~c8+)^zx@ z16Aw}EkDM*&(3ENL$6n8{|g?nvTq%O=c};$DL0z^Cf)rSLeskWa6-khYc9YJPjRhk zS7GYE0_AwiTJPPgdO53NwVmzuvelI8p%Xr+5&7o5aDUsH+~xyu&UUSOOu6`;=c8sX zSZGx_aPXD`X9tZ(MaKa~Cm5feg5SvIx_GahS0ijt-r~ZeHuJR+!)N`p;uCNMr*4ZP zdYi!(KWV?Ie*(N*7a#0_YCJSnLMxX-JtYUZ6i$tUNe6cFt@R=VXo$;=*6)H6^ei*+ z=TVD{y}Wn#8&1IaghA%FV3vvIlKwc?e3TuJVE@Tk^CG$r#Lc+QOq%auzQm6NBQ1XJ z33T*hniTlqN z;gLfgy4uDB`#%LYZtw@NEm?`72?rT%;8-L@C6ys9E{RX;a3buCw7rDe$t(8@1pglM zO259eOs!>S=c*N4kn$u~NSBash$wP4&CF3XDf?Fm)dwLalVSs8e58OrGa24d*045^fF42@+|T6Xp<}`)ii@oD1DP4 zTO?`dK0T%WO^2`zydzeU~*ve!%3^XQW)u;dV@=^X&#)IG}WJ+xOv)n14 z8>#W^-U)C9+GD!(MT9tS$iF>8WH5;r(i}gx z3}v*O>|nq*UEvF(fN~;35koRV216!88AB?A0)v`@0)sC@B10vRT>_-D8B(X~zF?Hp z*8>W;0F5XDD$Qd^2TOyr6$4oXK+$}lF{waaF@p|JBzt=Pc1H2(4PO}LgcTT4(DhB1 zf5a%tuK+Y3q`e%d5#+Lk9gLEjbFOsRPTwcM$T3xgQEd7H0Y;JSw?r5@7~x{u|A{hQ zW1Ai!#3Zu)fGnfR0pli~-C*f$hIfGd7BBU-g7>7T5Hk z_l#L-~O9cB( j0qDmfsAvJu$)J$PXGod;P=v{Gdxr?qJI3iI;!I5dtzB>_ diff --git a/iOSClient/Supporting Files/zh-Hans.lproj/Localizable.strings b/iOSClient/Supporting Files/zh-Hans.lproj/Localizable.strings index 50e69d0e2491afa1f56cd609b0c7f5c3ef284ea7..7007d068f6b19b64d521d19a286b71f35daf9f42 100644 GIT binary patch delta 2515 zcmZ`*drVvB6~CWnjBSizgg6dys6$-FNCM8<=0Oq~mNX*;KH~?ZE(V)xE;iyB0#1R{ zhSp7B=9w?$qAaC!N;g!h%B__;Yp1rWR#a)>u`KN}EoEq%t1gQuB@t9hH}5-koc51Y z$ilkk@jK^t&ix%%=xu>9g^XWu>e-pCZuK#tQl7- zXK|wA6@#-}itl#Rj0SG~s0ES9EEPFCl0vR#rKa0-uQW&;ha9D&BFh93 z>AbF8+}sOf;`eERh97IBD1#Jyb3pjN^10H$&mXyyApeUSd;lZfe!e2m|F~E{YdJfV zM*IbNP}Qx1aYX-<9D4@ocBsg_SQMB)G$9cVk4_DeHl6@ z{+GEZ8v=!i=3R#SVg7^&Sj2F*V=`)Jx$=e3!Za~_%i~BHXq|aFjy&3*Oio|gMYIE= z$Wy*efwq&0Gh*eMPcr;3mciNg#-wI)B7gi+J4y>IUe(Crzchxh!ZSj87B>X4|K=8H zY`U99i9o;-Yg{Gw0S z_;H`Je3dI<~X$i1zb~M^Q3=p%BSba+54<(}6YX7{u+se;q3B?W_2xSD@va zoI?S1Lbf-q+}LUGI4H9D`2| zcyw9qq@_*CXJEAb>~5hN9>hXq;l=ViIui(AshsNV>BwpizK+ow0>LcitMsTeVHT^I zGRD9ZGwNVak3NYP|Au3&de43*vCS&@%Vu;6=i$@1>6y=TkS3vXsLEcyrz%uw(aFM$ zHP-}B<)&rhZiAo8t^LT_)8};ww3(*3Zdn*?Lh~R7`#hy?m6L^uivoe;T>VZ) z%ZLmwpZmaB)X3m1_1^{TH$?$sO+@**7@;}F4|MzO@?1avYj<>kLXL!!Ax`{fE6hls z4GOI2Lr^Gw+J=5h{=O-H+jliIgW1POsmxfJlZ=xta0A29?{@kf7TQ66xc(usd_^jJ zLjPDhxv@!2uFa;CgF8gx!=n=h5Mh3~0Hv-!K|@+;C`TE$($JIiQ_oiypiK&(21;4w zKGPw)&Qeyjr|Jz%uv{^kg2d$MjcSmiWH+0LwT3m{PaC|h!vn)jynG8%N}6fDT3S>c z@g!{qzs#NObl4a?ye^})#PD`2vc0A6>7<)TC)Zz*%t8)GpVSg5$<9EM8k*uO$wnw0 zq#-R4XLgBA;2PKMb(I+A?bVJ|T!a}MJXMWU@nlcGoF8+dOn$rsiTJBd)Jdk#q>zP9 z1u5=IRz}4c`ho@8L3cbVXvBX@Nd`B_$(?c~+ybH=7uvvWuX&aGqw%43(4(|44EAB> zAS2MmM!e=bVEMQ8AAJFR$hY+37~Qs*zU93_Kk<-c;Ft$YiMglf1iORh3&tO+%<=7V9uw(n~M zy^jVa)i5j_ZwuXyW~Pm4h7{{~mPIKVNCB_PQx$gS_obm%lP^w28Z!8`sJPbmB-K9P z0G2r05qzGv_Sz3QWZ~#RpX!CP7JFM)D(BoFSa6J|E6_9WHiM$~pZ^_IchYi6LD)A_ zh~Z@gkj|YjdLs8;@44Y#7<$zX5Hon!aS+-zPSgfzqC=5|W5-pXW$@<|0MbhF`)-IS z!X;D!W@;>y`kOtI2APSg7ijOjGo<>>b^PhAv>ytoO2pB7@NUQZZrI6$h?0kV5F9*G zhsM^YC7PBy80({E1^Xv=z6I1a&bZCg*-4dfzD&WNa#Jy9Jt#9>r3PL$tJO?yEogsF z2U`!S>vchjZ=JvKp#7ODbxvfJ9^?)^*0E(4`V-(j!iU``CsGQ4FLH(4v5L&Dv~Udb szT;0$&_P$z_ZkEtgI>1i%71?o^d24QL5I_$lY!qI0Iy)9M8|0RKSk%b82|tP delta 3063 zcmb7GYiv|S6rQQIrL>k>V4+yZvXqLXEEOY2AwXN=6SnOxZ%S#m`)b(kE_=7FG^Pu- ziBxopTwshrQcY}{_^0M3tT7}ifB2*LNQg1fq$FwxF_4G>Uj)xL)7frGYhpHc@4a)+ zoH^(FzH?^29?bi;KX2^9yqtAj>*IN$d}l55(XWFg=!&ukYhz(%XWjJXXFc>3WwFdR zgIQJOfx>5MSv&ortc&auWYfV6`tPB8jAr@hIl$udy_22{8i|uX{97ZB$GEt$jF(s^ zpQxB^{qxk#!s5FpzVB;o80fBibDh_gwEoyUD~Ey;!w2{h(_Xh0f$ph0&*^TK_#D=v zt(OY&9&z3Bd_HK6Z$Gz4j}M1>m*ctb)$U+sHX zTBNy4NpPv zJ5S@;weErLFW(yTA8z{4RUd3TYpuCnoogR$#rr@v?BCd-X5}>__T0Uj0yRfOV=-T7 zz4L3;x_~DOeLFGWCq815XP*p4v6f&V3QmU@m)yk(AUkU$^!)S%EZXTg%7S$7kjb>s zbDMllk1yk{@^s%8elxFhwDorD-<`wm+sDR?f*@E;_dUmdc_>*g*A38&Ah8msxmt5} ziV1qbH>_tTb*X5e$S^xXBVIY~BdZ=c79|UvLkG>YGY^df=!%mUtx3cUg;EAf>4pg~ zfhaKSmCN=_h*ZpaiElJNJDzJEO%^3<$udFqaXD}5vH$=;062vEVE9KU%q|+&X~5nf zYZMpC3mV8S63~>OpYm%E^bT|Aw=z+^jeoLr2ThBSIj}$`n5c0|(5Rj2EGmT+kVr*k z$lw~)+(~yQ6E$o2N>RO?FG=p8m{q}c&_|Gn4AcmmZ=+|FE65L#V=PfcZ|(F3QiAkd zWv{Y8*{iG|4mfotqF zv5eqAuF^4fB9twprKnV5n@Tn;KqVCAucHYtMQx&FG&a-cskIMmGPdSNz4Mb!)=Xo# zrrRXZmJobQa;saU3JbhpRiK&iNpFDVL7ZnyDoGQRaNq)zXJqY$Q$-0)x><6%0@hDhr=laZ5lSJ4?sw& zt>K*NGnbAm5U=;srj~k@S1xE~4z`?aAy6G;ry3T`5RdlrxdkW{x!~la(tf_N493nG zr!CIjXZ`GDNh3ro&&k@=o?Vm40DT;Y)uBl3PXG`w0zgzg;8Qy(geltr`+fJE z-#zDde&^iYx#jr&P~p_M26i-r+&yZy8dZZTp`!fLFMi(C;HY{7&m+oG5q!thGL^!* z&)9V8Xae+g+ab3G%Ml3cfp7tIvN#PJOSPihZ_y1a>ie#Gcz` zYZt(s=g!tVVkoe201spg3p45Qa9LH(*q@C;h>ELT7=s8d`dA_;=YrLBIosl_?VuV0 zZF0JPYn&dn7yzZC_Oa#s^{j)>|9V$ANME>qWuZRRLSB955H)h!&0-y&M1IHjQ#s$# zUfsNks`=tJr~a*jitCcVkX*ya)r^q*C5^-{Q?1T)OR5dQ5+Mn0$*Z zb3ihq=&-)Ohn7$e4}Vr7g5*?_kC!j^fp?dldYPO>63#GsMzo&N5B31Ld4ha9q=)3K z2vT}Ps1IWmM~}pitX+^XVH>e!WNU0j5P_{cDH%}Vo6^G-RI*rJ&xZ!leo-GZ0ocG1 zH1e@cL7`1gjiZ;&@1#(Xh*z|3$>9bjIArkWov;}-ktOxfgX9+;yp))kIZS(9`tuel z*3FZ&R7WQ14&OZP;Yn?v; zj@W|d7%cMAHy$E;!4}*k{6)4Bs3bRnwIs7%9L>zaw-jbD123H>K|LIE#Li7ql7iav z6}k9Gdq@WAJf&;@8>JGPJ>3kYJXc-f&LdD?+Dd-@(BoVPU8w}movGp<0^Z6j|K=?q zM9Ah6fgY!$m4%jCYFVmZe{zHZ$4^)5%UjWBfLR4ap}3tVADa?6h+zh{yrmt22|WeW782i43(ktTK!@ zAuRbtkX3!Q#^N$RZ?G?EpnLfCfhDdasFI_a6rX%hq5E2?&diGD*T_{UHzsmA&b5%mHa7_)Gg3Yv@pr+JWYXz;Me~zcudSc(scA+!3tgM delta 387 zcmX@s$^PjD+lF0QlcTN)Z0^wx;ofXu&L%WDK$UB=ljoD9=^5&beC%lqr3^U?IgW!taCGoF#;PXfEr4d{p*kk!+-S29L30|loyRxt{1*QsKR5ZS(I z5@Qk9bfdY9YLoQ>_@+;=V-%U5Gnet+_7h7PjifD8fqqGWx+xRr;S8Yb5`nlF=%+-m lj}(ADD}ss^0IdP}Kc69G`p1Kej@xG(WPHas-Q@^l699v@aRdMW diff --git a/iOSClient/Supporting Files/zh_HK.lproj/Localizable.strings b/iOSClient/Supporting Files/zh_HK.lproj/Localizable.strings index 758fd2319d0bacb7c66ae9e5d53970fa8f316aba..2bfa36577e743cb086e6282f32d54a3eb63a6640 100644 GIT binary patch delta 1369 zcmaJ>eN0nV6u-C1E3~gztCsRHEd@!%fd!0VGh?pm{9=z%3QVk&mcABf2UwtDS+`+| zR)pDc91k}n%(#TaWHxqz#1O-lY|0eJ5=@NC7Ur^L(PfG=hDc_2Z<*ttWqG-|ALssl z=bYa;_x-jIcls0)&ffrcv}qvfQ^4)XM7XSFm147}`*g$Zx-dWV=AWG-H39Dy{@X6N z_ElO$zoAMZ*wU~9xMwdy+BHdJu%NI3?p;5GZpZF^n!+a@?VPYN@j@&Ym zLls=P!9*sesu;-XRlw-z=Dv9brdKIJ)gu?eDOe4m;&?@S=&JqY5}C8ELXC1`pYWp- zr$lmRZn~C{B%yRR2Zl2w!o!_-xunPP{MOi`SMm5t(U3(5mtq5%$3#Z#Wnn(mwB|6X zBigtTk1VK`tgXm{obX9PEbzFUr3?Lfm`hCu&GcH(1Kz7Wp+;(!QZXBmgG5;mO|NBS<+Y9T;-l0Y@Os(Sq}y~ zeQfl4x89w8=$5WDW{K-_za{&tu8+-K{a zaQisKjd`kZz8JE5S>YUywZd&42jO$E0w&s(@Z^jfo((c9X*LorV#wkCQCj$7BTg0! zNjMR9^r~QbI|JV&Nlbdj+un6;o8WwRO4Lj;S&z0Mk&dqDxZbTQy>I#XgwJhvm6R6` zELpbM5}cG@Y`)+y8`$V$ug*ws(7OUxf>R(@DuJ67X)s(94{|A^pb@EK{A#3kJT2V> zFnnJrt0!)pq##5BG>|^DCd+wfmuE4y|MMg?AH*qw zO@T9E;$vp7M2g(UF>Lr>8}*1ZyQOx(j#rl^^XnDbm5iQ56I($%WS|aH60)LUO;kJ-(`??A?++cY^bh|3Pm? z=+jzCY~dc1{le|=o8DRcF<{-(>gzrlyGVz)U-^+5N-N>+_!q|Kox%^OZ(C(s`u__1lu2IUkNo!o$ zM$^d;a|V<)gTKh^^ZC7FvmH2$h2J~yW?c&r*-9FjK2r@+qgTk#Aekx8_up6{;K%mA E0UMO-vH$=8 delta 269 zcmV+o0rLKoj|S-b1+X$Hlg7mmv#cpv3bRf$IBJtf7Y+wz0CfOt0Bn=7o+*+N99&~- zC|7k-`d(ODlxiRVE&wh7E&w0^?^FGN0GEM@h+lV=LS~bd0x&37WNS{43wkY14F80X zfr%(rvrssXA7_)5-A@5y$zdpU3wn=URffD}J8jicz;a7r0x*+cGZwRwp7<^YNqdD~ z3Qn_=kdz6tmdY&TF^$LIJW70tsW6tW*I|0tsWc z{!{^W47cE80W1fX8fyV2lS(}lldx+BmrQE`e7BZw0dN%%k9A62%vRjKoADbjvz*Ye1#Z82&1&!*3pmIw=hSP zuRtIuC>+UA0}2TeGz8K8sPQW!BoYzHY6$Q{$sGPrOqM8y;17f6-nQHL;UBZy_q~13 zInO!gInTX&cdzyOY3l`jF8{tm8oKZRs`0MhhQoWy=voh0aF@eID>QceAplKu@+{O; zWVj4NL=fz5Tp8B#5PR&8Sku%Pgb8NZ)8aa(0-#R~&a(Hej!VOZ0@@ZmMu#=fC*`rj zoS|3rQ@aM!=)KE@#P+-~!NzUa9`PBj5lN zt0vds_5A_ton*z~Z?$PA6BOqa78!0O#7llji*4%JEclP!zX#szH#Jall;USW$5EdX zPg-hKoj7k)&t5$X^Xu)M9F}6%83tRdZfdv$t1Aj0v#Cq4ySm&jIz)($iX zB3F4l^S%QeLfH7C3;l^|T7DRGkICZ*@sb6kIGMrwZV{z_UWHjzy{Z!_jiiW(7={|_ zaA*d1-4E6D@(19oNN`|OWH9%+&n0JD@MMsf+|!zQ;Gr8gId3}q+f6uOqu2XEqtOvo z{_zp6X~`ajx;6s0srn8ds$Y-S(BeDX(CT$o7E#AL9(rXZE1|7Lqva1kJL=Uilc>cv zTP@fbbe79englDs<$ILCJjt>%HAwoZ2RyD;v4K08E`qE!x#LASC~?g-x!AapVxfQL zA;#pJeE20ZoP?j!OE#!}WA23O*)bT=&hc@!Y~@oFXRi+l_tD}ZEB&$p^zs;EnDkO9`0(7&0=jYcdZ*7Xk4}6jw-D#zy!$o z$antkEl^1N55ZIj;PpbsEbf* zluN3fp!PI)N~59~8~>=orT6(5f5F;garvH6YLM7{%#=J6@*hF|>Pq5Iw`T2}SYnt- zm%b@xp-b&!VmoITOUk=Ww#HSZX`Zu;9@nV!?BqPM2a_jmh*gXio`5i;prg%<9u`}K zv4r8093bT#oxoR`gNriD-(iwLdF!eZsU#*_V!7hOGgB)~=2@m{rr+m-m;C#nNu91; z$ueBM)IvA5gOfJoz)A-Xu@pAW2CbBFvE``Sl2Ywt&)k87-pYhhI5R{uJ_FxEnNv0+ zvrF34wZd;Xv@TV3Qa!u)o21R)@A7Cg&j_{RMIa4fh(i%WL916<~^$6rJDVFxA^QW|1NG?<3_C zM{P>iw#NpJ1%CalUYbXh0u>yjs1wapwV>GuInatd*2k@;m+z@Lj@|i%EX1_&7dOccVbbu~gf`N%Lpb^a1A20I diff --git a/iOSClient/Transfers/NCTransferCell.swift b/iOSClient/Transfers/NCTransferCell.swift index d2087643c3..7df6bcedbc 100755 --- a/iOSClient/Transfers/NCTransferCell.swift +++ b/iOSClient/Transfers/NCTransferCell.swift @@ -26,6 +26,7 @@ import UIKit class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProtocol { @IBOutlet weak var imageItem: UIImageView! + @IBOutlet weak var imageStatus: UIImageView! @IBOutlet weak var labelTitle: UILabel! @IBOutlet weak var labelPath: UILabel! @IBOutlet weak var labelStatus: UILabel! @@ -36,21 +37,28 @@ class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellP @IBOutlet weak var separator: UIView! @IBOutlet weak var separatorHeightConstraint: NSLayoutConstraint! - private var objectId = "" + private var ocId = "" + private var ocIdTransfer = "" private var user = "" - var indexPath = IndexPath() weak var delegate: NCTransferCellDelegate? - var namedButtonMore = "" - var fileObjectId: String? { - get { return objectId } - set { objectId = newValue ?? "" } + var fileOcId: String? { + get { return ocId } + set { ocId = newValue ?? "" } + } + var fileOcIdTransfer: String? { + get { return ocIdTransfer } + set { ocIdTransfer = newValue ?? "" } } var filePreviewImageView: UIImageView? { get { return imageItem } set { imageItem = newValue } } + var fileStatusImage: UIImageView? { + get { return imageStatus } + set { imageStatus = newValue } + } var fileUser: String? { get { return user } set { user = newValue ?? "" } @@ -80,7 +88,7 @@ class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellP imageItem.layer.cornerRadius = 6 imageItem.layer.masksToBounds = true - progressView.tintColor = NCBrandColor.shared.brandElement + progressView.tintColor = NCBrandColor.shared.iconImageColor progressView.transform = CGAffineTransform(scaleX: 1.0, y: 0.5) progressView.trackTintColor = .clear @@ -109,19 +117,19 @@ class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellP } @IBAction func touchUpInsideShare(_ sender: Any) { - delegate?.tapShareListItem(with: objectId, indexPath: indexPath, sender: sender) + delegate?.tapShareListItem(with: ocId, ocIdTransfer: ocIdTransfer, sender: sender) } @IBAction func touchUpInsideMore(_ sender: Any) { - delegate?.tapMoreListItem(with: objectId, namedButtonMore: namedButtonMore, image: imageItem.image, indexPath: indexPath, sender: sender) + delegate?.tapMoreListItem(with: ocId, ocIdTransfer: ocIdTransfer, image: imageItem.image, sender: sender) } @objc func longPressInsideMore(gestureRecognizer: UILongPressGestureRecognizer) { - delegate?.longPressMoreListItem(with: objectId, namedButtonMore: namedButtonMore, indexPath: indexPath, gestureRecognizer: gestureRecognizer) + delegate?.longPressMoreListItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) } @objc func longPress(gestureRecognizer: UILongPressGestureRecognizer) { - delegate?.longPressListItem(with: objectId, indexPath: indexPath, gestureRecognizer: gestureRecognizer) + delegate?.longPressListItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) } func hideButtonMore(_ status: Bool) { @@ -129,8 +137,16 @@ class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellP buttonMore.isHidden = status } - func setButtonMore(named: String, image: UIImage) { - namedButtonMore = named + func setProgress(progress: Float) { + progressView.progress = progress + if progress > 0.0 { + progressView.isHidden = false + } else { + progressView.isHidden = true + } + } + + func setButtonMore(image: UIImage) { imageMore.image = image self.accessibilityCustomActions = [ UIAccessibilityCustomAction( @@ -143,11 +159,19 @@ class NCTransferCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellP func writeInfoDateSize(date: NSDate, size: Int64) { labelInfo.text = NCUtility().dateDiff(date as Date) + " · " + NCUtilityFileSystem().transformedSize(size) } + + func setIconOutlines() { + if imageStatus.image != nil { + imageStatus.makeCircularBackground(withColor: .systemBackground) + } else { + imageStatus.backgroundColor = .clear + } + } } protocol NCTransferCellDelegate: AnyObject { - func tapShareListItem(with objectId: String, indexPath: IndexPath, sender: Any) - func tapMoreListItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) - func longPressMoreListItem(with objectId: String, namedButtonMore: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) - func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) + func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) + func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) + func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) + func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) } diff --git a/iOSClient/Transfers/NCTransferCell.xib b/iOSClient/Transfers/NCTransferCell.xib index ad8fe3e67e..9a651e37aa 100755 --- a/iOSClient/Transfers/NCTransferCell.xib +++ b/iOSClient/Transfers/NCTransferCell.xib @@ -1,29 +1,35 @@ - + - - + - + - + + + + + + + + - - + + - + @@ -93,23 +100,24 @@ - + - + - - - - - + + + + + + diff --git a/iOSClient/Transfers/NCTransfers.swift b/iOSClient/Transfers/NCTransfers.swift index 1360afde59..a118bd42e7 100644 --- a/iOSClient/Transfers/NCTransfers.swift +++ b/iOSClient/Transfers/NCTransfers.swift @@ -23,10 +23,8 @@ import UIKit import NextcloudKit -import JGProgressHUD class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { - var metadataTemp: tableMetadata? required init?(coder aDecoder: NSCoder) { @@ -36,8 +34,8 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { layoutKey = NCGlobal.shared.layoutViewTransfers enableSearchBar = false headerRichWorkspaceDisable = true - headerMenuTransferView = true - emptyImage = utility.loadImage(named: "arrow.left.arrow.right", colors: [NCBrandColor.shared.brandElement]) + headerMenuTransferView = false + emptyImageName = "arrow.left.arrow.right.circle" emptyTitle = "_no_transfer_" emptyDescription = "_no_transfer_sub_" } @@ -48,16 +46,14 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { super.viewDidLoad() listLayout.itemHeight = 105 - NCManageDatabase.shared.setLayoutForView(account: appDelegate.account, key: layoutKey, serverUrl: serverUrl, layout: NCGlobal.shared.layoutList) + self.database.setLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl, layout: NCGlobal.shared.layoutList) self.navigationItem.title = titleCurrentFolder } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + reloadDataSource() - Task { - await NCNetworkingProcess.shared.verifyZombie() - } } override func setNavigationLeftItems() { @@ -67,53 +63,81 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { // MARK: - NotificationCenter - override func downloadStartFile(_ notification: NSNotification) { + override func deleteFile(_ notification: NSNotification) { + reloadDataSource() + } - notificationReloadDataSource += 1 + override func renameFile(_ notification: NSNotification) { + reloadDataSource() } - override func downloadedFile(_ notification: NSNotification) { + override func createFolder(_ notification: NSNotification) { + reloadDataSource() + } - notificationReloadDataSource += 1 + override func copyMoveFile(_ notification: NSNotification) { + reloadDataSource() } - override func downloadCancelFile(_ notification: NSNotification) { + override func downloadStartFile(_ notification: NSNotification) { + reloadDataSource() + } + override func downloadedFile(_ notification: NSNotification) { reloadDataSource() } - override func uploadStartFile(_ notification: NSNotification) { + override func downloadCancelFile(_ notification: NSNotification) { + reloadDataSource() + } - notificationReloadDataSource += 1 + override func uploadStartFile(_ notification: NSNotification) { + reloadDataSource() } override func uploadedFile(_ notification: NSNotification) { - - notificationReloadDataSource += 1 + reloadDataSource() } override func uploadedLivePhoto(_ notification: NSNotification) { - - notificationReloadDataSource += 1 + reloadDataSource() } override func uploadCancelFile(_ notification: NSNotification) { - reloadDataSource() } - // MARK: TAP EVENT - - override func tapMoreGridItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) { - guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(objectId) else { return } - - Task { - await cancelSession(metadata: metadata) + override func triggerProgressTask(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let progressNumber = userInfo["progress"] as? NSNumber, + let totalBytes = userInfo["totalBytes"] as? Int64, + let totalBytesExpected = userInfo["totalBytesExpected"] as? Int64, + let ocId = userInfo["ocId"] as? String, + let ocIdTransfer = userInfo["ocIdTransfer"] as? String, + let session = userInfo["session"] as? String + else { return } + let chunk: Int = userInfo["chunk"] as? Int ?? 0 + let e2eEncrypted: Bool = userInfo["e2eEncrypted"] as? Bool ?? false + NCTransferProgress.shared.append(NCTransferProgress.Transfer(ocId: ocId, ocIdTransfer: ocIdTransfer, session: session, chunk: chunk, e2eEncrypted: e2eEncrypted, progressNumber: progressNumber, totalBytes: totalBytes, totalBytesExpected: totalBytesExpected)) + + DispatchQueue.main.async { + for case let cell as NCTransferCell in self.collectionView.visibleCells { + if cell.fileOcIdTransfer == ocIdTransfer { + cell.setProgress(progress: progressNumber.floatValue) + cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + " - " + self.utilityFileSystem.transformedSize(totalBytes) + } + } } } - override func longPressMoreListItem(with objectId: String, namedButtonMore: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { + // MARK: TAP EVENT + + override func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { + guard let metadata = self.database.getMetadataFromOcIdAndocIdTransfer(ocIdTransfer) else { return } + NCNetworking.shared.cancelTask(metadata: metadata) + } + override func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state != .began { return } let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .alert) @@ -128,11 +152,10 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { self.present(alertController, animated: true, completion: nil) } - override func longPressListItem(with objectId: String, indexPath: IndexPath, gestureRecognizer: UILongPressGestureRecognizer) { - + override func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state != .began { return } - if let metadata = NCManageDatabase.shared.getMetadataFromOcId(objectId) { + if let metadata = self.database.getMetadataFromOcIdAndocIdTransfer(ocIdTransfer) { metadataTemp = metadata let touchPoint = gestureRecognizer.location(in: collectionView) becomeFirstResponder() @@ -145,29 +168,25 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { override func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { } @objc func startTask(_ notification: Any) { - - guard let metadata = metadataTemp, - let hudView = self.tabBarController?.view, - appDelegate.account == metadata.account else { return } - + guard let metadata = metadataTemp else { return } let cameraRoll = NCCameraRoll() + cameraRoll.extractCameraRoll(from: metadata) { metadatas in for metadata in metadatas { - if let metadata = NCManageDatabase.shared.setMetadataStatus(ocId: metadata.ocId, status: NCGlobal.shared.metadataStatusUploading) { - NCNetworking.shared.removeTransferInError(ocId: metadata.ocId) - NCNetworking.shared.upload(metadata: metadata, hudView: hudView, hud: JGProgressHUD()) + if let metadata = self.database.setMetadataStatus(ocId: metadata.ocId, status: NCGlobal.shared.metadataStatusUploading) { + NCTransferProgress.shared.clearCountError(ocIdTransfer: metadata.ocIdTransfer) + NCNetworking.shared.upload(metadata: metadata) } } } } override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if action != #selector(startTask(_:)) { return false } guard let metadata = metadataTemp else { return false } if metadata.isDirectoryE2EE { return false } - if metadata.status == NCGlobal.shared.metadataStatusWaitUpload || metadata.isUpload { + if metadata.isUpload { return true } @@ -185,71 +204,103 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "transferCell", for: indexPath) as? NCTransferCell, - let metadata = dataSource.cellForItemAt(indexPath: indexPath) else { - return NCTransferCell() + let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "transferCell", for: indexPath) as? NCTransferCell)! + guard let metadata = self.dataSource.getResultMetadata(indexPath: indexPath) else { + return cell } + let transfer = NCTransferProgress.shared.get(ocId: metadata.ocId, ocIdTransfer: metadata.ocIdTransfer, session: metadata.session) cell.delegate = self - - cell.fileObjectId = metadata.ocId - cell.indexPath = indexPath + cell.fileOcId = metadata.ocId + cell.fileOcIdTransfer = metadata.ocIdTransfer cell.fileUser = metadata.ownerId - cell.indexPath = indexPath - cell.imageItem.image = NCImageCache.images.file - cell.imageItem.backgroundColor = nil + cell.filePreviewImageView?.image = imageCache.getImageFile() + cell.filePreviewImageView?.backgroundColor = nil cell.labelTitle.text = metadata.fileNameView cell.labelTitle.textColor = NCBrandColor.shared.textColor - let serverUrlHome = utilityFileSystem.getHomeServer(urlBase: metadata.urlBase, userId: metadata.userId) + let serverUrlHome = utilityFileSystem.getHomeServer(session: session) var pathText = metadata.serverUrl.replacingOccurrences(of: serverUrlHome, with: "") if pathText.isEmpty { pathText = "/" } cell.labelPath.text = pathText - cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop) - cell.progressView.progress = 0.0 - if let image = utility.getIcon(metadata: metadata) { - cell.imageItem.image = image - } else if !metadata.iconName.isEmpty { - cell.imageItem.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true) - } else { - cell.imageItem.image = NCImageCache.images.file - } - cell.labelInfo.text = utility.dateDiff(metadata.date as Date) + " · " + utilityFileSystem.transformedSize(metadata.size) - if metadata.status == NCGlobal.shared.metadataStatusDownloading || metadata.status == NCGlobal.shared.metadataStatusUploading { - cell.progressView.isHidden = false + cell.setButtonMore(image: imageCache.getImageButtonStop()) + + /// Image item + if !metadata.iconName.isEmpty { + cell.filePreviewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } else { - cell.progressView.isHidden = true + cell.filePreviewImageView?.image = imageCache.getImageFile() } - // Write status on Label Info + + /// Status and Info + let user = (metadata.user == session.user ? "" : " - " + metadata.account) switch metadata.status { + case NCGlobal.shared.metadataStatusWaitCreateFolder: + cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_create_folder_", comment: "") + user + cell.labelInfo.text = "" + case NCGlobal.shared.metadataStatusWaitDelete: + cell.fileStatusImage?.image = utility.loadImage(named: "trash.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_delete_", comment: "") + user + cell.labelInfo.text = "" + case NCGlobal.shared.metadataStatusWaitFavorite: + cell.fileStatusImage?.image = utility.loadImage(named: "star.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_favorite_", comment: "") + user + cell.labelInfo.text = "" + case NCGlobal.shared.metadataStatusWaitCopy: + cell.fileStatusImage?.image = utility.loadImage(named: "c.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_copy_", comment: "") + user + cell.labelInfo.text = "" + case NCGlobal.shared.metadataStatusWaitMove: + cell.fileStatusImage?.image = utility.loadImage(named: "m.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_move_", comment: "") + user + cell.labelInfo.text = "" + case NCGlobal.shared.metadataStatusWaitRename: + cell.fileStatusImage?.image = utility.loadImage(named: "a.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_rename_", comment: "") + user + cell.labelInfo.text = "" case NCGlobal.shared.metadataStatusWaitDownload: - cell.labelStatus.text = NSLocalizedString("_status_wait_download_", comment: "") + cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_download_", comment: "") + user cell.labelInfo.text = utilityFileSystem.transformedSize(metadata.size) case NCGlobal.shared.metadataStatusDownloading: - cell.labelStatus.text = NSLocalizedString("_status_downloading_", comment: "") - cell.labelInfo.text = utilityFileSystem.transformedSize(metadata.size) + " - ↓ …" + if #available(iOS 17.0, *) { + cell.fileStatusImage?.image = utility.loadImage(named: "arrowshape.down.circle", colors: NCBrandColor.shared.iconImageMultiColors) + } + cell.labelStatus.text = NSLocalizedString("_status_downloading_", comment: "") + user + cell.labelInfo.text = utilityFileSystem.transformedSize(metadata.size) + " - " + self.utilityFileSystem.transformedSize(transfer.totalBytes) case NCGlobal.shared.metadataStatusWaitUpload: - cell.labelStatus.text = NSLocalizedString("_status_wait_upload_", comment: "") + cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_wait_upload_", comment: "") + user cell.labelInfo.text = "" case NCGlobal.shared.metadataStatusUploading: - cell.labelStatus.text = NSLocalizedString("_status_uploading_", comment: "") - cell.labelInfo.text = utilityFileSystem.transformedSize(metadata.size) + " - ↑ …" - case NCGlobal.shared.metadataStatusUploadError: - cell.labelStatus.text = NSLocalizedString("_status_upload_error_", comment: "") + if #available(iOS 17.0, *) { + cell.fileStatusImage?.image = utility.loadImage(named: "arrowshape.up.circle", colors: NCBrandColor.shared.iconImageMultiColors) + } + cell.labelStatus.text = NSLocalizedString("_status_uploading_", comment: "") + user + cell.labelInfo.text = utilityFileSystem.transformedSize(metadata.size) + " - " + self.utilityFileSystem.transformedSize(transfer.totalBytes) + case NCGlobal.shared.metadataStatusDownloadError, NCGlobal.shared.metadataStatusUploadError: + cell.fileStatusImage?.image = utility.loadImage(named: "exclamationmark.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.labelStatus.text = NSLocalizedString("_status_upload_error_", comment: "") + user cell.labelInfo.text = metadata.sessionError default: + cell.fileStatusImage?.image = nil cell.labelStatus.text = "" cell.labelInfo.text = "" } - if self.appDelegate.account != metadata.account { - cell.labelInfo.text = NSLocalizedString("_waiting_for_", comment: "") + " " + NSLocalizedString("_user_", comment: "").lowercased() + " \(metadata.userId) " + NSLocalizedString("_in_", comment: "") + " \(metadata.urlBase)" - } - let isWiFi = NCNetworking.shared.networkReachability == .reachableEthernetOrWiFi - if metadata.session == NCNetworking.shared.sessionUploadBackgroundWWan && !isWiFi { + + if metadata.session == NCNetworking.shared.sessionUploadBackgroundWWan && !(NCNetworking.shared.networkReachability == .reachableEthernetOrWiFi) { cell.labelInfo.text = NSLocalizedString("_waiting_for_", comment: "") + " " + NSLocalizedString("_reachable_wifi_", comment: "") } cell.accessibilityLabel = metadata.fileNameView + ", " + (cell.labelInfo.text ?? "") - // Remove last separator + + /// Progress view + if let transfer = NCTransferProgress.shared.get(ocIdTransfer: metadata.ocIdTransfer) { + cell.setProgress(progress: transfer.progressNumber.floatValue) + } else { + cell.setProgress(progress: 0.0) + } + + /// Remove last separator if collectionView.numberOfItems(inSection: indexPath.section) == indexPath.row + 1 { cell.separator.isHidden = true } else { @@ -259,23 +310,23 @@ class NCTransfers: NCCollectionViewCommon, NCTransferCellDelegate { return cell } - // MARK: - DataSource + NC Endpoint + // MARK: - DataSource - override func queryDB() { - super.queryDB() + override func reloadDataSource() { + if let results = self.database.getResultsMetadatas(predicate: NSPredicate(format: "status != %i", NCGlobal.shared.metadataStatusNormal), sortedByKeyPath: "sessionDate", ascending: true) { + self.dataSource = NCCollectionViewDataSource(results: results, layoutForView: layoutForView) + } else { + self.dataSource.removeAll() + } - let metadatas: [tableMetadata] = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "status != %i", NCGlobal.shared.metadataStatusNormal), sorted: "sessionDate", ascending: true) ?? [] - self.dataSource = NCDataSource(metadatas: metadatas, account: self.appDelegate.account, layoutForView: layoutForView) - } + if self.dataSource.isEmpty() { + NCTransferProgress.shared.removeAll() + } - override func reloadDataSource(withQueryDB: Bool = true) { - super.reloadDataSource(withQueryDB: withQueryDB) + super.reloadDataSource() } - override func reloadDataSourceNetwork(withQueryDB: Bool = false) { - Task { - await NCNetworkingProcess.shared.verifyZombie() - super.reloadDataSource(withQueryDB: true) - } + override func getServerData() { + reloadDataSource() } } diff --git a/iOSClient/Trash/Cell/NCTrashCellProtocol.swift b/iOSClient/Trash/Cell/NCTrashCellProtocol.swift index 84cd9214d6..a07941c575 100644 --- a/iOSClient/Trash/Cell/NCTrashCellProtocol.swift +++ b/iOSClient/Trash/Cell/NCTrashCellProtocol.swift @@ -28,9 +28,9 @@ protocol NCTrashCellProtocol { var labelTitle: UILabel! { get set } var labelInfo: UILabel! { get set } var imageItem: UIImageView! { get set } - var indexPath: IndexPath { get set } + var account: String { get set } - func selected(_ status: Bool, isEditMode: Bool) + func selected(_ status: Bool, isEditMode: Bool, account: String) } extension NCTrashCellProtocol where Self: UICollectionViewCell { @@ -48,7 +48,7 @@ extension NCTrashCellProtocol where Self: UICollectionViewCell { self.labelInfo?.text = dateFormatter.string(from: tableTrash.trashbinDeletionTime as Date) } if tableTrash.directory { - self.imageItem.image = NCImageCache.images.folder + self.imageItem.image = NCImageCache.shared.getFolder(account: tableTrash.account) } else { self.imageItem.image = image self.labelInfo?.text = (self.labelInfo?.text ?? "") + " · " + NCUtilityFileSystem().transformedSize(tableTrash.size) diff --git a/iOSClient/Trash/Cell/NCTrashGridCell.swift b/iOSClient/Trash/Cell/NCTrashGridCell.swift index 44c53a1ae2..81743c6653 100644 --- a/iOSClient/Trash/Cell/NCTrashGridCell.swift +++ b/iOSClient/Trash/Cell/NCTrashGridCell.swift @@ -24,7 +24,7 @@ import UIKit protocol NCTrashGridCellDelegate: AnyObject { - func tapMoreGridItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) + func tapMoreGridItem(with objectId: String, image: UIImage?, sender: Any) } class NCTrashGridCell: UICollectionViewCell, NCTrashCellProtocol { @@ -40,8 +40,8 @@ class NCTrashGridCell: UICollectionViewCell, NCTrashCellProtocol { weak var delegate: NCTrashGridCellDelegate? var objectId = "" var indexPath = IndexPath() + var account = "" var user = "" - var namedButtonMore = "" override func awakeFromNib() { super.awakeFromNib() @@ -76,22 +76,19 @@ class NCTrashGridCell: UICollectionViewCell, NCTrashCellProtocol { } @IBAction func touchUpInsideMore(_ sender: Any) { - delegate?.tapMoreGridItem(with: objectId, namedButtonMore: namedButtonMore, image: imageItem.image, indexPath: indexPath, sender: sender) + delegate?.tapMoreGridItem(with: objectId, image: imageItem.image, sender: sender) } fileprivate func setA11yActions() { - let moreName = namedButtonMore == NCGlobal.shared.buttonMoreStop ? "_cancel_" : "_more_" - self.accessibilityCustomActions = [ UIAccessibilityCustomAction( - name: NSLocalizedString(moreName, comment: ""), + name: NSLocalizedString("_more_", comment: ""), target: self, selector: #selector(touchUpInsideMore)) ] } - func setButtonMore(named: String, image: UIImage) { - namedButtonMore = named + func setButtonMore(image: UIImage) { buttonMore.setImage(image, for: .normal) setA11yActions() } @@ -100,7 +97,7 @@ class NCTrashGridCell: UICollectionViewCell, NCTrashCellProtocol { buttonMore.isHidden = status } - func selected(_ status: Bool, isEditMode: Bool) { + func selected(_ status: Bool, isEditMode: Bool, account: String) { if isEditMode { buttonMore.isHidden = true accessibilityCustomActions = nil @@ -109,7 +106,7 @@ class NCTrashGridCell: UICollectionViewCell, NCTrashCellProtocol { setA11yActions() } if status { - imageSelect.image = NCImageCache.images.checkedYes + imageSelect.image = NCImageCache.shared.getImageCheckedYes() imageSelect.isHidden = false imageVisualEffect.isHidden = false } else { diff --git a/iOSClient/Trash/Cell/NCTrashListCell.swift b/iOSClient/Trash/Cell/NCTrashListCell.swift index 5a10f3ac34..56cf6fa63c 100644 --- a/iOSClient/Trash/Cell/NCTrashListCell.swift +++ b/iOSClient/Trash/Cell/NCTrashListCell.swift @@ -45,7 +45,7 @@ class NCTrashListCell: UICollectionViewCell, NCTrashCellProtocol { weak var delegate: NCTrashListCellDelegate? var objectId = "" - var indexPath = IndexPath() + var account = "" override func awakeFromNib() { super.awakeFromNib() @@ -89,7 +89,7 @@ class NCTrashListCell: UICollectionViewCell, NCTrashCellProtocol { delegate?.tapRestoreListItem(with: objectId, image: imageItem.image, sender: sender) } - func selected(_ status: Bool, isEditMode: Bool) { + func selected(_ status: Bool, isEditMode: Bool, account: String) { if isEditMode { imageItemLeftConstraint.constant = 45 imageSelect.isHidden = false @@ -112,11 +112,11 @@ class NCTrashListCell: UICollectionViewCell, NCTrashCellProtocol { blurEffectView?.backgroundColor = .lightGray blurEffectView?.frame = self.bounds blurEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] - imageSelect.image = NCImageCache.images.checkedYes + imageSelect.image = NCImageCache.shared.getImageCheckedYes() backgroundView = blurEffectView separator.isHidden = true } else { - imageSelect.image = NCImageCache.images.checkedNo + imageSelect.image = NCImageCache.shared.getImageCheckedNo() backgroundView = nil separator.isHidden = false } diff --git a/iOSClient/Trash/NCTrash+CollectionView.swift b/iOSClient/Trash/NCTrash+CollectionView.swift index 48ea792fe5..90f673dc23 100644 --- a/iOSClient/Trash/NCTrash+CollectionView.swift +++ b/iOSClient/Trash/NCTrash+CollectionView.swift @@ -27,23 +27,23 @@ import RealmSwift // MARK: UICollectionViewDelegate extension NCTrash: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let tableTrash = datasource[indexPath.item] + guard let resultTableTrash = datasource?[indexPath.item] else { return } guard !isEditMode else { - if let index = selectOcId.firstIndex(of: tableTrash.fileId) { + if let index = selectOcId.firstIndex(of: resultTableTrash.fileId) { selectOcId.remove(at: index) } else { - selectOcId.append(tableTrash.fileId) + selectOcId.append(resultTableTrash.fileId) } collectionView.reloadItems(at: [indexPath]) tabBarSelect.update(selectOcId: selectOcId) return } - if tableTrash.directory, + if resultTableTrash.directory, let ncTrash: NCTrash = UIStoryboard(name: "NCTrash", bundle: nil).instantiateInitialViewController() as? NCTrash { - ncTrash.filePath = tableTrash.filePath + tableTrash.fileName - ncTrash.titleCurrentFolder = tableTrash.trashbinFileName - ncTrash.filename = tableTrash.fileName + ncTrash.filePath = resultTableTrash.filePath + resultTableTrash.fileName + ncTrash.titleCurrentFolder = resultTableTrash.trashbinFileName + ncTrash.filename = resultTableTrash.fileName self.navigationController?.pushViewController(ncTrash, animated: true) } } @@ -52,50 +52,53 @@ extension NCTrash: UICollectionViewDelegate { // MARK: UICollectionViewDataSource extension NCTrash: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return datasource.count + return datasource?.count ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let tableTrash = datasource[indexPath.item] var image: UIImage? var cell: NCTrashCellProtocol & UICollectionViewCell if layoutForView?.layout == NCGlobal.shared.layoutList { - guard let listCell = collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCTrashListCell else { return NCTrashListCell() } + let listCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCTrashListCell)! listCell.delegate = self cell = listCell } else { - guard let gridCell = collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCTrashGridCell else { return NCTrashGridCell() } - gridCell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCImageCache.images.buttonMore) + let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCTrashGridCell)! + gridCell.setButtonMore(image: NCImageCache.shared.getImageButtonMore()) gridCell.delegate = self cell = gridCell } + guard let resultTableTrash = datasource?[indexPath.item] else { return cell } - if tableTrash.iconName.isEmpty { - image = NCImageCache.images.file + cell.imageItem.contentMode = .scaleAspectFit + + if resultTableTrash.iconName.isEmpty { + image = NCImageCache.shared.getImageFile() } else { - image = NCUtility().loadImage(named: tableTrash.iconName, useTypeIconFile: true) + image = NCUtility().loadImage(named: resultTableTrash.iconName, useTypeIconFile: true, account: resultTableTrash.account) } - if FileManager().fileExists(atPath: utilityFileSystem.getDirectoryProviderStorageIconOcId(tableTrash.fileId, etag: tableTrash.fileName)) { - image = UIImage(contentsOfFile: utilityFileSystem.getDirectoryProviderStorageIconOcId(tableTrash.fileId, etag: tableTrash.fileName)) + if let imageIcon = utility.getImage(ocId: resultTableTrash.fileId, etag: resultTableTrash.fileName, ext: NCGlobal.shared.previewExt512) { + image = imageIcon + cell.imageItem.contentMode = .scaleAspectFill } else { - if tableTrash.hasPreview && !utilityFileSystem.fileProviderStoragePreviewIconExists(tableTrash.fileId, etag: tableTrash.fileName) { - if NCNetworking.shared.downloadThumbnailTrashQueue.operations.filter({ ($0 as? NCOperationDownloadThumbnailTrash)?.fileId == tableTrash.fileId }).isEmpty { - NCNetworking.shared.downloadThumbnailTrashQueue.addOperation(NCOperationDownloadThumbnailTrash(tableTrash: tableTrash, fileId: tableTrash.fileId, account: appDelegate.account, cell: cell, collectionView: collectionView)) + if resultTableTrash.hasPreview { + if NCNetworking.shared.downloadThumbnailTrashQueue.operations.filter({ ($0 as? NCOperationDownloadThumbnailTrash)?.fileId == resultTableTrash.fileId }).isEmpty { + NCNetworking.shared.downloadThumbnailTrashQueue.addOperation(NCOperationDownloadThumbnailTrash(fileId: resultTableTrash.fileId, fileName: resultTableTrash.fileName, account: session.account, collectionView: collectionView)) } } } - cell.indexPath = indexPath - cell.objectId = tableTrash.fileId - cell.setupCellUI(tableTrash: tableTrash, image: image) - cell.selected(selectOcId.contains(tableTrash.fileId), isEditMode: isEditMode) + cell.account = resultTableTrash.account + cell.objectId = resultTableTrash.fileId + cell.setupCellUI(tableTrash: resultTableTrash, image: image) + cell.selected(selectOcId.contains(resultTableTrash.fileId), isEditMode: isEditMode, account: resultTableTrash.account) return cell } - func setTextFooter(datasource: [tableTrash]) -> String { + func setTextFooter(datasource: Results) -> String { var folders: Int = 0, foldersText = "" var files: Int = 0, filesText = "" var size: Int64 = 0 @@ -137,15 +140,17 @@ extension NCTrash: UICollectionViewDataSource { if kind == UICollectionView.elementKindSectionHeader { guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFirstHeaderEmptyData", for: indexPath) as? NCSectionFirstHeaderEmptyData else { return NCSectionFirstHeaderEmptyData() } - header.emptyImage.image = utility.loadImage(named: "trash", colors: [NCBrandColor.shared.brandElement]) + header.emptyImage.image = utility.loadImage(named: "trash", colors: [NCBrandColor.shared.getElement(account: session.account)]) header.emptyTitle.text = NSLocalizedString("_trash_no_trash_", comment: "") header.emptyDescription.text = NSLocalizedString("_trash_no_trash_description_", comment: "") return header } else { guard let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "sectionFooter", for: indexPath) as? NCSectionFooter else { return NCSectionFooter() } - footer.setTitleLabel(setTextFooter(datasource: datasource)) - footer.separatorIsHidden(true) + if let datasource { + footer.setTitleLabel(setTextFooter(datasource: datasource)) + footer.separatorIsHidden(true) + } return footer } } @@ -155,8 +160,8 @@ extension NCTrash: UICollectionViewDataSource { extension NCTrash: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { var height: Double = 0 - if datasource.isEmpty { - height = NCGlobal.shared.getHeightHeaderEmptyData(view: view, portraitOffset: 0, landscapeOffset: 0) + if let datasource, datasource.isEmpty { + height = utility.getHeightHeaderEmptyData(view: view, portraitOffset: 0, landscapeOffset: 0) } return CGSize(width: collectionView.frame.width, height: height) } diff --git a/iOSClient/Trash/NCTrash+Networking.swift b/iOSClient/Trash/NCTrash+Networking.swift index 00750176af..d7125c55ed 100644 --- a/iOSClient/Trash/NCTrash+Networking.swift +++ b/iOSClient/Trash/NCTrash+Networking.swift @@ -26,14 +26,14 @@ import RealmSwift extension NCTrash { @objc func loadListingTrash() { - NextcloudKit.shared.listingTrash(filename: filename, showHiddenFiles: false, account: appDelegate.account) { task in + NextcloudKit.shared.listingTrash(filename: filename, showHiddenFiles: false, account: session.account) { task in self.dataSourceTask = task self.collectionView.reloadData() } completion: { account, items, _, error in self.refreshControl.endRefreshing() - if account == self.appDelegate.account { - NCManageDatabase.shared.deleteTrash(filePath: self.getFilePath(), account: account) - NCManageDatabase.shared.addTrash(account: account, items: items) + if let items { + self.database.deleteTrash(filePath: self.getFilePath(), account: account) + self.database.addTrash(account: account, items: items) } self.reloadDataSource() if error != .success { @@ -43,89 +43,76 @@ extension NCTrash { } func restoreItem(with fileId: String) { - guard let tableTrash = NCManageDatabase.shared.getTrashItem(fileId: fileId, account: appDelegate.account) else { return } - let fileNameFrom = tableTrash.filePath + tableTrash.fileName - let fileNameTo = appDelegate.urlBase + "/" + NextcloudKit.shared.nkCommonInstance.dav + "/trashbin/" + appDelegate.userId + "/restore/" + tableTrash.fileName + guard let resultTableTrash = self.database.getResultTrashItem(fileId: fileId, account: session.account) else { return } + let fileNameFrom = resultTableTrash.filePath + resultTableTrash.fileName + let fileNameTo = session.urlBase + "/remote.php/dav/trashbin/" + session.userId + "/restore/" + resultTableTrash.fileName - NextcloudKit.shared.moveFileOrFolder(serverUrlFileNameSource: fileNameFrom, serverUrlFileNameDestination: fileNameTo, overwrite: true, account: appDelegate.account) { account, error in - guard error == .success, account == self.appDelegate.account else { + NextcloudKit.shared.moveFileOrFolder(serverUrlFileNameSource: fileNameFrom, serverUrlFileNameDestination: fileNameTo, overwrite: true, account: session.account) { account, _, error in + guard error == .success else { NCContentPresenter().showError(error: error) return } - NCManageDatabase.shared.deleteTrash(fileId: fileId, account: account) + self.database.deleteTrash(fileId: fileId, account: account) self.reloadDataSource() } } func emptyTrash() { - let serverUrlFileName = appDelegate.urlBase + "/" + NextcloudKit.shared.nkCommonInstance.dav + "/trashbin/" + appDelegate.userId + "/trash" + let serverUrlFileName = session.urlBase + "/remote.php/dav/trashbin/" + session.userId + "/trash" - NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: appDelegate.account) { account, error in - guard error == .success, account == self.appDelegate.account else { + NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: session.account) { account, _, error in + guard error == .success else { NCContentPresenter().showError(error: error) return } - NCManageDatabase.shared.deleteTrash(fileId: nil, account: self.appDelegate.account) + self.database.deleteTrash(fileId: nil, account: account) self.reloadDataSource() } } func deleteItem(with fileId: String) { - guard let tableTrash = NCManageDatabase.shared.getTrashItem(fileId: fileId, account: appDelegate.account) else { return } - let serverUrlFileName = tableTrash.filePath + tableTrash.fileName + guard let resultTableTrash = self.database.getResultTrashItem(fileId: fileId, account: session.account) else { return } + let serverUrlFileName = resultTableTrash.filePath + resultTableTrash.fileName - NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: appDelegate.account) { account, error in - guard error == .success, account == self.appDelegate.account else { + NextcloudKit.shared.deleteFileOrFolder(serverUrlFileName: serverUrlFileName, account: session.account) { account, _, error in + guard error == .success else { NCContentPresenter().showError(error: error) return } - NCManageDatabase.shared.deleteTrash(fileId: fileId, account: account) + self.database.deleteTrash(fileId: fileId, account: account) self.reloadDataSource() } } } -class NCOperationDownloadThumbnailTrash: ConcurrentOperation { - - var tableTrash: tableTrash +class NCOperationDownloadThumbnailTrash: ConcurrentOperation, @unchecked Sendable { var fileId: String - var collectionView: UICollectionView? - var cell: NCTrashCellProtocol? + var fileName: String + var collectionView: UICollectionView var account: String - init(tableTrash: tableTrash, fileId: String, account: String, cell: NCTrashCellProtocol?, collectionView: UICollectionView?) { - self.tableTrash = tableTrash + init(fileId: String, fileName: String, account: String, collectionView: UICollectionView) { self.fileId = fileId + self.fileName = fileName self.account = account - self.cell = cell self.collectionView = collectionView } override func start() { guard !isCancelled else { return self.finish() } - let fileNamePreviewLocalPath = NCUtilityFileSystem().getDirectoryProviderStoragePreviewOcId(tableTrash.fileId, etag: tableTrash.fileName) - let fileNameIconLocalPath = NCUtilityFileSystem().getDirectoryProviderStorageIconOcId(tableTrash.fileId, etag: tableTrash.fileName) - NextcloudKit.shared.downloadTrashPreview(fileId: tableTrash.fileId, - fileNamePreviewLocalPath: fileNamePreviewLocalPath, - fileNameIconLocalPath: fileNameIconLocalPath, - widthPreview: NCGlobal.shared.sizePreview, - heightPreview: NCGlobal.shared.sizePreview, - sizeIcon: NCGlobal.shared.sizeIcon, - account: account, - options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { _, imagePreview, _, _, _, error in + NextcloudKit.shared.downloadTrashPreview(fileId: fileId, account: account) { _, _, _, responseData, error in + if error == .success, let data = responseData?.data { + NCUtility().createImageFileFrom(data: data, ocId: self.fileId, etag: self.fileName) + + for case let cell as NCTrashCellProtocol in self.collectionView.visibleCells where cell.objectId == self.fileId { + cell.imageItem?.contentMode = .scaleAspectFill - if error == .success, let imagePreview = imagePreview { - DispatchQueue.main.async { - if self.fileId == self.cell?.objectId, let imageView = self.cell?.imageItem { - UIView.transition(with: imageView, - duration: 0.75, - options: .transitionCrossDissolve, - animations: { imageView.image = imagePreview }, - completion: nil) - } else { - self.collectionView?.reloadData() - } + UIView.transition(with: cell.imageItem, + duration: 0.75, + options: .transitionCrossDissolve, + animations: { cell.imageItem.image = UIImage(data: data) }, + completion: nil) } } self.finish() diff --git a/iOSClient/Trash/NCTrash+SelectTabBarDelegate.swift b/iOSClient/Trash/NCTrash+SelectTabBarDelegate.swift index 2a854f799a..fe773d7b4c 100644 --- a/iOSClient/Trash/NCTrash+SelectTabBarDelegate.swift +++ b/iOSClient/Trash/NCTrash+SelectTabBarDelegate.swift @@ -20,15 +20,15 @@ // import Foundation +import UIKit extension NCTrash: NCTrashSelectTabBarDelegate { func onListSelected() { if layoutForView?.layout == NCGlobal.shared.layoutGrid { layoutForView?.layout = NCGlobal.shared.layoutList - NCManageDatabase.shared.setLayoutForView(account: appDelegate.account, key: layoutKey, serverUrl: "", layout: layoutForView?.layout) + self.database.setLayoutForView(account: session.account, key: layoutKey, serverUrl: "", layout: layoutForView?.layout) self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) } } @@ -36,19 +36,19 @@ extension NCTrash: NCTrashSelectTabBarDelegate { func onGridSelected() { if layoutForView?.layout == NCGlobal.shared.layoutList { layoutForView?.layout = NCGlobal.shared.layoutGrid - NCManageDatabase.shared.setLayoutForView(account: appDelegate.account, key: layoutKey, serverUrl: "", layout: layoutForView?.layout) + self.database.setLayoutForView(account: session.account, key: layoutKey, serverUrl: "", layout: layoutForView?.layout) self.collectionView.reloadData() - self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) } } func selectAll() { + guard let datasource else { return } if !selectOcId.isEmpty, datasource.count == selectOcId.count { selectOcId = [] } else { - selectOcId = self.datasource.compactMap({ $0.fileId }) + selectOcId = datasource.compactMap({ $0.fileId }) } tabBarSelect.update(selectOcId: selectOcId) collectionView.reloadData() diff --git a/iOSClient/Trash/NCTrash.swift b/iOSClient/Trash/NCTrash.swift index 8651d4a040..a899cf7ceb 100644 --- a/iOSClient/Trash/NCTrash.swift +++ b/iOSClient/Trash/NCTrash.swift @@ -28,26 +28,28 @@ import NextcloudKit import RealmSwift class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegate { - @IBOutlet weak var collectionView: UICollectionView! var filePath = "" var titleCurrentFolder = NSLocalizedString("_trash_view_", comment: "") var blinkFileId: String? var dataSourceTask: URLSessionTask? - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let utilityFileSystem = NCUtilityFileSystem() + let database = NCManageDatabase.shared let utility = NCUtility() var isEditMode = false var selectOcId: [String] = [] var tabBarSelect: NCTrashSelectTabBar! - var datasource: [tableTrash] = [] + var datasource: Results? var layoutForView: NCDBLayoutForView? var listLayout: NCListLayout! var gridLayout: NCGridLayout! var layoutKey = NCGlobal.shared.layoutViewTrash let refreshControl = UIRefreshControl() var filename: String? + var session: NCSession.Session { + NCSession.shared.getSession(controller: tabBarController) + } // MARK: - View Life Cycle @@ -81,9 +83,11 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + navigationController?.setNavigationBarAppearance() navigationItem.title = titleCurrentFolder - layoutForView = NCManageDatabase.shared.getLayoutForView(account: appDelegate.account, key: NCGlobal.shared.layoutViewTrash, serverUrl: "") + + layoutForView = self.database.getLayoutForView(account: session.account, key: NCGlobal.shared.layoutViewTrash, serverUrl: "") if layoutForView?.layout == NCGlobal.shared.layoutList { collectionView.collectionViewLayout = listLayout @@ -106,14 +110,6 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat dataSourceTask?.cancel() } - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - coordinator.animate(alongsideTransition: nil) { _ in - self.collectionView?.collectionViewLayout.invalidateLayout() - } - } - override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() @@ -126,8 +122,9 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat func setNavigationRightItems() { func createMenuActions() -> [UIMenuElement] { - guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: appDelegate.account, key: layoutKey, serverUrl: "") else { return [] } - let select = UIAction(title: NSLocalizedString("_select_", comment: ""), image: utility.loadImage(named: "checkmark.circle", colors: [NCBrandColor.shared.iconImageColor]), attributes: self.datasource.isEmpty ? .disabled : []) { _ in + guard let layoutForView = self.database.getLayoutForView(account: session.account, key: layoutKey, serverUrl: ""), + let datasource else { return [] } + let select = UIAction(title: NSLocalizedString("_select_", comment: ""), image: utility.loadImage(named: "checkmark.circle", colors: [NCBrandColor.shared.iconImageColor]), attributes: datasource.isEmpty ? .disabled : []) { _ in self.setEditMode(true) } let list = UIAction(title: NSLocalizedString("_list_", comment: ""), image: utility.loadImage(named: "list.bullet", colors: [NCBrandColor.shared.iconImageColor]), state: layoutForView.layout == NCGlobal.shared.layoutList ? .on : .off) { _ in @@ -162,7 +159,6 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat // MARK: TAP EVENT func tapRestoreListItem(with ocId: String, image: UIImage?, sender: Any) { - if !isEditMode { restoreItem(with: ocId) } else if let button = sender as? UIView { @@ -173,7 +169,6 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat } func tapMoreListItem(with objectId: String, image: UIImage?, sender: Any) { - if !isEditMode { toggleMenuMore(with: objectId, image: image, isGridCell: false) } else if let button = sender as? UIView { @@ -183,8 +178,7 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat } // else: undefined sender } - func tapMoreGridItem(with objectId: String, namedButtonMore: String, image: UIImage?, indexPath: IndexPath, sender: Any) { - + func tapMoreGridItem(with objectId: String, image: UIImage?, sender: Any) { if !isEditMode { toggleMenuMore(with: objectId, image: image, isGridCell: true) } else if let button = sender as? UIView { @@ -196,18 +190,17 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat func longPressGridItem(with objectId: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreGridItem(with objectId: String, namedButtonMore: String, gestureRecognizer: UILongPressGestureRecognizer) { } + func longPressMoreGridItem(with objectId: String, gestureRecognizer: UILongPressGestureRecognizer) { } // MARK: - DataSource @objc func reloadDataSource(withQueryDB: Bool = true) { - - datasource = NCManageDatabase.shared.getTrash(filePath: getFilePath(), account: appDelegate.account) + datasource = self.database.getResultsTrash(filePath: getFilePath(), account: session.account) collectionView.reloadData() setNavigationRightItems() - guard let blinkFileId = blinkFileId else { return } - for itemIx in 0.. String { if filePath.isEmpty { - guard let userId = (appDelegate.userId as NSString).addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlFragmentAllowed) else { return "" } - let filePath = appDelegate.urlBase + "/" + NextcloudKit.shared.nkCommonInstance.dav + "/trashbin/" + userId + "/trash" + guard let userId = (session.userId as NSString).addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlFragmentAllowed) else { return "" } + let filePath = session.urlBase + "/remote.php/dav/trashbin/" + userId + "/trash" return filePath + "/" } else { return filePath + "/" diff --git a/iOSClient/Trash/NCTrashSelectTabBar.swift b/iOSClient/Trash/NCTrashSelectTabBar.swift index 1c48a6e499..cca186bc70 100644 --- a/iOSClient/Trash/NCTrashSelectTabBar.swift +++ b/iOSClient/Trash/NCTrashSelectTabBar.swift @@ -20,6 +20,7 @@ // import Foundation +import UIKit import SwiftUI protocol NCTrashSelectTabBarDelegate: AnyObject { @@ -92,7 +93,6 @@ struct NCTrashSelectTabBarView: View { var body: some View { VStack { Spacer().frame(height: sizeClass == .compact ? 5 : 10) - HStack { Button { tabBarSelect.delegate?.recover() diff --git a/iOSClient/UserStatus/NCUserStatus.swift b/iOSClient/UserStatus/NCUserStatus.swift index a7e66f81c8..93d9d2bbe4 100644 --- a/iOSClient/UserStatus/NCUserStatus.swift +++ b/iOSClient/UserStatus/NCUserStatus.swift @@ -66,10 +66,10 @@ class NCUserStatus: UIViewController { private var statusPredefinedStatuses: [NKUserStatus] = [] private let utility = NCUtility() private var clearAtTimestamp: Double = 0 // Unix Timestamp representing the time to clear the status - private let borderWidthButton: CGFloat = 1.5 - private let borderColorButton: CGColor = NCBrandColor.shared.brandElement.cgColor - private let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! + private var borderColorButton: CGColor = NCBrandColor.shared.customer.cgColor + + public var account: String = "" // MARK: - View Life Cycle @@ -82,6 +82,7 @@ class NCUserStatus: UIViewController { view.backgroundColor = .systemBackground tableView.backgroundColor = .systemBackground + borderColorButton = NCBrandColor.shared.getElement(account: account).cgColor buttonCancel.image = utility.loadImage(named: "xmark", colors: [NCBrandColor.shared.iconImageColor]) onlineButton.layer.cornerRadius = 10 @@ -163,9 +164,9 @@ class NCUserStatus: UIViewController { setStatusMessageButton.layer.cornerRadius = 20 setStatusMessageButton.layer.masksToBounds = true - setStatusMessageButton.backgroundColor = NCBrandColor.shared.brandElement + setStatusMessageButton.backgroundColor = NCBrandColor.shared.getElement(account: account) setStatusMessageButton.setTitle(NSLocalizedString("_set_status_message_", comment: ""), for: .normal) - setStatusMessageButton.setTitleColor(NCBrandColor.shared.brandText, for: .normal) + setStatusMessageButton.setTitleColor(NCBrandColor.shared.getText(account: account), for: .normal) getStatus() } @@ -173,7 +174,7 @@ class NCUserStatus: UIViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - NextcloudKit.shared.getUserStatus(account: appDelegate.account) { account, clearAt, icon, message, messageId, messageIsPredefined, status, statusIsUserDefined, _, _, error in + NextcloudKit.shared.getUserStatus(account: account) { account, clearAt, icon, message, messageId, messageIsPredefined, status, statusIsUserDefined, _, _, error in if error == .success { NCManageDatabase.shared.setAccountUserStatus(userStatusClearAt: clearAt, userStatusIcon: icon, userStatusMessage: message, userStatusMessageId: messageId, userStatusMessageIsPredefined: messageIsPredefined, userStatusStatus: status, userStatusStatusIsUserDefined: statusIsUserDefined, account: account) } @@ -206,7 +207,7 @@ class NCUserStatus: UIViewController { self.invisibleButton.layer.borderWidth = 0 self.invisibleButton.layer.borderColor = nil - NextcloudKit.shared.setUserStatus(status: "online", account: appDelegate.account) { _, error in + NextcloudKit.shared.setUserStatus(status: "online", account: account) { _, _, error in self.dismissIfError(error) } } @@ -221,7 +222,7 @@ class NCUserStatus: UIViewController { self.invisibleButton.layer.borderWidth = 0 self.invisibleButton.layer.borderColor = nil - NextcloudKit.shared.setUserStatus(status: "away", account: appDelegate.account) { _, error in + NextcloudKit.shared.setUserStatus(status: "away", account: account) { _, _, error in self.dismissIfError(error) } } @@ -236,7 +237,7 @@ class NCUserStatus: UIViewController { self.invisibleButton.layer.borderWidth = 0 self.invisibleButton.layer.borderColor = nil - NextcloudKit.shared.setUserStatus(status: "dnd", account: appDelegate.account) { _, error in + NextcloudKit.shared.setUserStatus(status: "dnd", account: account) { _, _, error in self.dismissIfError(error) } } @@ -251,7 +252,7 @@ class NCUserStatus: UIViewController { self.invisibleButton.layer.borderWidth = self.borderWidthButton self.invisibleButton.layer.borderColor = self.borderColorButton - NextcloudKit.shared.setUserStatus(status: "invisible", account: appDelegate.account) { _, error in + NextcloudKit.shared.setUserStatus(status: "invisible", account: account) { _, _, error in self.dismissIfError(error) } } @@ -302,7 +303,7 @@ class NCUserStatus: UIViewController { } @IBAction func actionClearStatusMessage(_ sender: UIButton) { - NextcloudKit.shared.clearMessage(account: appDelegate.account) { _, error in + NextcloudKit.shared.clearMessage(account: account) { _, _, error in if error != .success { NCContentPresenter().showError(error: error) } @@ -314,7 +315,7 @@ class NCUserStatus: UIViewController { @IBAction func actionSetStatusMessage(_ sender: UIButton) { guard let message = statusMessageTextField.text else { return } - NextcloudKit.shared.setCustomMessageUserDefined(statusIcon: statusMessageEmojiTextField.text, message: message, clearAt: clearAtTimestamp, account: appDelegate.account) { _, error in + NextcloudKit.shared.setCustomMessageUserDefined(statusIcon: statusMessageEmojiTextField.text, message: message, clearAt: clearAtTimestamp, account: account) { _, _, error in if error != .success { NCContentPresenter().showError(error: error) } @@ -326,7 +327,7 @@ class NCUserStatus: UIViewController { // MARK: - Networking func getStatus() { - NextcloudKit.shared.getUserStatus(account: appDelegate.account) { account, clearAt, icon, message, _, _, status, _, _, _, error in + NextcloudKit.shared.getUserStatus(account: account) { account, clearAt, icon, message, _, _, status, _, _, _, error in if error == .success || error.errorCode == NCGlobal.shared.errorResourceNotFound { if icon != nil { @@ -518,7 +519,7 @@ extension NCUserStatus: UITableViewDelegate { let status = statusPredefinedStatuses[indexPath.row] if let messageId = status.id { - NextcloudKit.shared.setCustomMessagePredefined(messageId: messageId, clearAt: 0, account: appDelegate.account) { _, error in + NextcloudKit.shared.setCustomMessagePredefined(messageId: messageId, clearAt: 0, account: account) { _, _, error in cell.isSelected = false if error == .success { @@ -565,6 +566,7 @@ extension NCUserStatus: UITableViewDataSource { struct UserStatusView: UIViewControllerRepresentable { @Binding var showUserStatus: Bool + var account: String class Coordinator: NSObject { var parent: UserStatusView @@ -577,6 +579,8 @@ struct UserStatusView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UINavigationController { let storyboard = UIStoryboard(name: "NCUserStatus", bundle: nil) let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController + let viewController = navigationController!.topViewController as? NCUserStatus + viewController?.account = account return navigationController! } diff --git a/iOSClient/Utility/FileNameValidator+Extensions.swift b/iOSClient/Utility/FileNameValidator+Extensions.swift new file mode 100644 index 0000000000..0de7a271e7 --- /dev/null +++ b/iOSClient/Utility/FileNameValidator+Extensions.swift @@ -0,0 +1,28 @@ +// +// FileNameValidator+Extensions.swift +// Nextcloud +// +// Created by Milen Pivchev on 26.08.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Foundation +import NextcloudKit +import UIKit + +extension FileNameValidator { + private func setup(account: String?) { + let capabilities = NCCapabilities.shared.getCapabilities(account: account) + FileNameValidator.shared.setup(forbiddenFileNames: capabilities.capabilityForbiddenFileNames, forbiddenFileNameBasenames: capabilities.capabilityForbiddenFileNameBasenames, forbiddenFileNameCharacters: capabilities.capabilityForbiddenFileNameCharacters, forbiddenFileNameExtensions: capabilities.capabilityForbiddenFileNameExtensions) + } + + func checkFileName(_ filename: String, account: String?) -> NKError? { + setup(account: account) + return FileNameValidator.shared.checkFileName(filename) + } + + func checkFolderPath(_ folderPath: String, account: String?) -> Bool { + setup(account: account) + return FileNameValidator.shared.checkFolderPath(folderPath) + } +} diff --git a/iOSClient/Utility/NCAskAuthorization.swift b/iOSClient/Utility/NCAskAuthorization.swift index 9f5c7ffd7b..be5100a55e 100644 --- a/iOSClient/Utility/NCAskAuthorization.swift +++ b/iOSClient/Utility/NCAskAuthorization.swift @@ -63,7 +63,7 @@ class NCAskAuthorization: NSObject { } } - @objc func askAuthorizationPhotoLibrary(viewController: UIViewController?, completion: @escaping (_ hasPermission: Bool) -> Void) { + @objc func askAuthorizationPhotoLibrary(controller: UIViewController?, completion: @escaping (_ hasPermission: Bool) -> Void) { switch PHPhotoLibrary.authorizationStatus() { case PHAuthorizationStatus.authorized: @@ -80,7 +80,7 @@ class NCAskAuthorization: NSObject { completion(false) })) DispatchQueue.main.async { - viewController?.present(alert, animated: true, completion: nil) + controller?.present(alert, animated: true, completion: nil) } case PHAuthorizationStatus.notDetermined: isRequesting = true diff --git a/iOSClient/Utility/NCCameraRoll.swift b/iOSClient/Utility/NCCameraRoll.swift index 2a5f93d6c7..7b6dc0204b 100644 --- a/iOSClient/Utility/NCCameraRoll.swift +++ b/iOSClient/Utility/NCCameraRoll.swift @@ -23,26 +23,26 @@ import Foundation import Photos +import UIKit import NextcloudKit class NCCameraRoll: NSObject { - let utilityFileSystem = NCUtilityFileSystem() + let database = NCManageDatabase.shared func extractCameraRoll(from metadata: tableMetadata, completition: @escaping (_ metadatas: [tableMetadata]) -> Void) { - + var metadatas: [tableMetadata] = [] + let metadataSource = tableMetadata.init(value: metadata) var chunkSize = NCGlobal.shared.chunkSizeMBCellular if NCNetworking.shared.networkReachability == NKCommon.TypeReachability.reachableEthernetOrWiFi { chunkSize = NCGlobal.shared.chunkSizeMBEthernetOrWiFi } - var metadatas: [tableMetadata] = [] - let metadataSource = tableMetadata.init(value: metadata) - guard !metadata.isExtractFile else { return completition([metadataSource]) } + guard !metadataSource.assetLocalIdentifier.isEmpty else { let filePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadataSource.ocId, fileNameView: metadataSource.fileName) metadataSource.size = utilityFileSystem.getFileSize(filePath: filePath) - let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadataSource.fileNameView, mimeType: metadataSource.contentType, directory: false) + let results = NextcloudKit.shared.nkCommonInstance.getInternalType(fileName: metadataSource.fileNameView, mimeType: metadataSource.contentType, directory: false, account: metadataSource.account) metadataSource.contentType = results.mimeType metadataSource.iconName = results.iconName metadataSource.classFile = results.classFile @@ -59,11 +59,11 @@ class NCCameraRoll: NSObject { } metadataSource.e2eEncrypted = metadata.isDirectoryE2EE if metadataSource.chunk > 0 || metadataSource.e2eEncrypted { - metadataSource.session = NextcloudKit.shared.nkCommonInstance.sessionIdentifierUpload + metadataSource.session = NCNetworking.shared.sessionUpload } metadataSource.isExtractFile = true - if let metadata = NCManageDatabase.shared.addMetadata(metadataSource) { - metadatas.append(metadata) + if let metadata = self.database.addMetadata(metadataSource) { + metadatas.append(tableMetadata(value: metadata)) } return completition(metadatas) } @@ -76,8 +76,8 @@ class NCCameraRoll: NSObject { let fetchAssets = PHAsset.fetchAssets(withLocalIdentifiers: [metadataSource.assetLocalIdentifier], options: nil) if metadata.isLivePhoto, fetchAssets.count > 0 { self.createMetadataLivePhoto(metadata: metadata, asset: fetchAssets.firstObject) { metadata in - if let metadata = metadata, let metadata = NCManageDatabase.shared.addMetadata(metadata) { - metadatas.append(metadata) + if let metadata, let metadata = self.database.addMetadata(metadata) { + metadatas.append(tableMetadata(value: metadata)) } completition(metadatas) } @@ -123,11 +123,11 @@ class NCCameraRoll: NSObject { } metadata.e2eEncrypted = metadata.isDirectoryE2EE if metadata.chunk > 0 || metadata.e2eEncrypted { - metadata.session = NextcloudKit.shared.nkCommonInstance.sessionIdentifierUpload + metadata.session = NCNetworking.shared.sessionUpload } metadata.isExtractFile = true - if let metadata = NCManageDatabase.shared.addMetadata(metadata) { - metadataReturn = metadata + if let metadata = self.database.addMetadata(metadata) { + metadataReturn = tableMetadata(value: metadata) } } completion(metadataReturn, fileNamePath, error) @@ -259,18 +259,16 @@ class NCCameraRoll: NSObject { guard let videoResource = videoResource else { return completion(nil) } self.utilityFileSystem.removeFile(atPath: fileNamePath) PHAssetResourceManager.default().writeData(for: videoResource, toFile: URL(fileURLWithPath: fileNamePath), options: nil) { error in - if error != nil { return completion(nil) } - let metadataLivePhoto = NCManageDatabase.shared.createMetadata(account: metadata.account, - user: metadata.user, - userId: metadata.userId, - fileName: fileName, - fileNameView: fileName, - ocId: ocId, - serverUrl: metadata.serverUrl, - urlBase: metadata.urlBase, - url: "", - contentType: "") - + guard error == nil else { return completion(nil) } + let session = NCSession.shared.getSession(account: metadata.account) + let metadataLivePhoto = self.database.createMetadata(fileName: fileName, + fileNameView: fileName, + ocId: ocId, + serverUrl: metadata.serverUrl, + url: "", + contentType: "", + session: session, + sceneIdentifier: metadata.sceneIdentifier) metadataLivePhoto.livePhotoFile = metadata.fileName metadataLivePhoto.classFile = NKCommon.TypeClassFile.video.rawValue metadataLivePhoto.isExtractFile = true @@ -285,12 +283,16 @@ class NCCameraRoll: NSObject { } metadataLivePhoto.e2eEncrypted = metadata.isDirectoryE2EE if metadataLivePhoto.chunk > 0 || metadataLivePhoto.e2eEncrypted { - metadataLivePhoto.session = NextcloudKit.shared.nkCommonInstance.sessionIdentifierUpload + metadataLivePhoto.session = NCNetworking.shared.sessionUpload } metadataLivePhoto.creationDate = metadata.creationDate metadataLivePhoto.date = metadata.date metadataLivePhoto.uploadDate = metadata.uploadDate - return completion(NCManageDatabase.shared.addMetadata(metadataLivePhoto)) + if let metadata = self.database.addMetadata(metadataLivePhoto) { + let returnMetadata = tableMetadata(value: metadata) + return completion(returnMetadata) + } + completion(nil) } } } diff --git a/iOSClient/Utility/NCContentPresenter.swift b/iOSClient/Utility/NCContentPresenter.swift index 0e8475034c..cf2085e0a0 100644 --- a/iOSClient/Utility/NCContentPresenter.swift +++ b/iOSClient/Utility/NCContentPresenter.swift @@ -27,7 +27,6 @@ import CFNetwork import NextcloudKit class NCContentPresenter: NSObject { - typealias MainFont = Font.HelveticaNeue enum Font { enum HelveticaNeue: String { @@ -109,7 +108,7 @@ class NCContentPresenter: NSObject { switch error.errorCode { case Int(CFNetworkErrors.cfurlErrorNotConnectedToInternet.rawValue): let image = UIImage(named: "InfoNetwork")?.image(color: .white, size: 20) - self.noteTop(text: NSLocalizedString(title, comment: ""), image: image, color: .lightGray, delay: delay, priority: .max) + self.noteTop(text: NSLocalizedString("_network_not_available_", comment: ""), image: image, color: .lightGray, delay: delay, priority: .max) default: var responseMessage = "" if let data = error.responseData { @@ -287,7 +286,7 @@ class NCContentPresenter: NSObject { private func getBackgroundColorFromType(_ type: messageType) -> UIColor { switch type { case .info: - return NCBrandColor.shared.brandElement + return NCBrandColor.shared.customer case .error: return UIColor(red: 1, green: 0, blue: 0, alpha: 0.9) case .success: diff --git a/iOSClient/Utility/NCLivePhoto.swift b/iOSClient/Utility/NCLivePhoto.swift index d599f12744..4a37830793 100644 --- a/iOSClient/Utility/NCLivePhoto.swift +++ b/iOSClient/Utility/NCLivePhoto.swift @@ -12,6 +12,7 @@ import MobileCoreServices import Photos import NextcloudKit import UniformTypeIdentifiers +import Alamofire class NCLivePhoto { @@ -466,16 +467,16 @@ extension NCLivePhoto { func setLivephoto(serverUrlfileNamePath: String, livePhotoFile: String, account: String, - options: NKRequestOptions = NKRequestOptions()) async -> (account: String, error: NKError) { + options: NKRequestOptions = NKRequestOptions()) async -> (account: String, responseData: AFDataResponse?, error: NKError) { await withUnsafeContinuation({ continuation in - NextcloudKit.shared.setLivephoto(serverUrlfileNamePath: serverUrlfileNamePath, livePhotoFile: livePhotoFile, account: account, options: options) { account, error in - continuation.resume(returning: (account: account, error: error)) + NextcloudKit.shared.setLivephoto(serverUrlfileNamePath: serverUrlfileNamePath, livePhotoFile: livePhotoFile, account: account, options: options) { account, responseData, error in + continuation.resume(returning: (account: account, responseData: responseData,error: error)) } }) } func setLivephotoUpload(metadata: tableMetadata) { - guard NCGlobal.shared.capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion28 else { return } + guard NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion28 else { return } livePhotoFile = metadata.livePhotoFile livePhotoFile2 = metadata.fileName @@ -490,7 +491,12 @@ extension NCLivePhoto { guard metadata.isLivePhoto, !livePhotoFile.isEmpty, - let metadata2 = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND urlBase == %@ AND path == %@ AND fileName == %@ AND status == %d", metadata.account, metadata.urlBase, metadata.path, livePhotoFile, NCGlobal.shared.metadataStatusNormal)) else { return } + let metadata2 = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND urlBase == %@ AND path == %@ AND fileName == %@ AND status == %d", + metadata.account, + metadata.urlBase, + metadata.path, + livePhotoFile, + NCGlobal.shared.metadataStatusNormal)) else { return } let serverUrlfileNamePath1 = metadata.urlBase + metadata.path + metadata.fileName let serverUrlfileNamePath2 = metadata2.urlBase + metadata2.path + livePhotoFile @@ -505,7 +511,7 @@ extension NCLivePhoto { } func setLivePhoto(metadata1: tableMetadata, metadata2: tableMetadata) { - guard NCGlobal.shared.capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion28, + guard NCCapabilities.shared.getCapabilities(account: metadata1.account).capabilityServerVersionMajor >= NCGlobal.shared.nextcloudVersion28, (!metadata1.livePhotoFile.isEmpty && !metadata2.livePhotoFile.isEmpty) else { return } Task { diff --git a/iOSClient/Utility/NCUserBaseUrl.swift b/iOSClient/Utility/NCUserBaseUrl.swift deleted file mode 100644 index f92b2ae9f7..0000000000 --- a/iOSClient/Utility/NCUserBaseUrl.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// NCUserBaseUrl.swift -// Nextcloud -// -// Created by Henrik Storch on 22.11.21. -// Copyright © 2021 Henrik Storch. All rights reserved. -// -// Author Henrik Storch -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import Foundation - -@objc public protocol NCUserBaseUrl { - var user: String { get } - var urlBase: String { get } - var account: String { get } - var userId: String { get } -} - -public extension NCUserBaseUrl { - var userBaseUrl: String { - user + "-" + (URL(string: urlBase)?.host ?? "") - } - var userAccount: String { - user + " " + urlBase - } -} diff --git a/iOSClient/Utility/NCUtility+Image.swift b/iOSClient/Utility/NCUtility+Image.swift index daabdd9575..def503dea4 100644 --- a/iOSClient/Utility/NCUtility+Image.swift +++ b/iOSClient/Utility/NCUtility+Image.swift @@ -30,7 +30,7 @@ import Photos import SVGKit extension NCUtility { - func loadImage(named imageName: String, colors: [UIColor]? = nil, size: CGFloat? = nil, useTypeIconFile: Bool = false ) -> UIImage { + func loadImage(named imageName: String, colors: [UIColor]? = nil, size: CGFloat? = nil, useTypeIconFile: Bool = false, account: String? = nil) -> UIImage { var image: UIImage? if useTypeIconFile { @@ -38,7 +38,7 @@ extension NCUtility { case NKCommon.TypeIconFile.audio.rawValue: image = UIImage(systemName: "waveform", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) case NKCommon.TypeIconFile.code.rawValue: image = UIImage(systemName: "ellipsis.curlybraces", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) case NKCommon.TypeIconFile.compress.rawValue: image = UIImage(systemName: "doc.zipper", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) - case NKCommon.TypeIconFile.directory.rawValue: image = UIImage(named: "folder")! .image(color: NCBrandColor.shared.brandElement, size: UIScreen.main.bounds.width / 2) + case NKCommon.TypeIconFile.directory.rawValue: image = UIImage(named: "folder")! .image(color: NCBrandColor.shared.getElement(account: account), size: UIScreen.main.bounds.width / 2) case NKCommon.TypeIconFile.document.rawValue: image = UIImage(systemName: "doc.richtext", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.documentIconColor])) case NKCommon.TypeIconFile.image.rawValue: image = UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) case NKCommon.TypeIconFile.movie.rawValue: image = UIImage(systemName: "video", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) @@ -82,8 +82,8 @@ extension NCUtility { } } - func loadUserImage(for user: String, displayName: String?, userBaseUrl: NCUserBaseUrl) -> UIImage { - let fileName = userBaseUrl.userBaseUrl + "-" + user + ".png" + func loadUserImage(for user: String, displayName: String?, urlBase: String) -> UIImage { + let fileName = NCSession.shared.getFileName(urlBase: urlBase, user: user) let localFilePath = utilityFileSystem.directoryUserData + "/" + fileName if var localImage = UIImage(contentsOfFile: localFilePath) { @@ -94,8 +94,8 @@ extension NCUtility { localImage = UIGraphicsGetImageFromCurrentImageContext() ?? localImage UIGraphicsEndImageContext() return localImage - } else if let loadedAvatar = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) { - return loadedAvatar + } else if let image = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName).image { + return image } else if let displayName = displayName, !displayName.isEmpty, let avatarImg = createAvatar(displayName: displayName, size: 30) { return avatarImg } else { @@ -122,87 +122,64 @@ extension NCUtility { return UIImage(cgImage: thumbnailImageRef) } - func createImageFrom(fileNameView: String, ocId: String, etag: String, classFile: String) { - var originalImage, scaleImagePreview, scaleImageIcon: UIImage? - let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(ocId, fileNameView: fileNameView) - let fileNamePathPreview = utilityFileSystem.getDirectoryProviderStoragePreviewOcId(ocId, etag: etag) - let fileNamePathIcon = utilityFileSystem.getDirectoryProviderStorageIconOcId(ocId, etag: etag) - - if utilityFileSystem.fileProviderStorageSize(ocId, fileNameView: fileNameView) > 0 && FileManager().fileExists(atPath: fileNamePathPreview) && FileManager().fileExists(atPath: fileNamePathIcon) { return } - if classFile != NKCommon.TypeClassFile.image.rawValue && classFile != NKCommon.TypeClassFile.video.rawValue { return } - - if classFile == NKCommon.TypeClassFile.image.rawValue { - originalImage = UIImage(contentsOfFile: fileNamePath) - scaleImagePreview = originalImage?.resizeImage(size: CGSize(width: NCGlobal.shared.sizePreview, height: NCGlobal.shared.sizePreview)) - scaleImageIcon = originalImage?.resizeImage(size: CGSize(width: NCGlobal.shared.sizeIcon, height: NCGlobal.shared.sizeIcon)) - try? scaleImagePreview?.jpegData(compressionQuality: 0.7)?.write(to: URL(fileURLWithPath: fileNamePathPreview)) - try? scaleImageIcon?.jpegData(compressionQuality: 0.7)?.write(to: URL(fileURLWithPath: fileNamePathIcon)) - } else if classFile == NKCommon.TypeClassFile.video.rawValue { - let videoPath = NSTemporaryDirectory() + "tempvideo.mp4" - utilityFileSystem.linkItem(atPath: fileNamePath, toPath: videoPath) - originalImage = imageFromVideo(url: URL(fileURLWithPath: videoPath), at: 0) - try? originalImage?.jpegData(compressionQuality: 0.7)?.write(to: URL(fileURLWithPath: fileNamePathPreview)) - try? originalImage?.jpegData(compressionQuality: 0.7)?.write(to: URL(fileURLWithPath: fileNamePathIcon)) + func createImageFileFrom(metadata: tableMetadata) { + if metadata.classFile != NKCommon.TypeClassFile.image.rawValue, metadata.classFile != NKCommon.TypeClassFile.video.rawValue { return } + var image: UIImage? + let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) + + if image == nil { + if metadata.classFile == NKCommon.TypeClassFile.image.rawValue { + image = UIImage(contentsOfFile: fileNamePath) + } else if metadata.classFile == NKCommon.TypeClassFile.video.rawValue { + let videoPath = NSTemporaryDirectory() + "tempvideo.mp4" + utilityFileSystem.linkItem(atPath: fileNamePath, toPath: videoPath) + image = imageFromVideo(url: URL(fileURLWithPath: videoPath), at: 0) + } } - } - func getImageMetadata(_ metadata: tableMetadata, for size: CGFloat) -> UIImage? { - if let image = getImage(metadata: metadata) { return image } + guard let image else { return } - if metadata.isVideo && !metadata.hasPreview { - createImageFrom(fileNameView: metadata.fileNameView, ocId: metadata.ocId, etag: metadata.etag, classFile: metadata.classFile) - } - - if utilityFileSystem.fileProviderStoragePreviewIconExists(metadata.ocId, etag: metadata.etag) { - return UIImage(contentsOfFile: utilityFileSystem.getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag)) - } + createImageStandard(ocId: metadata.ocId, etag: metadata.etag, image: image) + } - if metadata.isVideo { - return loadImage(named: "video", colors: [NCBrandColor.shared.iconImageColor2]) - } else if metadata.isAudio { - return loadImage(named: "waveform", colors: [NCBrandColor.shared.iconImageColor2]) - } else { - return loadImage(named: "photo", colors: [NCBrandColor.shared.iconImageColor2]) - } + func createImageFileFrom(data: Data, metadata: tableMetadata) { + createImageFileFrom( data: data, ocId: metadata.ocId, etag: metadata.etag) } - func getImage(metadata: tableMetadata) -> UIImage? { - let ext = (metadata.fileNameView as NSString).pathExtension.uppercased() - var image: UIImage? + func createImageFileFrom(data: Data, ocId: String, etag: String) { + guard let image = UIImage(data: data) else { return } + let fileNamePath1024 = self.utilityFileSystem.getDirectoryProviderStorageImageOcId(ocId, etag: etag, ext: global.previewExt1024) - if utilityFileSystem.fileProviderStorageExists(metadata) && metadata.isImage { - let previewPath = utilityFileSystem.getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag) - let iconPath = utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag) - let imagePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) + do { + try data.write(to: URL(fileURLWithPath: fileNamePath1024), options: .atomic) + } catch { } - if ext == "GIF" { - if !FileManager().fileExists(atPath: previewPath) { - createImageFrom(fileNameView: metadata.fileNameView, ocId: metadata.ocId, etag: metadata.etag, classFile: metadata.classFile) - } - image = UIImage.animatedImage(withAnimatedGIFURL: URL(fileURLWithPath: imagePath)) - } else if ext == "SVG" { - if let svgImage = SVGKImage(contentsOfFile: imagePath) { - svgImage.size = CGSize(width: NCGlobal.shared.sizePreview, height: NCGlobal.shared.sizePreview) - if let image = svgImage.uiImage { - if !FileManager().fileExists(atPath: previewPath) { - do { - try image.pngData()?.write(to: URL(fileURLWithPath: previewPath), options: .atomic) - try image.pngData()?.write(to: URL(fileURLWithPath: iconPath), options: .atomic) - } catch { } - } - return image - } else { - return nil - } - } else { - return nil - } - } else { - createImageFrom(fileNameView: metadata.fileNameView, ocId: metadata.ocId, etag: metadata.etag, classFile: metadata.classFile) - image = UIImage(contentsOfFile: imagePath) + createImageStandard(ocId: ocId, etag: etag, image: image) + } + + private func createImageStandard(ocId: String, etag: String, image: UIImage) { + let ext = [global.previewExt1024, global.previewExt512, global.previewExt256] + let size = [global.size1024, global.size512, global.size256] + let compressionQuality = [0.5, 0.6, 0.7] + + for i in 0.. UIImage? { + return UIImage(contentsOfFile: self.utilityFileSystem.getDirectoryProviderStorageImageOcId(ocId, etag: etag, ext: ext)) + } + + func existsImage(ocId: String, etag: String, ext: String) -> Bool { + return FileManager().fileExists(atPath: self.utilityFileSystem.getDirectoryProviderStorageImageOcId(ocId, etag: etag, ext: ext)) } func imageFromVideo(url: URL, at time: TimeInterval, completion: @escaping (UIImage?) -> Void) { @@ -228,22 +205,6 @@ extension NCUtility { } } - func getIcon(metadata: tableMetadata) -> UIImage? { - let iconPath = self.utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag) - guard let icon = UIImage(contentsOfFile: iconPath) else { return nil } - - if Int(icon.size.width) > NCGlobal.shared.sizeIcon, Int(icon.size.height) > NCGlobal.shared.sizeIcon, - let iconResize = icon.resizeImage(size: CGSize(width: NCGlobal.shared.sizeIcon, height: NCGlobal.shared.sizeIcon)) { - do { - if let data = iconResize.jpegData(compressionQuality: 0.5) { - try data.write(to: URL(fileURLWithPath: iconPath), options: .atomic) - } - } catch { } - return iconResize - } - return icon - } - func pdfThumbnail(url: URL, width: CGFloat = 240) -> UIImage? { guard let data = try? Data(contentsOf: url), let page = PDFDocument(data: data)?.page(at: 0) else { return nil @@ -258,10 +219,38 @@ extension NCUtility { } func createAvatar(displayName: String, size: CGFloat) -> UIImage? { + func usernameToColor(_ username: String) -> CGColor { + // Normalize hash + let lowerUsername = username.lowercased() + var hash: String + // swiftlint:disable force_try + let regex = try! NSRegularExpression(pattern: "^([0-9a-f]{4}-?){8}$") + // swiftlint:enable force_try + let matches = regex.matches( + in: username, + range: NSRange(username.startIndex..., in: username)) + + if !matches.isEmpty { + // Already a md5 hash? + // done, use as is. + hash = lowerUsername + } else { + hash = lowerUsername.md5() + } + + hash = hash.replacingOccurrences(of: "[^0-9a-f]", with: "", options: .regularExpression) + + // userColors has 18 colors by default + let result = hash.compactMap(\.hexDigitValue) + let userColorIx = result.reduce(0, { $0 + $1 }) % 18 + + return NCBrandColor.shared.userColors[userColorIx] + } + guard let initials = displayName.uppercaseInitials else { return nil } - let userColor = NCGlobal.shared.usernameToColor(displayName) + let userColor = usernameToColor(displayName) let rect = CGRect(x: 0, y: 0, width: size, height: size) var avatarImage: UIImage? @@ -297,9 +286,9 @@ extension NCUtility { let imageNamePath = utilityFileSystem.directoryUserData + "/" + fileNamePNG if !FileManager.default.fileExists(atPath: imageNamePath) || rewrite == true { - NextcloudKit.shared.downloadContent(serverUrl: iconURL.absoluteString, account: account) { _, data, error in - if error == .success && data != nil { - if let image = UIImage(data: data!) { + NextcloudKit.shared.downloadContent(serverUrl: iconURL.absoluteString, account: account) { _, responseData, error in + if error == .success, let data = responseData?.data { + if let image = UIImage(data: data) { var newImage: UIImage = image if width != nil { @@ -350,23 +339,6 @@ extension NCUtility { } } - func getSizePreview(width: Int, height: Int) -> CGSize { - var widthPreview: Double = Double(NCGlobal.shared.sizePreview) - var heightPreview: Double = Double(NCGlobal.shared.sizePreview) - - if width > 0, height > 0 { - var ratio: Double = 0 - if width >= height { - ratio = Double(width) / Double(height) - heightPreview = widthPreview / ratio - } else { - ratio = Double(height) / Double(width) - widthPreview = heightPreview / ratio - } - } - return CGSize(width: widthPreview, height: heightPreview) - } - func getUserStatus(userIcon: String?, userStatus: String?, userMessage: String?) -> (statusImage: UIImage?, statusMessage: String, descriptionMessage: String) { var statusImage: UIImage? var statusMessage: String = "" @@ -405,4 +377,9 @@ extension NCUtility { return(statusImage, statusMessage, descriptionMessage) } + + func memorySizeOfImage(_ image: UIImage) -> Int { + guard let imageData = image.pngData() else { return 0 } + return imageData.count + } } diff --git a/iOSClient/Utility/NCUtility.swift b/iOSClient/Utility/NCUtility.swift index 280b23d47f..b5e70c8ddf 100644 --- a/iOSClient/Utility/NCUtility.swift +++ b/iOSClient/Utility/NCUtility.swift @@ -31,6 +31,7 @@ import Alamofire class NCUtility: NSObject { let utilityFileSystem = NCUtilityFileSystem() + let global = NCGlobal.shared func isSimulatorOrTestFlight() -> Bool { guard let path = Bundle.main.appStoreReceiptURL?.path else { @@ -52,14 +53,14 @@ class NCUtility: NSObject { guard !fileExtension.isEmpty else { return false } guard let mimeType = UTType(tag: fileExtension.uppercased(), tagClass: .filenameExtension, conformingTo: nil)?.identifier else { return false } /// contentype - if !NCGlobal.shared.capabilityRichDocumentsMimetypes.filter({ $0.contains(metadata.contentType) || $0.contains("text/plain") }).isEmpty { + if !NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityRichDocumentsMimetypes.filter({ $0.contains(metadata.contentType) || $0.contains("text/plain") }).isEmpty { return true } /// mimetype - if !NCGlobal.shared.capabilityRichDocumentsMimetypes.isEmpty && mimeType.components(separatedBy: ".").count > 2 { + if !NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityRichDocumentsMimetypes.isEmpty && mimeType.components(separatedBy: ".").count > 2 { let mimeTypeArray = mimeType.components(separatedBy: ".") let mimeType = mimeTypeArray[mimeTypeArray.count - 2] + "." + mimeTypeArray[mimeTypeArray.count - 1] - if !NCGlobal.shared.capabilityRichDocumentsMimetypes.filter({ $0.contains(mimeType) }).isEmpty { + if !NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityRichDocumentsMimetypes.filter({ $0.contains(mimeType) }).isEmpty { return true } } @@ -284,9 +285,19 @@ class NCUtility: NSObject { func removeForbiddenCharacters(_ fileName: String) -> String { var fileName = fileName - for character in NCGlobal.shared.forbiddenCharacters { + for character in global.forbiddenCharacters { fileName = fileName.replacingOccurrences(of: character, with: "") } return fileName } + + func getHeightHeaderEmptyData(view: UIView, portraitOffset: CGFloat, landscapeOffset: CGFloat, isHeaderMenuTransferViewEnabled: Bool = false) -> CGFloat { + var height: CGFloat = 0 + if UIDevice.current.orientation.isPortrait { + height = (view.frame.height / 2) - (view.safeAreaInsets.top / 2) + portraitOffset + } else { + height = (view.frame.height / 2) + landscapeOffset + CGFloat(isHeaderMenuTransferViewEnabled ? 35 : 0) + } + return height + } } diff --git a/iOSClient/Utility/NCUtilityFileSystem.swift b/iOSClient/Utility/NCUtilityFileSystem.swift index f5dbc2c00c..151f374269 100644 --- a/iOSClient/Utility/NCUtilityFileSystem.swift +++ b/iOSClient/Utility/NCUtilityFileSystem.swift @@ -94,12 +94,8 @@ class NCUtilityFileSystem: NSObject { return path } - func getDirectoryProviderStorageIconOcId(_ ocId: String, etag: String) -> String { - return getDirectoryProviderStorageOcId(ocId) + "/" + etag + NCGlobal.shared.storageExtIcon - } - - func getDirectoryProviderStoragePreviewOcId(_ ocId: String, etag: String) -> String { - return getDirectoryProviderStorageOcId(ocId) + "/" + etag + NCGlobal.shared.storageExtPreview + func getDirectoryProviderStorageImageOcId(_ ocId: String, etag: String, ext: String) -> String { + return getDirectoryProviderStorageOcId(ocId) + "/" + etag + ext } func fileProviderStorageExists(_ metadata: tableMetadata) -> Bool { @@ -133,15 +129,12 @@ class NCUtilityFileSystem: NSObject { return 0 } - func fileProviderStoragePreviewIconExists(_ ocId: String, etag: String) -> Bool { - let fileNamePathPreview = getDirectoryProviderStoragePreviewOcId(ocId, etag: etag) - let fileNamePathIcon = getDirectoryProviderStorageIconOcId(ocId, etag: etag) + func fileProviderStorageImageExists(_ ocId: String, etag: String, ext: String) -> Bool { + let fileNamePath = getDirectoryProviderStorageImageOcId(ocId, etag: etag, ext: ext) do { - let fileNamePathPreviewAttribute = try fileManager.attributesOfItem(atPath: fileNamePathPreview) - let fileSizePreview: UInt64 = fileNamePathPreviewAttribute[FileAttributeKey.size] as? UInt64 ?? 0 - let fileNamePathIconAttribute = try fileManager.attributesOfItem(atPath: fileNamePathIcon) - let fileSizeIcon: UInt64 = fileNamePathIconAttribute[FileAttributeKey.size] as? UInt64 ?? 0 - if fileSizePreview > 0 && fileSizeIcon > 0 { + let fileNamePathAttribute = try fileManager.attributesOfItem(atPath: fileNamePath) + let fileSize: UInt64 = fileNamePathAttribute[FileAttributeKey.size] as? UInt64 ?? 0 + if fileSize > 0 { return true } else { return false @@ -150,6 +143,15 @@ class NCUtilityFileSystem: NSObject { return false } + func fileProviderStorageImageExists(_ ocId: String, etag: String) -> Bool { + if fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt1024), + fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt512), + fileProviderStorageImageExists(ocId, etag: etag, ext: NCGlobal.shared.previewExt256) { + return true + } + return false + } + func createDirectoryStandard() { guard let directoryGroup = fileManager.containerURL(forSecurityApplicationGroupIdentifier: NCBrandOptions.shared.capabilitiesGroup)?.path else { return } if !fileManager.fileExists(atPath: directoryDocuments) { try? fileManager.createDirectory(atPath: directoryDocuments, withIntermediateDirectories: true) } @@ -202,17 +204,18 @@ class NCUtilityFileSystem: NSObject { } catch { print("Error: \(error)") } } - func isDirectoryE2EE(serverUrl: String, userBase: NCUserBaseUrl) -> Bool { - return isDirectoryE2EE(account: userBase.account, urlBase: userBase.urlBase, userId: userBase.userId, serverUrl: serverUrl) + func isDirectoryE2EE(serverUrl: String, account: String) -> Bool { + return isDirectoryE2EE(session: NCSession.shared.getSession(account: account), serverUrl: serverUrl) } func isDirectoryE2EE(file: NKFile) -> Bool { - return isDirectoryE2EE(account: file.account, urlBase: file.urlBase, userId: file.userId, serverUrl: file.serverUrl) + let session = NCSession.Session(account: file.account, urlBase: file.urlBase, user: file.user, userId: file.userId) + return isDirectoryE2EE(session: session, serverUrl: file.serverUrl) } - func isDirectoryE2EE(account: String, urlBase: String, userId: String, serverUrl: String) -> Bool { - if serverUrl == getHomeServer(urlBase: urlBase, userId: userId) || serverUrl == ".." { return false } - if let directory = NCManageDatabase.shared.getTableDirectory(account: account, serverUrl: serverUrl) { + func isDirectoryE2EE(session: NCSession.Session, serverUrl: String) -> Bool { + if serverUrl == getHomeServer(session: session) || serverUrl == ".." { return false } + if let directory = NCManageDatabase.shared.getTableDirectory(account: session.account, serverUrl: serverUrl) { return directory.e2eEncrypted } return false @@ -380,8 +383,8 @@ class NCUtilityFileSystem: NSObject { // MARK: - - func getHomeServer(urlBase: String, userId: String) -> String { - return urlBase + "/remote.php/dav/files/" + userId + func getHomeServer(session: NCSession.Session) -> String { + return session.urlBase + "/remote.php/dav/files/" + session.userId } func getPath(path: String, user: String, fileName: String? = nil) -> String { @@ -418,8 +421,8 @@ class NCUtilityFileSystem: NSObject { } } - func getFileNamePath(_ fileName: String, serverUrl: String, urlBase: String, userId: String) -> String { - let home = getHomeServer(urlBase: urlBase, userId: userId) + func getFileNamePath(_ fileName: String, serverUrl: String, session: NCSession.Session) -> String { + let home = getHomeServer(session: session) var fileNamePath = serverUrl.replacingOccurrences(of: home, with: "") + "/" + fileName if fileNamePath.first == "/" { fileNamePath.removeFirst() @@ -638,7 +641,7 @@ class NCUtilityFileSystem: NSObject { try manager.removeItem(atPath: fileURL.path) } catch { } manager.createFile(atPath: fileURL.path, contents: nil, attributes: nil) - NCManageDatabase.shared.deleteLocalFile(predicate: NSPredicate(format: "ocId == %@", ocId)) + NCManageDatabase.shared.deleteLocalFileOcId(ocId) } } } diff --git a/iOSClient/Utility/PKCS12.swift b/iOSClient/Utility/PKCS12.swift index 0c241ddeda..d78b2dc173 100644 --- a/iOSClient/Utility/PKCS12.swift +++ b/iOSClient/Utility/PKCS12.swift @@ -20,6 +20,7 @@ // import Foundation +import UIKit typealias UserCertificate = (data: Data, password: String) diff --git a/iOSClient/Utility/ParallelWorker.swift b/iOSClient/Utility/ParallelWorker.swift index 376e822abd..264b5509d2 100644 --- a/iOSClient/Utility/ParallelWorker.swift +++ b/iOSClient/Utility/ParallelWorker.swift @@ -21,8 +21,8 @@ // along with this program. If not, see . // +import Foundation import UIKit -import JGProgressHUD /// Object to execute multiple tasks in parallel like uploading or downloading. /// - Can display a progress indicator with status message @@ -32,7 +32,7 @@ class ParallelWorker { let queue = DispatchQueue(label: "ParallelWorker") let semaphore: DispatchSemaphore let titleKey: String - var hud: JGProgressHUD? + var hud = NCHud() var totalTasks: Int? var completedTasks = 0 var isCancelled = false @@ -43,35 +43,17 @@ class ParallelWorker { /// - titleKey: Localized String key, used for the status. Default: *Please Wait...* /// - totalTasks: Number of total tasks, if known /// - hudView: The parent view or current view which should present the progress indicator. If `nil`, no progress indicator will be shown. - init(n: Int, titleKey: String?, totalTasks: Int?, hudView: UIView?) { + init(n: Int, titleKey: String?, totalTasks: Int?, controller: NCMainTabBarController?) { semaphore = DispatchSemaphore(value: n) self.totalTasks = totalTasks self.titleKey = titleKey ?? "_wait_" - guard let hudView = hudView else { return } - DispatchQueue.main.async { - let hud = JGProgressHUD() - hud.indicatorView = JGProgressHUDRingIndicatorView() - if let indicatorView = hud.indicatorView as? JGProgressHUDRingIndicatorView { - indicatorView.ringWidth = 1.5 - indicatorView.ringColor = NCBrandColor.shared.brandElement - } - hud.textLabel.text = NSLocalizedString(self.titleKey, comment: "") - hud.detailTextLabel.text = NSLocalizedString("_tap_to_cancel_", comment: "") - hud.detailTextLabel.textColor = NCBrandColor.shared.iconImageColor2 - hud.show(in: hudView) - hud.tapOnHUDViewBlock = { hud in - self.isCancelled = true - // Cancel all download / upload - for uploadRequest in NCNetworking.shared.uploadRequest { - uploadRequest.value.cancel() - } - for downloadRequest in NCNetworking.shared.downloadRequest { - downloadRequest.value.cancel() - } - hud.dismiss() - } - self.hud = hud + hud.initHudRing(view: controller?.view, + text: NSLocalizedString(self.titleKey, comment: ""), + tapToCancelDetailText: true) { + self.isCancelled = true + NCNetworking.shared.cancelUploadTasks() + NCNetworking.shared.cancelDownloadTasks() } } @@ -84,9 +66,7 @@ class ParallelWorker { guard !self.isCancelled else { return self.completionGroup.leave() } task { self.completedTasks += 1 - DispatchQueue.main.async { - self.hud?.textLabel.text = "\(NSLocalizedString(self.titleKey, comment: ""))" - } + self.hud.setText(text: "\(NSLocalizedString(self.titleKey, comment: ""))") self.semaphore.signal() self.completionGroup.leave() } @@ -98,7 +78,7 @@ class ParallelWorker { func completeWork(completion: (() -> Void)? = nil) { completionGroup.notify(queue: .main) { guard !self.isCancelled else { return } - self.hud?.dismiss() + self.hud.dismiss() completion?() } } diff --git a/iOSClient/Utility/ScreenAwakeManager/AwakeMode.swift b/iOSClient/Utility/ScreenAwakeManager/AwakeMode.swift new file mode 100644 index 0000000000..08490e0af7 --- /dev/null +++ b/iOSClient/Utility/ScreenAwakeManager/AwakeMode.swift @@ -0,0 +1,34 @@ +// +// AwakeMode.swift +// Nextcloud +// +// Created by Milen Pivchev on 18.09.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +import Foundation + +/** + Modes: + + - `disabled`: Nothing will change (disabled functionality). + - `always`: Device will never timeout and lock. + - `whenCharging`: Device will stay active as long as it's connected to charger. + + */ + enum AwakeMode: CaseIterable, Identifiable { + /** + Nothing will change (disabled functionality). + */ + case off + /** + Device will never timeout and lock. + */ + case on + /** + Device will stay active as long as it's connected to charger. + */ + case whileCharging + + var id: Self { self } + } diff --git a/iOSClient/Utility/ScreenAwakeManager/ScreenAwakeManager.swift b/iOSClient/Utility/ScreenAwakeManager/ScreenAwakeManager.swift new file mode 100644 index 0000000000..43af39ecf6 --- /dev/null +++ b/iOSClient/Utility/ScreenAwakeManager/ScreenAwakeManager.swift @@ -0,0 +1,90 @@ +// +// ScreenAwakeManager.swift +// Nextcloud +// +// Created by Milen Pivchev on 18.09.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// + +// Modified from https://github.com/ochococo/Insomnia +import UIKit + +/** + + Sometimes you want your iPhone to stay active a little bit longer is it an import or just game interface. + + This simple class aims to simplify the code and give you a well tested solution. + + */ +class ScreenAwakeManager { + static let shared: ScreenAwakeManager = { + let instance = ScreenAwakeManager() + return instance + }() + + /** + This mode will change the behavior: + + - `disabled`: Nothing will change (disabled functionality). + - `always`: Your iOS device will never timeout and lock. + - `whenCharging`: Device will stay active as long as it's connected to charger. + + */ + var mode: AwakeMode = .off { + didSet { + updateMode() + } + } + + private unowned let device = UIDevice.current + private unowned let notificationCenter = NotificationCenter.default + private unowned let application = UIApplication.shared + + private init() {} + + private func startMonitoring() { + device.isBatteryMonitoringEnabled = true + notificationCenter.addObserver(self, + selector: #selector(batteryStateDidChange), + name: UIDevice.batteryStateDidChangeNotification, object: nil) + } + + private func stopMonitoring() { + notificationCenter.removeObserver(self) + device.isBatteryMonitoringEnabled = false + } + + @objc private func batteryStateDidChange(notification: NSNotification) { + updateMode() + } + + private func updateMode() { + DispatchQueue.main.async { [self] in + switch mode { + case .whileCharging: + startMonitoring() + application.isIdleTimerDisabled = isPlugged + case .on: + stopMonitoring() + application.isIdleTimerDisabled = true + case .off: + stopMonitoring() + application.isIdleTimerDisabled = false + } + } + } + + private var isPlugged: Bool { + switch device.batteryState { + case .unknown, .unplugged: + return false + default: + return true + } + } + + deinit { + stopMonitoring() + application.isIdleTimerDisabled = false + } +} diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h new file mode 100644 index 0000000000..6000f1fca6 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h @@ -0,0 +1,57 @@ +// +// TOPasscodeCircleImage.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A subclass of `UIImage` that can procedurally generate both hollow and full circle graphics at any size. + These are used for the 'normal' and 'tapped' states of the passcode circle buttons. + */ +@interface TOPasscodeCircleImage : UIImage + +/** + Generates and returns a `UIImage` of a filled circle at the specified size. + + @param size The diameter of the final circle image + @param inset An inset value that will shrink the size of the circle. This is so it can be overlaid on a hollow circle + without interfering with the anti-aliasing on the outer border. + @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. + Setting this value will increase the dimensions of the final `UIImage`. + @param antialias Whether the circle boundary will be antialiased (Since antialiasing is unnecessary if this circle will overlay another.) + */ ++ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias; + +/** + Generates and returns a `UIImage` of a hollow circle at the specified size. + + @param size The diameter of the final circle image + @param strokeWidth The thickness, in points, of the stroke making up the circle image. + @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. + Setting this value will increase the dimensions of the final `UIImage`. + */ ++ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m new file mode 100644 index 0000000000..4288a33092 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m @@ -0,0 +1,75 @@ +// +// TOPasscodeCircleImage.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeCircleImage.h" + +@implementation TOPasscodeCircleImage + ++ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias +{ + UIImage *image = nil; + CGSize imageSize = (CGSize){size + (padding * 2), size + (padding * 2)}; + + UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0f); + { + CGContextRef context = UIGraphicsGetCurrentContext(); + + if (!antialias) { + CGContextSetShouldAntialias(context, NO); + } + + CGRect rect = (CGRect){padding + inset, padding + inset, size - (inset * 2), size - (inset * 2)}; + UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect:rect]; + [[UIColor blackColor] setFill]; + [ovalPath fill]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + ++ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding +{ + UIImage *image = nil; + CGSize canvasSize = (CGSize){size + (padding * 2), size + (padding * 2)}; + CGSize circleSize = (CGSize){size, size}; + + UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); + { + CGRect circleRect = (CGRect){{padding, padding}, circleSize}; + circleRect = CGRectInset(circleRect, (strokeWidth * 0.5f), (strokeWidth * 0.5f)); + + UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect]; + [[UIColor blackColor] setStroke]; + path.lineWidth = strokeWidth; + [path stroke]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h new file mode 100644 index 0000000000..97191fd6ae --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h @@ -0,0 +1,100 @@ +// +// TOPasscodeViewContentLayout.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import + +/** + Depending on the width of the application window, all of the content views in + the passcode view need to be resized in order to fit in the available space. + + This means that not only does the spacing and sizing of views need to be changed, but + image assets need to be regenerated and font sizes need to change as well. + + This class assumes there will be three major screen sizes, and provides layout + sizes, spacing, and styles in order to resize the passcode view for each one. + + The three screen styles it supports are: + + * Small Screens - iPhone 5/ or iPad 9.7" in 1/4 split screen mode + * Medium Screens - iPhone 6/ or iPad 12.9" in 1/4 split screen mode + * Large Screens - iPhone 6 Plus and all iPads when not in split screen mode. + + */ +@interface TOPasscodeViewContentLayout : NSObject + +/* The width of the PIN view in which this layout object is sizing the content to fit. */ +@property (nonatomic, assign) CGFloat viewWidth; + +/* Extra padding at the bottom in order to shift the content slightly up */ +@property (nonatomic, assign) CGFloat bottomPadding; + +/* The title view at the very top */ +@property (nonatomic, assign) CGFloat titleViewBottomSpacing; // Space from the bottom of the title view to the title label + +/* The Title Label Explaining the Passcode View */ +@property (nonatomic, assign) CGFloat titleLabelBottomSpacing; // Space from the title label to the input view +@property (nonatomic, assign) CGFloat subtitleLabelBottomSpacing; // Space from the subtitle label to the input view + +@property (nonatomic, strong) UIFont *titleLabelFont; // The font of the title label +@property (nonatomic, strong) UIFont *subtitleLabelFont; // The font of the subtitle label + +/* Title Label properties when the view is laid out horizontally */ +@property (nonatomic, assign) CGFloat titleHorizontalLayoutWidth; // When laid out horizontally, the width of the title view +@property (nonatomic, assign) CGFloat titleHorizontalLayoutSpacing; // The amount of spacing between the title label and the passcode keypad +@property (nonatomic, assign) CGFloat titleViewHorizontalBottomSpacing; // Space from the bottom of the title view when iPhone is horizontal +@property (nonatomic, assign) CGFloat titleLabelHorizontalBottomSpacing; // Spacing from the title label to input view in horizontal mode +@property (nonatomic, assign) CGFloat subtitleLabelHorizontalBottomSpacing; // Spacing from the subtitle label to input view in horizontal mode + +/* Circle Row Configuration */ +@property (nonatomic, assign) CGFloat circleRowDiameter; // The diameter of each circle representing a PIN number +@property (nonatomic, assign) CGFloat circleRowSpacing; // The spacing between each circle +@property (nonatomic, assign) CGFloat circleRowBottomSpacing; // Space between the view used to indicate input + +/* Text Field Configuration */ +@property (nonatomic, assign) CGFloat textFieldBorderThickness; // The thickness of the border stroke +@property (nonatomic, assign) CGFloat textFieldBorderRadius; // The corner radius of the border +@property (nonatomic, assign) CGFloat textFieldCircleDiameter; // The size of the circles in the passcode field +@property (nonatomic, assign) CGFloat textFieldCircleSpacing; // The amount of spacing between each circle +@property (nonatomic, assign) CGSize textFieldBorderPadding; // The amount of padding between the circles and the border +@property (nonatomic, assign) NSInteger textFieldNumericCharacterLength; // The amount of circles to have in this field when set to numeric +@property (nonatomic, assign) NSInteger textFieldAlphanumericCharacterLength; // The amount of circles to have in this field when set to alphanumeric +@property (nonatomic, assign) CGFloat submitButtonFontSize; // The font size of the 'OK' button +@property (nonatomic, assign) CGFloat submitButtonSpacing; // The spacing of the 'OK' button from the input + +/* Circle Button Shape and Layout */ +@property (nonatomic, assign) CGFloat circleButtonDiameter; // The size of each PIN button +@property (nonatomic, assign) CGSize circleButtonSpacing; // The vertical/horizontal spacing between buttons +@property (nonatomic, assign) CGFloat circleButtonStrokeWidth; // The thickness of the border line + +/* Circle Button Label */ +@property (nonatomic, strong) UIFont *circleButtonTitleLabelFont; // The font used for the '1' number labels +@property (nonatomic, strong) UIFont *circleButtonLetteringLabelFont; // The font used for the 'ABC' labels +@property (nonatomic, assign) CGFloat circleButtonLabelSpacing; // The vertical spacing between the number and lettering labels +@property (nonatomic, assign) CGFloat circleButtonLetteringSpacing; // The spacing between the 'ABC' characters + +/* Default layout configurations for the various sizes */ ++ (TOPasscodeViewContentLayout *)defaultScreenContentLayout; /* Default layout values. Designed for iPhone 6 Plus and above. */ ++ (TOPasscodeViewContentLayout *)mediumScreenContentLayout; /* For medium screen sizes, like iPhone 6, or 1/4 view on iPad Pro. */ ++ (TOPasscodeViewContentLayout *)smallScreenContentLayout; /* For the smallest screens, like iPhone SE, and 1/4 on standard size iPads/ */ + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m new file mode 100644 index 0000000000..5e8029133c --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m @@ -0,0 +1,205 @@ +// +// TOPasscodeViewContentLayout.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeViewContentLayout.h" + +@implementation TOPasscodeViewContentLayout + ++ (TOPasscodeViewContentLayout *)defaultScreenContentLayout +{ + TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; + + /* Width of the PIN View */ + contentLayout.viewWidth = 414.0f; + + /* Bottom Padding */ + contentLayout.bottomPadding = 25.0f; + + /* Title View Constraints */ + contentLayout.titleViewBottomSpacing = 34.0f; + + /* The Title Label Explaining the PIN View */ + contentLayout.titleLabelBottomSpacing = 34.0f; + contentLayout.titleLabelFont = [UIFont systemFontOfSize: 22.0f]; + + /* Horizontal title constraints */ + contentLayout.titleHorizontalLayoutWidth = 250.0f; + contentLayout.titleHorizontalLayoutSpacing = 35.0f; + contentLayout.titleViewHorizontalBottomSpacing = 20.0f; + contentLayout.titleLabelHorizontalBottomSpacing = 20.0f; + + /* Circle Row Configuration */ + contentLayout.circleRowDiameter = 15.5f; + contentLayout.circleRowSpacing = 30.0f; + contentLayout.circleRowBottomSpacing = 35.0f; + + /* The Subtitle Label */ + contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 17.0f]; + contentLayout.subtitleLabelBottomSpacing = 40.0f; + contentLayout.subtitleLabelHorizontalBottomSpacing = 40.0f; + + /* Text Field Input Configuration */ + contentLayout.textFieldBorderThickness = 1.5f; + contentLayout.textFieldBorderRadius = 5.0f; + contentLayout.textFieldCircleDiameter = 10.0f; + contentLayout.textFieldCircleSpacing = 6.0f; + contentLayout.textFieldBorderPadding = (CGSize){10, 10}; + contentLayout.textFieldNumericCharacterLength = 10; + contentLayout.textFieldAlphanumericCharacterLength = 15; + contentLayout.submitButtonFontSize = 17.0f; + contentLayout.submitButtonSpacing = 4.0f; + + /* Circle Button Shape and Layout */ + contentLayout.circleButtonDiameter = 90.0f; + contentLayout.circleButtonSpacing = (CGSize){25.0f, 16.0f}; + contentLayout.circleButtonStrokeWidth = 1.5f; + + /* Circle Button Label */ + contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; + contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; + contentLayout.circleButtonLabelSpacing = 6.0f; + contentLayout.circleButtonLetteringSpacing = 3.0f; + + return contentLayout; +} + ++ (TOPasscodeViewContentLayout *)mediumScreenContentLayout +{ + TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; + + /* Width of the PIN View */ + contentLayout.viewWidth = 375.0f; + + /* Bottom Padding */ + contentLayout.bottomPadding = 17.0f; + + /* Title View Constraints */ + contentLayout.titleViewBottomSpacing = 20.0f; + + /* The Title Label Explaining the PIN View */ + contentLayout.titleLabelFont = [UIFont systemFontOfSize: 19.0f]; + contentLayout.titleLabelBottomSpacing = 23.0f; + + /* Horizontal title constraints */ + contentLayout.titleHorizontalLayoutWidth = 185.0f; + contentLayout.titleHorizontalLayoutSpacing = 16.0f; + contentLayout.titleViewHorizontalBottomSpacing = 18.0f; + contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; + + /* Circle Row Configuration */ + contentLayout.circleRowDiameter = 13.5f; + contentLayout.circleRowSpacing = 26.0f; + contentLayout.circleRowBottomSpacing = 21.0f; + + /* The Subtitle Label */ + contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 14.0f]; + contentLayout.subtitleLabelHorizontalBottomSpacing = 20.0f; + contentLayout.subtitleLabelBottomSpacing = 20.0f; + + /* Submit Button */ + contentLayout.submitButtonFontSize = 16.0f; + contentLayout.submitButtonSpacing = 4.0f; + + /* Circle Button Shape and Layout */ + contentLayout.circleButtonDiameter = 80.0f; + contentLayout.circleButtonSpacing = (CGSize){28.0f, 15.0f}; + contentLayout.circleButtonStrokeWidth = 1.5f; + + /* Text Field Input Configuration */ + contentLayout.textFieldBorderThickness = 1.5f; + contentLayout.textFieldBorderRadius = 5.0f; + contentLayout.textFieldCircleDiameter = 9.0f; + contentLayout.textFieldCircleSpacing = 5.0f; + contentLayout.textFieldBorderPadding = (CGSize){10, 10}; + contentLayout.textFieldNumericCharacterLength = 10; + contentLayout.textFieldAlphanumericCharacterLength = 15; + + /* Circle Button Label */ + contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:36.5f weight:UIFontWeightThin]; + contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:8.5f weight:UIFontWeightThin]; + contentLayout.circleButtonLabelSpacing = 5.0f; + contentLayout.circleButtonLetteringSpacing = 2.5f; + + return contentLayout; +} + ++ (TOPasscodeViewContentLayout *)smallScreenContentLayout +{ + TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; + + /* Width of the PIN View */ + contentLayout.viewWidth = 320.0f; + + /* Bottom Padding */ + contentLayout.bottomPadding = 12.0f; + + /* Title View Constraints */ + contentLayout.titleViewBottomSpacing = 15.0f; + + /* The Title Label Explaining the PIN View */ + contentLayout.titleLabelFont = [UIFont systemFontOfSize: 16.0f]; + contentLayout.titleLabelBottomSpacing = 19.0f; + + /* Horizontal title constraints */ + contentLayout.titleHorizontalLayoutWidth = 185.0f; + contentLayout.titleHorizontalLayoutSpacing = 5.0f; + contentLayout.titleViewHorizontalBottomSpacing = 18.0f; + contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; + + /* Circle Row Configuration */ + contentLayout.circleRowDiameter = 12.5f; + contentLayout.circleRowSpacing = 22.0f; + contentLayout.circleRowBottomSpacing = 19.0f; + + /* The Subtitle Label */ + contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 12.0f]; + contentLayout.subtitleLabelHorizontalBottomSpacing = 22.0f; + contentLayout.subtitleLabelBottomSpacing = 19.0f; + + /* Text Field Input Configuration */ + contentLayout.textFieldBorderThickness = 1.5f; + contentLayout.textFieldBorderRadius = 5.0f; + contentLayout.textFieldCircleDiameter = 8.0f; + contentLayout.textFieldCircleSpacing = 4.0f; + contentLayout.textFieldBorderPadding = (CGSize){8, 8}; + contentLayout.textFieldNumericCharacterLength = 10; + contentLayout.textFieldAlphanumericCharacterLength = 15; + + /* Submit Button */ + contentLayout.submitButtonFontSize = 15.0f; + contentLayout.submitButtonSpacing = 3.0f; + + /* Circle Button Shape and Layout */ + contentLayout.circleButtonDiameter = 70.0f; + contentLayout.circleButtonSpacing = (CGSize){20.0f, 8.5f}; + contentLayout.circleButtonStrokeWidth = 1.5f; + + /* Circle Button Label */ + contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:35.0f weight:UIFontWeightThin]; + contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; + contentLayout.circleButtonLabelSpacing = 4.5f; + contentLayout.circleButtonLetteringSpacing = 2.0f; + + return contentLayout; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h new file mode 100644 index 0000000000..23147a4481 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h @@ -0,0 +1,60 @@ +// +// TOPasscodeViewControllerAnimatedTransitioning.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import + +@class TOPasscodeViewController; + +NS_ASSUME_NONNULL_BEGIN + +/** + An class conforming to `UIViewControllerAnimatedTransitioning` that handles the custom animation + that plays when the passcode view controller is presented on the user's screen. + */ +@interface TOPasscodeViewControllerAnimatedTransitioning : NSObject + +/** The parent passcode view controller that this object will be controlling */ +@property (nonatomic, weak, readonly) TOPasscodeViewController *passcodeViewController; + +/** Whether the controller is being presented or dismissed. The animation is played in reverse when dismissing. */ +@property (nonatomic, assign) BOOL dismissing; + +/** If the correct passcode was successfully entered, this property can be set to YES. When the view controller + is dismissing, the keypad view will also play a zooming out animation to give added context to the dismissal. */ +@property (nonatomic, assign) BOOL success; + +/** + Creates a new instanc of `TOPasscodeViewControllerAnimatedTransitioning` that will control the provided passcode + view controller. + + @param passcodeViewController The passcode view controller in which this object will coordinate the animation upon. + @param dismissing Whether the animation is played to present the view controller, or dismiss it. + @param success Whether the object needs to play an additional zooming animation denoting the passcode was successfully entered. + */ +- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController + dismissing:(BOOL)dismissing + success:(BOOL)success; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m new file mode 100644 index 0000000000..c43ad12e8f --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m @@ -0,0 +1,120 @@ +// +// TOPasscodeViewControllerAnimatedTransitioning.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeViewControllerAnimatedTransitioning.h" +#import "TOPasscodeViewController.h" +#import "TOPasscodeView.h" + +@interface TOPasscodeViewControllerAnimatedTransitioning () +@property (nonatomic, weak) TOPasscodeViewController *passcodeViewController; +@end + +@implementation TOPasscodeViewControllerAnimatedTransitioning + +- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController dismissing:(BOOL)dismissing success:(BOOL)success +{ + if (self = [super init]) { + _passcodeViewController = passcodeViewController; + _dismissing = dismissing; + _success = success; + } + + return self; +} + +- (NSTimeInterval)transitionDuration:(nullable id )transitionContext +{ + return 0.35f; +} + +- (void)animateTransition:(id )transitionContext +{ + BOOL isPhone = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone; + UIView *containerView = transitionContext.containerView; + UIVisualEffectView *backgroundEffectView = self.passcodeViewController.backgroundEffectView; + UIView *backgroundView = self.passcodeViewController.backgroundView; + UIVisualEffect *backgroundEffect = backgroundEffectView.effect; + TOPasscodeView *passcodeView = self.passcodeViewController.passcodeView; + + // Set the initial properties when presenting + if (!self.dismissing) { + backgroundEffectView.effect = nil; + backgroundView.alpha = 0.0f; + + self.passcodeViewController.view.frame = containerView.bounds; + [containerView addSubview:self.passcodeViewController.view]; + } + else { + UIViewController *baseController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; + if (baseController.view.superview == nil) { + [containerView insertSubview:baseController.view atIndex:0]; + } + } + + CGFloat alpha = self.dismissing ? 1.0f : 0.0f; + passcodeView.contentAlpha = alpha; + + // Animate the accessory views + if (isPhone) { + self.passcodeViewController.leftAccessoryButton.alpha = alpha; + self.passcodeViewController.rightAccessoryButton.alpha = alpha; + self.passcodeViewController.cancelButton.alpha = alpha; + self.passcodeViewController.biometricButton.alpha = alpha; + } + + id animationBlock = ^{ + backgroundEffectView.effect = self.dismissing ? nil : backgroundEffect; + backgroundView.alpha = self.dismissing ? 0.0f : 1.0f; + + CGFloat toAlpha = self.dismissing ? 0.0f : 1.0f; + passcodeView.contentAlpha = toAlpha; + if (isPhone) { + self.passcodeViewController.leftAccessoryButton.alpha = toAlpha; + self.passcodeViewController.rightAccessoryButton.alpha = toAlpha; + self.passcodeViewController.cancelButton.alpha = toAlpha; + self.passcodeViewController.biometricButton.alpha = toAlpha; + } + }; + + id completedBlock = ^(BOOL completed) { + backgroundEffectView.effect = backgroundEffect; + [transitionContext completeTransition:completed]; + }; + + // If we're animating out from a successful passcode, play a zooming out animation + // to give some more context + if (self.success && self.dismissing) { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; + animation.duration = [self transitionDuration:transitionContext]; + animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9f, 0.9f, 1)]; + animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + [passcodeView.layer addAnimation:animation forKey:@"transform"]; + } + + [UIView animateWithDuration:[self transitionDuration:transitionContext] + delay:0.0f + options:UIViewAnimationOptionAllowUserInteraction + animations:animationBlock + completion:completedBlock]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h new file mode 100644 index 0000000000..51d2828aae --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h @@ -0,0 +1,54 @@ +// +// TOSettingsKeypadImage.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A subclass of `UIImage` that procedurally generates images for `TOPasscodeSettingsKeypadView`. + This includes background images for each keypad button, and a delete icon for the bottom right corner + of the keypad. + */ +@interface TOSettingsKeypadImage : UIImage + +/** + Generates and returns an image of button background with a raised border in a pseudo-skeuomorphic style. + + @param radius The rounded radius of the button image's corners + @param foregroundColor The fill color of the primary section of the button + @param edgeColor The color of the raised border edge along the bottom. + @param thickness The size of the border running along the bottom + */ ++ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius + foregroundColor:(UIColor *)foregroundColor + edgeColor:(UIColor *)edgeColor + edgeThickness:(CGFloat)thickness; + +/** +Generates and returns a tintable delete icon. + */ ++ (UIImage *)deleteIcon; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m new file mode 100644 index 0000000000..b92b7afb31 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m @@ -0,0 +1,186 @@ +// +// TOSettingsKeypadImage.m +// TOPasscodeViewControllerExample +// +// Created by Tim Oliver on 6/20/17. +// Copyright © 2017 Timothy Oliver. All rights reserved. +// + +#import "TOSettingsKeypadImage.h" + +#define TOP_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + Y * limitedRadius) +#define TOP_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + Y * limitedRadius) +#define BOTTOM_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) +#define BOTTOM_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) + +@implementation TOSettingsKeypadImage + ++ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius + foregroundColor:(UIColor *)foregroundColor + edgeColor:(UIColor *)edgeColor + edgeThickness:(CGFloat)thickness +{ + CGFloat width = (radius * 2.0f) + 1.0f; + CGFloat height = width + thickness; + + CGRect frame = (CGRect){CGPointZero, {width, height}}; + + UIImage *image = nil; + UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); + { + CGContextRef context = UIGraphicsGetCurrentContext(); + + NSShadow* shadow = [[NSShadow alloc] init]; + shadow.shadowColor = edgeColor; + shadow.shadowOffset = CGSizeMake(0, thickness); + shadow.shadowBlurRadius = 0; + + CGRect buttonFrame = frame; + buttonFrame.size.height -= thickness; + + CGContextSaveGState(context); + { + CGContextSetShadowWithColor(context, shadow.shadowOffset, shadow.shadowBlurRadius, [shadow.shadowColor CGColor]); + UIBezierPath *buttonPath = [[self class] bezierPathWithContinuousRoundedRect:buttonFrame cornerRadius:radius];//bezierPathWithRoundedRect:buttonFrame cornerRadius:radius]; + [foregroundColor setFill]; + [buttonPath fill]; + } + CGContextRestoreGState(context); + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + UIEdgeInsets insets = UIEdgeInsetsMake(radius, radius, radius + thickness, radius); + image = [image resizableImageWithCapInsets:insets]; + + return image; +} + ++ (UIImage *)deleteIcon +{ + UIImage *image = nil; + + CGRect frame = CGRectMake(0, 0, 40.0f, 21.0f); + UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); + { + //// DeleteIcon + { + //// Border Drawing + UIBezierPath* borderPath = [UIBezierPath bezierPath]; + [borderPath moveToPoint: CGPointMake(25.73, 1.5)]; + [borderPath addLineToPoint: CGPointMake(25.9, 1.53)]; + [borderPath addCurveToPoint: CGPointMake(28.34, 3.46) controlPoint1: CGPointMake(27.03, 1.86) controlPoint2: CGPointMake(27.93, 2.56)]; + [borderPath addCurveToPoint: CGPointMake(28.67, 6.56) controlPoint1: CGPointMake(28.67, 4.28) controlPoint2: CGPointMake(28.67, 5.04)]; + [borderPath addLineToPoint: CGPointMake(28.64, 14.23)]; + [borderPath addCurveToPoint: CGPointMake(28.35, 17.05) controlPoint1: CGPointMake(28.64, 15.76) controlPoint2: CGPointMake(28.64, 16.37)]; + [borderPath addLineToPoint: CGPointMake(28.31, 17.19)]; + [borderPath addCurveToPoint: CGPointMake(25.86, 19.11) controlPoint1: CGPointMake(27.89, 18.08) controlPoint2: CGPointMake(27, 18.79)]; + [borderPath addCurveToPoint: CGPointMake(21.4, 19.37) controlPoint1: CGPointMake(24.82, 19.37) controlPoint2: CGPointMake(23.34, 19.37)]; + [borderPath addLineToPoint: CGPointMake(11.51, 19.37)]; + [borderPath addCurveToPoint: CGPointMake(9.9, 19.07) controlPoint1: CGPointMake(11.51, 19.37) controlPoint2: CGPointMake(10.41, 19.3)]; + [borderPath addCurveToPoint: CGPointMake(7.38, 17.06) controlPoint1: CGPointMake(9.09, 18.68) controlPoint2: CGPointMake(8.52, 18.14)]; + [borderPath addLineToPoint: CGPointMake(3.92, 13.81)]; + [borderPath addCurveToPoint: CGPointMake(1.87, 11.55) controlPoint1: CGPointMake(2.78, 12.73) controlPoint2: CGPointMake(2.21, 12.19)]; + [borderPath addLineToPoint: CGPointMake(1.79, 11.43)]; + [borderPath addCurveToPoint: CGPointMake(1.82, 9.06) controlPoint1: CGPointMake(1.36, 10.57) controlPoint2: CGPointMake(1.4, 9.92)]; + [borderPath addCurveToPoint: CGPointMake(3.96, 6.68) controlPoint1: CGPointMake(2.25, 8.29) controlPoint2: CGPointMake(2.82, 7.76)]; + [borderPath addLineToPoint: CGPointMake(7.21, 3.61)]; + [borderPath addCurveToPoint: CGPointMake(9.61, 1.67) controlPoint1: CGPointMake(8.35, 2.54) controlPoint2: CGPointMake(8.92, 2)]; + [borderPath addLineToPoint: CGPointMake(9.73, 1.6)]; + [borderPath addCurveToPoint: CGPointMake(11.41, 1.31) controlPoint1: CGPointMake(10.26, 1.37) controlPoint2: CGPointMake(10.84, 1.27)]; + [borderPath addLineToPoint: CGPointMake(21.44, 1.27)]; + [borderPath addCurveToPoint: CGPointMake(25.73, 1.5) controlPoint1: CGPointMake(23.38, 1.27) controlPoint2: CGPointMake(24.85, 1.27)]; + [borderPath closePath]; + [UIColor.blackColor setStroke]; + borderPath.lineWidth = 2.5; + [borderPath stroke]; + + + //// Cross Drawing + UIBezierPath* crossPath = [UIBezierPath bezierPath]; + [crossPath moveToPoint: CGPointMake(15.22, 5.9)]; + [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(15.27, 5.95) controlPoint2: CGPointMake(15.21, 5.88)]; + [crossPath addLineToPoint: CGPointMake(15.22, 5.9)]; + [crossPath closePath]; + [crossPath moveToPoint: CGPointMake(16.18, 10.28)]; + [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(16.22, 10.29) controlPoint2: CGPointMake(16.2, 10.28)]; + [crossPath addLineToPoint: CGPointMake(16.18, 10.28)]; + [crossPath closePath]; + [crossPath moveToPoint: CGPointMake(14.52, 5.35)]; + [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(14.75, 5.46) controlPoint2: CGPointMake(14.93, 5.62)]; + [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.26, 5.94) controlPoint2: CGPointMake(15.32, 5.99)]; + [crossPath addCurveToPoint: CGPointMake(15.43, 6.09) controlPoint1: CGPointMake(15.42, 6.09) controlPoint2: CGPointMake(15.43, 6.09)]; + [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.21, 5.88) controlPoint2: CGPointMake(15.27, 5.95)]; + [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(15.94, 6.59) controlPoint2: CGPointMake(17.66, 8.25)]; + [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(17.91, 8.61) controlPoint2: CGPointMake(17.94, 8.58)]; + [crossPath addCurveToPoint: CGPointMake(21.36, 5.39) controlPoint1: CGPointMake(20.95, 5.68) controlPoint2: CGPointMake(21.14, 5.5)]; + [crossPath addCurveToPoint: CGPointMake(22.67, 5.58) controlPoint1: CGPointMake(21.83, 5.17) controlPoint2: CGPointMake(22.34, 5.26)]; + [crossPath addCurveToPoint: CGPointMake(22.98, 6.89) controlPoint1: CGPointMake(23.09, 5.99) controlPoint2: CGPointMake(23.18, 6.47)]; + [crossPath addCurveToPoint: CGPointMake(22.28, 7.68) controlPoint1: CGPointMake(22.84, 7.14) controlPoint2: CGPointMake(22.65, 7.32)]; + [crossPath addCurveToPoint: CGPointMake(19.68, 10.19) controlPoint1: CGPointMake(22.28, 7.68) controlPoint2: CGPointMake(20.88, 9.03)]; + [crossPath addCurveToPoint: CGPointMake(22.97, 13.47) controlPoint1: CGPointMake(22.66, 13.06) controlPoint2: CGPointMake(22.85, 13.25)]; + [crossPath addCurveToPoint: CGPointMake(22.76, 14.79) controlPoint1: CGPointMake(23.21, 13.95) controlPoint2: CGPointMake(23.11, 14.46)]; + [crossPath addCurveToPoint: CGPointMake(21.35, 15.1) controlPoint1: CGPointMake(22.33, 15.22) controlPoint2: CGPointMake(21.8, 15.31)]; + [crossPath addCurveToPoint: CGPointMake(20.48, 14.4) controlPoint1: CGPointMake(21.07, 14.97) controlPoint2: CGPointMake(20.87, 14.78)]; + [crossPath addCurveToPoint: CGPointMake(17.89, 11.91) controlPoint1: CGPointMake(20.48, 14.4) controlPoint2: CGPointMake(19.08, 13.05)]; + [crossPath addCurveToPoint: CGPointMake(14.5, 15.06) controlPoint1: CGPointMake(14.91, 14.78) controlPoint2: CGPointMake(14.73, 14.95)]; + [crossPath addCurveToPoint: CGPointMake(13.2, 14.87) controlPoint1: CGPointMake(14.04, 15.28) controlPoint2: CGPointMake(13.53, 15.19)]; + [crossPath addCurveToPoint: CGPointMake(12.89, 13.57) controlPoint1: CGPointMake(12.78, 14.47) controlPoint2: CGPointMake(12.69, 13.98)]; + [crossPath addCurveToPoint: CGPointMake(13.42, 12.93) controlPoint1: CGPointMake(13, 13.35) controlPoint2: CGPointMake(13.15, 13.19)]; + [crossPath addCurveToPoint: CGPointMake(13.59, 12.77) controlPoint1: CGPointMake(13.47, 12.88) controlPoint2: CGPointMake(13.53, 12.83)]; + [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(14.12, 12.25) controlPoint2: CGPointMake(15.78, 10.66)]; + [crossPath addCurveToPoint: CGPointMake(12.89, 6.98) controlPoint1: CGPointMake(13.21, 7.39) controlPoint2: CGPointMake(13.01, 7.2)]; + [crossPath addCurveToPoint: CGPointMake(12.77, 6.63) controlPoint1: CGPointMake(12.82, 6.84) controlPoint2: CGPointMake(12.79, 6.73)]; + [crossPath addCurveToPoint: CGPointMake(13.1, 5.66) controlPoint1: CGPointMake(12.72, 6.28) controlPoint2: CGPointMake(12.83, 5.92)]; + [crossPath addCurveToPoint: CGPointMake(14.52, 5.35) controlPoint1: CGPointMake(13.54, 5.24) controlPoint2: CGPointMake(14.07, 5.15)]; + [crossPath closePath]; + [UIColor.blackColor setFill]; + [crossPath fill]; + } + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +/** + Creates a bezier path with the iOS 7 squircle shape. + + A HUGE thanks to the folks at PaintCode for open-sourcing this + https://www.paintcodeapp.com/news/code-for-ios-7-rounded-rectangles + */ ++ (UIBezierPath *)bezierPathWithContinuousRoundedRect:(CGRect)rect cornerRadius:(CGFloat)radius +{ + UIBezierPath* path = UIBezierPath.bezierPath; + CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483; + CGFloat limitedRadius = MIN(radius, limit); + + [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)]; + [path addLineToPoint: TOP_RIGHT(1.52866471, 0.00000000)]; + [path addCurveToPoint: TOP_RIGHT(0.66993427, 0.06549600) controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)]; + [path addLineToPoint: TOP_RIGHT(0.63149399, 0.07491100)]; + [path addCurveToPoint: TOP_RIGHT(0.07491176, 0.63149399) controlPoint1: TOP_RIGHT(0.37282392, 0.16905899) controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)]; + [path addCurveToPoint: TOP_RIGHT(0.00000000, 1.52866483) controlPoint1: TOP_RIGHT(0.00000000, 0.86840701) controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)]; + [path addLineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)]; + [path addCurveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493) controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323) controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)]; + [path addLineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)]; + [path addCurveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111) controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392) controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)]; + [path addCurveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000) controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000) controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)]; + [path addLineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)]; + [path addCurveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569) controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000) controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)]; + [path addLineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)]; + [path addCurveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399) controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883) controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)]; + [path addCurveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471) controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689) controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)]; + [path addLineToPoint: TOP_LEFT(0.00000000, 1.52866483)]; + [path addCurveToPoint: TOP_LEFT(0.06549600, 0.66993397) controlPoint1: TOP_LEFT(0.00000000, 1.08849299) controlPoint2: TOP_LEFT(0.00000000, 0.86840701)]; + [path addLineToPoint: TOP_LEFT(0.07491100, 0.63149399)]; + [path addCurveToPoint: TOP_LEFT(0.63149399, 0.07491100) controlPoint1: TOP_LEFT(0.16906001, 0.37282401) controlPoint2: TOP_LEFT(0.37282401, 0.16906001)]; + [path addCurveToPoint: TOP_LEFT(1.52866483, 0.00000000) controlPoint1: TOP_LEFT(0.86840701, 0.00000000) controlPoint2: TOP_LEFT(1.08849299, 0.00000000)]; + [path closePath]; + return path; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h b/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h new file mode 100644 index 0000000000..3447370059 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h @@ -0,0 +1,71 @@ +// +// TOPasscodeViewControllerConstants.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* The visual style of the asscode view controller */ +typedef NS_ENUM(NSInteger, TOPasscodeViewStyle) { + TOPasscodeViewStyleTranslucentDark, + TOPasscodeViewStyleTranslucentLight, + TOPasscodeViewStyleOpaqueDark, + TOPasscodeViewStyleOpaqueLight +}; + +/* The visual style of the passcode settings view controller. */ +typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewStyle) { + TOPasscodeSettingsViewStyleLight, + TOPasscodeSettingsViewStyleDark +}; + +/* Depending on the amount of horizontal space, the sizing of the elements */ +typedef NS_ENUM(NSInteger, TOPasscodeViewContentSize) { + TOPasscodeViewContentSizeDefault = 414, // Default, 414 points and above (6 Plus, all remaining iPad sizes) + TOPasscodeViewContentSizeMedium = 375, // Greater or equal to 375 points: iPhone 6 / iPad Pro 1/4 split mode + TOPasscodeViewContentSizeSmall = 320 // Greater or equal to 320 points: iPhone SE / iPad 1/4 split mode +}; + +/* The types of passcodes that may be used. */ +typedef NS_ENUM(NSInteger, TOPasscodeType) { + TOPasscodeTypeFourDigits, // 4 Numbers + TOPasscodeTypeSixDigits, // 6 Numbers + TOPasscodeTypeCustomNumeric, // Any length of numbers + TOPasscodeTypeCustomAlphanumeric // Any length of characters +}; + +/* The type of biometrics this controller can handle */ +typedef NS_ENUM(NSInteger, TOPasscodeBiometryType) { + TOPasscodeBiometryTypeTouchID, + TOPasscodeBiometryTypeFaceID +}; + +static inline BOOL TOPasscodeViewStyleIsTranslucent(TOPasscodeViewStyle style) { + return style <= TOPasscodeViewStyleTranslucentLight; +} + +static inline BOOL TOPasscodeViewStyleIsDark(TOPasscodeViewStyle style) { + return style < TOPasscodeViewStyleTranslucentLight || style == TOPasscodeViewStyleOpaqueDark; +} + +static inline NSString *TOPasscodeBiometryTitleForType(TOPasscodeBiometryType type) { + switch (type) { + case TOPasscodeBiometryTypeFaceID: return NSLocalizedString(@"Face ID", @""); + default: return NSLocalizedString(@"Touch ID", @""); + } +} diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h new file mode 100644 index 0000000000..656e617dc7 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h @@ -0,0 +1,108 @@ +// +// TOPasscodeSettingsViewController.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" + +@class TOPasscodeSettingsViewController; + +typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewState) { + TOPasscodeSettingsViewStateEnterCurrentPasscode, + TOPasscodeSettingsViewStateEnterNewPasscode, + TOPasscodeSettingsViewStateConfirmNewPasscode +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + A delegate object in charge of validating and recording the passcodes entered by the user. + */ +@protocol TOPasscodeSettingsViewControllerDelegate + +@optional + +/** Called when the user was prompted to input their current passcode. + Return YES if passcode was right and NO otherwise. + + Returning NO will cause a warning label to appear + */ +- (BOOL)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController + didAttemptCurrentPasscode:(NSString *)passcode; + +/** Called when the user has successfully set a new passcode. At this point, you should save over + the old passcode with the new one. */ +- (void)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController + didChangeToNewPasscode:(NSString *)passcode ofType:(TOPasscodeType)type; + +@end + +// ---------------------------------------------------------------------- + +/** + A standard system-styled view controller that users can use to change the passcode + that they will need to enter for the main passcode view controller. + + This controller allows requiring the user to enter their previous passcode in first, + and has passcode validation by requiring them to enter the new passcode twice. + */ + +@interface TOPasscodeSettingsViewController : UIViewController + +/** Delegate event for controlling and responding to the behavior of this controller */ +@property (nonatomic, weak, nullable) id delegate; + +/** The current state of the controller (confirming old passcode or creating a new one) */ +@property (nonatomic, assign) TOPasscodeSettingsViewState state; + +/** The input type of the passcode */ +@property (nonatomic, assign) TOPasscodeType passcodeType; + +/** The number of incorrect passcode attempts the user has made. Use this property to decide when to disable input. */ +@property (nonatomic, assign) NSInteger failedPasscodeAttemptCount; + +/** Before setting a new passcode, show a UI to validate the existing passcode. (Default is NO) */ +@property (nonatomic, assign) BOOL requireCurrentPasscode; + +/** If set, the view controller will disable input until this date time has been reached */ +@property (nonatomic, strong, nullable) NSDate *disabledInputDate; + +/** Hide the button Options (Default is NO) */ +@property (nonatomic, assign) BOOL hideOptionsButton; + +/* + Create a new instance with the desird light or dark style + + @param style The visual style of the view controller + */ +- (instancetype)init; + +/* + Changes the passcode type and animates if required + + @param passcodeType Change the type of passcode to enter. + @param animated Play a crossfade animation. + */ +- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m new file mode 100644 index 0000000000..0b2d62d2be --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m @@ -0,0 +1,630 @@ +// +// TOPasscodeSettingsViewController.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsViewController.h" +#import "TOPasscodeInputField.h" +#import "TOPasscodeSettingsKeypadView.h" +#import "TOPasscodeSettingsWarningLabel.h" + +const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15.0f; +const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15.0f; +const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0.40f; +const CGFloat kTOPasscodeKeypadMinHeight = 185.0f; // was 165 +const CGFloat kTOPasscodeKeypadMaxHeight = 330.0f; + +@interface TOPasscodeSettingsViewController () + +@property (nonatomic, copy) NSString *potentialPasscode; + +/* Layout Calculations */ +@property (nonatomic, assign) CGFloat verticalMidPoint; +@property (nonatomic, assign) CGRect keyboardFrame; +@property (nonatomic, readonly) CGRect contentOverlapFrame; // Either the keypad or the system keyboard + +/* Views */ +@property (nonatomic, strong) UIView *containerView; +@property (nonatomic, strong) UILabel *titleLabel; +@property (nonatomic, strong) UILabel *errorLabel; +@property (nonatomic, strong) UIButton *optionsButton; +@property (nonatomic, strong) TOPasscodeInputField *inputField; +@property (nonatomic, strong) TOPasscodeSettingsKeypadView *keypadView; +@property (nonatomic, strong) TOPasscodeSettingsWarningLabel *warningLabel; + +/* Bar Items */ +@property (nonatomic, strong) UIBarButtonItem *nextBarButtonItem; +@property (nonatomic, strong) UIBarButtonItem *doneBarButtonItem; + +/* Style */ +@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; + +@end + +@implementation TOPasscodeSettingsViewController + +#pragma mark - Object Creation - + +- (instancetype)init +{ + if (self = [self initWithNibName:nil bundle:nil]) { + [self setUp]; + } + + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + if (@available(iOS 13.0, *)) { + if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { + self.style = TOPasscodeSettingsViewStyleDark; + } else { + self.style = TOPasscodeSettingsViewStyleLight; + } + } else { + self.style = TOPasscodeSettingsViewStyleLight; + } + + [self applyThemeForStyle:_style]; + + _failedPasscodeAttemptCount = 0; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +#pragma mark - View Set-up - + +- (void)viewDidLoad { + [super viewDidLoad]; + + __weak typeof(self) weakSelf = self; + + self.title = NSLocalizedString(@"Enter Passcode", @""); + + // Create container view + self.containerView = [[UIView alloc] initWithFrame:CGRectZero]; + self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin + | UIViewAutoresizingFlexibleBottomMargin; + [self.view addSubview:self.containerView]; + + // Create title label + self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.titleLabel.font = [UIFont systemFontOfSize:17.0f]; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.textColor = [UIColor blackColor]; + self.titleLabel.text = @"Enter your passcode"; + self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [self.titleLabel sizeToFit]; + [self.containerView addSubview:self.titleLabel]; + + // Create number view + self.inputField = [[TOPasscodeInputField alloc] init]; + self.inputField.tintColor = [UIColor blackColor]; + self.inputField.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { [weakSelf inputViewDidCompletePasscode:passcode]; }; + [self.inputField sizeToFit]; + [self.containerView addSubview:self.inputField]; + + // Create keypad view + self.keypadView = [[TOPasscodeSettingsKeypadView alloc] initWithFrame:CGRectZero]; + self.keypadView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; + [self.view addSubview:self.keypadView]; + + // Create warning label view + self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc] initWithFrame:CGRectZero]; + self.warningLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + self.warningLabel.hidden = YES; + [self.warningLabel sizeToFit]; + [self.containerView addSubview:self.warningLabel]; + + // Create error label view + self.errorLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.errorLabel.text = NSLocalizedString(@"Passcodes didn't match. Try again.", @""); + self.errorLabel.textAlignment = NSTextAlignmentCenter; + self.errorLabel.font = [UIFont systemFontOfSize:15.0f]; + self.errorLabel.numberOfLines = 0; + self.errorLabel.hidden = YES; + [self.errorLabel sizeToFit]; + [self.containerView addSubview:self.errorLabel]; + + // Create Options button + self.optionsButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.optionsButton setTitle:NSLocalizedString(@"Passcode Options", @"") forState:UIControlStateNormal]; + self.optionsButton.titleLabel.font = [UIFont systemFontOfSize:15.0f]; + [self.optionsButton sizeToFit]; + self.optionsButton.hidden = _hideOptionsButton; + [self.optionsButton addTarget:self action:@selector(optionsCodeButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.view addSubview:self.optionsButton]; + + // Add callbacks for the keypad view + self.keypadView.numberButtonTappedHandler = ^(NSInteger number) { + NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; + [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; + }; + + self.keypadView.deleteButtonTappedHandler = ^{ [weakSelf.inputField deletePasscodeCharactersOfCount:1 animated:NO]; }; + + // Set height of the container view (This will never change) + CGRect frame = self.containerView.frame; + frame.size.width = self.view.bounds.size.width; + frame.size.height = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) + + CGRectGetHeight(self.warningLabel.frame) + (kTOPasscodeSettingsLabelInputSpacing * 2.0f); + self.containerView.frame = CGRectIntegral(frame); + + //Work out the vertical offset of the container view assuming the warning label doesn't count + self.verticalMidPoint = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) + + kTOPasscodeSettingsLabelInputSpacing; + self.verticalMidPoint *= 0.5f; + + // Bar button items + self.nextBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Next", @"") style:UIBarButtonItemStylePlain target:self action:@selector(nextButtonTapped:)]; + self.doneBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped:)]; + + // Apply light/dark mode + [self applyThemeForStyle:self.style]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPasscode : TOPasscodeSettingsViewStateEnterNewPasscode; + [self updateContentForState:self.state type:self.passcodeType animated:NO]; +} + +#pragma mark - View Update - + +- (void)updateContentForState:(TOPasscodeSettingsViewState)state type:(TOPasscodeType)type animated:(BOOL)animated +{ + BOOL variableSizePasscode = (type >= TOPasscodeTypeCustomNumeric); + + // Update the visibility of the options button + if (_hideOptionsButton) { + self.optionsButton.hidden = YES; + } else { + self.optionsButton.hidden = !(state == TOPasscodeSettingsViewStateEnterNewPasscode); + } + + // Clear the input view + self.inputField.passcode = nil; + + // Disable the input view + self.inputField.enabled = NO; + + //Update the warning label + [self updateWarningLabelForState:state]; + + // Change the input view if needed + if (!variableSizePasscode) { + self.inputField.style = TOPasscodeInputFieldStyleFixed; + self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; + } + else { + self.inputField.style = TOPasscodeInputFieldStyleVariable; + } + + // Update text depending on state + switch (state) { + case TOPasscodeSettingsViewStateEnterCurrentPasscode: + self.titleLabel.text = NSLocalizedString(@"Enter your passcode", @""); + self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; + if (@available(iOS 9.0, *)) { + self.inputField.returnKeyType = UIReturnKeyContinue; + } + else { + self.inputField.returnKeyType = UIReturnKeyNext; + } + break; + case TOPasscodeSettingsViewStateEnterNewPasscode: + self.titleLabel.text = NSLocalizedString(@"Enter a new passcode", @""); + self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; + if (@available(iOS 9.0, *)) { + self.inputField.returnKeyType = UIReturnKeyContinue; + } + else { + self.inputField.returnKeyType = UIReturnKeyNext; + } + break; + case TOPasscodeSettingsViewStateConfirmNewPasscode: + self.titleLabel.text = NSLocalizedString(@"Confirm new passcode", @""); + self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.doneBarButtonItem : nil; + self.inputField.returnKeyType = UIReturnKeyDone; + break; + } + + CGRect frame = CGRectZero; + + // Reload the 'Done' button + [self.inputField reloadInputViews]; + + // Resize text label to fit new text + [self.titleLabel sizeToFit]; + frame = self.titleLabel.frame; + frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; + self.titleLabel.frame = CGRectIntegral(frame); + + // Resize passcode view + [self.inputField sizeToFit]; + frame = self.inputField.frame; + frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; + self.inputField.frame = CGRectIntegral(frame); + + // If we're the alphanumeric type, present the keyboard + if (type == TOPasscodeTypeCustomAlphanumeric) { + self.inputField.enabled = YES; + [self.inputField becomeFirstResponder]; + } + else { + if (self.inputField.isFirstResponder) { + [self.inputField resignFirstResponder]; + } + } + + // If not animated, force a blanket re-layout + if (!animated) { + [self viewDidLayoutSubviews]; + return; + } + + // If animated, perform the animation + [UIView animateWithDuration:0.3f animations:^{ + [self viewDidLayoutSubviews]; + }]; +} + +- (void)updateWarningLabelForState:(TOPasscodeSettingsViewState)state +{ + BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPasscode; + + // Update the warning label + self.warningLabel.hidden = !(confirmingPasscode && self.failedPasscodeAttemptCount > 0); + self.warningLabel.numberOfWarnings = self.failedPasscodeAttemptCount; + + CGRect frame = self.warningLabel.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - frame.size.width) * 0.5f; + self.warningLabel.frame = frame; +} + +- (void)transitionToState:(TOPasscodeSettingsViewState)state animated:(BOOL)animated +{ + // Preserve the current view state + UIView *snapshot = nil; + + BOOL reverseDirection = state < self.state; + + // If animated, take a snapshot of the current container view + if (animated) { + snapshot = [self.containerView snapshotViewAfterScreenUpdates:NO]; + snapshot.frame = self.containerView.frame; + [self.view addSubview:snapshot]; + } + + self.errorLabel.hidden = YES; + + // Update the layout for the new state + self.state = state; + + // Cancel out now if we're not animating + if (!animated) { + return; + } + + // Place the live container off screen to the right + CGFloat multiplier = reverseDirection ? -1.0f : 1.0f; + self.containerView.frame = CGRectOffset(self.containerView.frame, self.view.frame.size.width * multiplier, 0.0f); + + // Update the options button alpha depending on transition state + self.optionsButton.hidden = _hideOptionsButton; + self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 0.0f : 1.0f; + + // Perform an animation where the snapshot slides off, and the new container slides in + id animationBlock = ^{ + snapshot.frame = CGRectOffset(snapshot.frame, -self.view.frame.size.width * multiplier, 0.0f); + self.containerView.frame = CGRectOffset(self.containerView.frame, -self.view.frame.size.width * multiplier, 0.0f); + self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 1.0f : 0.0f; + }; + + // Clean up by removing the snapshot view + id completionBlock = ^(BOOL complete) { + [snapshot removeFromSuperview]; + }; + + // Perform the animation + [UIView animateWithDuration:0.4f + delay:0.0f + usingSpringWithDamping:1.0f + initialSpringVelocity:0.7f + options:0 + animations:animationBlock + completion:completionBlock]; +} + +- (void)viewDidLayoutSubviews +{ + [super viewDidLayoutSubviews]; + + CGSize viewSize = self.view.bounds.size; + + // Layout the keypad view + CGRect frame = self.keypadView.frame; + frame.size.height = viewSize.height * kTOPasscodeKeypadMaxSizeRatio; + frame.size.height = MAX(frame.size.height, kTOPasscodeKeypadMinHeight); + frame.size.height = MIN(frame.size.height, kTOPasscodeKeypadMaxHeight); + frame.size.width = viewSize.width; + frame.origin.y = viewSize.height; + if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { + frame.origin.y -= frame.size.height; + } + + self.keypadView.frame = CGRectIntegral(frame); + + BOOL horizontalLayout = frame.size.height < kTOPasscodeKeypadMinHeight + FLT_EPSILON; + BOOL animated = ([self.view.layer animationForKey:@"bounds.size"] != nil); + [self.keypadView setButtonLabelHorizontalLayout:horizontalLayout animated:animated]; + + CGFloat topContentHeight = self.topLayoutGuide.length; + + // Layout the container view + frame = self.containerView.frame; + frame.origin.y = (((viewSize.height - (topContentHeight + self.contentOverlapFrame.size.height))) * 0.5f) - self.verticalMidPoint; + frame.origin.y += topContentHeight; + self.containerView.frame = CGRectIntegral(frame); + + // Layout the passcode options button + frame = self.optionsButton.frame; + frame.origin.y = CGRectGetMinY(self.contentOverlapFrame) - kTOPasscodeSettingsOptionsButtonOffset - CGRectGetHeight(frame); + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + self.optionsButton.frame = frame; + + // Set frame of title label + frame = self.titleLabel.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + self.titleLabel.frame = CGRectIntegral(frame); + + // Set frame of number pad + frame = self.inputField.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing); + self.inputField.frame = CGRectIntegral(frame); + + // Set the frame for the warning view + frame = self.warningLabel.frame; + frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; + frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; + self.warningLabel.frame = CGRectIntegral(frame); + + // Set the frame of the error view + frame = self.errorLabel.frame; + frame.size = [self.errorLabel sizeThatFits:CGSizeMake(300.0f, CGFLOAT_MAX)]; + frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; + frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; + self.errorLabel.frame = CGRectIntegral(frame); +} + +- (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style +{ + BOOL isDark = (style == TOPasscodeSettingsViewStyleDark); + + // Set background color + UIColor *backgroundColor; + if (isDark) { + backgroundColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; + } + else { + backgroundColor = [UIColor colorWithRed:235.0f/255.0f green:235.0f/255.0f blue:241.0f/255.0f alpha:1.0f]; + } + self.view.backgroundColor = backgroundColor; + + // Set the style of the keypad view + self.keypadView.style = style; + + // Set the color for the input content + UIColor *inputColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + + // Set the label style + self.titleLabel.textColor = inputColor; + + // Set the number input tint + self.inputField.tintColor = inputColor; + + // Set the tint color of the incorrect warning label + UIColor *warningColor = nil; + if (isDark) { + warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; + } + else { + warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; + } +} + +#pragma mark - Data Management - +- (void)inputViewDidCompletePasscode:(NSString *)passcode +{ + switch (self.state) { + case TOPasscodeSettingsViewStateEnterCurrentPasscode: + [self validateCurrentPasscodeAttemptWithPasscode:passcode]; + break; + case TOPasscodeSettingsViewStateEnterNewPasscode: + [self didReceiveNewPasscode:passcode]; + break; + case TOPasscodeSettingsViewStateConfirmNewPasscode: + [self confirmNewPasscode:passcode]; + break; + } +} + +- (void)validateCurrentPasscodeAttemptWithPasscode:(NSString *)passcode +{ + if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didAttemptCurrentPasscode:)]) { + return; + } + + BOOL correct = [self.delegate passcodeSettingsViewController:self didAttemptCurrentPasscode:passcode]; + if (!correct) { + [self.inputField resetPasscodeAnimated:YES playImpact:YES]; + self.failedPasscodeAttemptCount++; + } + else { + [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; + } +} + +- (void)didReceiveNewPasscode:(NSString *)passcode +{ + self.potentialPasscode = passcode; + [self transitionToState:TOPasscodeSettingsViewStateConfirmNewPasscode animated:YES]; +} + +- (void)confirmNewPasscode:(NSString *)passcode +{ + if (![passcode isEqualToString:self.potentialPasscode]) { + [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; + self.errorLabel.hidden = NO; + return; + } + + if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didChangeToNewPasscode:ofType:)]) { + return; + } + + [self.delegate passcodeSettingsViewController:self didChangeToNewPasscode:self.potentialPasscode ofType:self.passcodeType]; +} + +#pragma mark - System Keyboard Handling - +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + self.keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + [self viewDidLayoutSubviews]; +} + +- (CGRect)contentOverlapFrame +{ + if (self.passcodeType < TOPasscodeTypeCustomAlphanumeric) { + return self.keypadView.frame; + } + + // Work out where our view is in relation to the screen + UIWindow *window = self.view.window; + CGRect viewFrame = [self.view.superview convertRect:self.view.frame toView:window]; + + CGFloat overlap = CGRectGetMaxY(viewFrame) - CGRectGetMinY(self.keyboardFrame); + + CGRect overlapFrame = self.keyboardFrame; + overlapFrame.origin.y = MIN(viewFrame.size.height - overlap, viewFrame.size.height); + overlapFrame.size.height = MAX(overlap, 0.0f); + return overlapFrame; +} + +#pragma mark - Button Callbacks - + +- (void)optionsCodeButtonTapped:(id)sender +{ + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + UIAlertActionStyle style = UIAlertActionStyleDefault; + + __weak typeof(self) weakSelf = self; + + NSArray *types = @[@(TOPasscodeTypeFourDigits), + @(TOPasscodeTypeSixDigits), + @(TOPasscodeTypeCustomNumeric), + @(TOPasscodeTypeCustomAlphanumeric) + ]; + + + NSArray *titles = @[NSLocalizedString(@"4-Digit Numeric Code", @""), + NSLocalizedString(@"6-Digit Numeric Code", @""), + NSLocalizedString(@"Custom Numeric Code", @""), + NSLocalizedString(@"Custom Alphanumeric Code", @"")]; + + // Add all the buttons + for (NSInteger i = 0; i < types.count; i++) { + TOPasscodeType type = [types[i] integerValue]; + if (type == self.passcodeType) { continue; } + + id handler = ^(UIAlertAction *action) { + [weakSelf setPasscodeType:type]; + }; + [alertController addAction:[UIAlertAction actionWithTitle:titles[i] style:style handler:handler]]; + } + + // Cancel button + [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:nil]]; + + alertController.modalPresentationStyle = UIModalPresentationPopover; + alertController.popoverPresentationController.sourceView = self.optionsButton; + alertController.popoverPresentationController.sourceRect = self.optionsButton.bounds; + alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (void)nextButtonTapped:(id)sender +{ + [self inputViewDidCompletePasscode:self.inputField.passcode]; +} + +- (void)doneButtonTapped:(id)sender +{ + [self inputViewDidCompletePasscode:self.inputField.passcode]; +} + +#pragma mark - Accessors - +- (void)setPasscodeType:(TOPasscodeType)passcodeType +{ + [self setPasscodeType:passcodeType animated:NO]; +} + +- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated +{ + if (_passcodeType == passcodeType) { return; } + _passcodeType = passcodeType; + + [self updateContentForState:self.state type:_passcodeType animated:animated]; +} + +- (void)setState:(TOPasscodeSettingsViewState)state +{ + if (_state == state) { return; } + _state = state; + + [self updateContentForState:_state type:self.passcodeType animated:NO]; +} + +- (void)setFailedPasscodeAttemptCount:(NSInteger)failedPasscodeAttemptCount +{ + if (_failedPasscodeAttemptCount == failedPasscodeAttemptCount) { return; } + _failedPasscodeAttemptCount = failedPasscodeAttemptCount; + [self updateWarningLabelForState:self.state]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h new file mode 100644 index 0000000000..f4f6a7023d --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h @@ -0,0 +1,163 @@ +// +// TOPasscodeViewController.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" +#import "TOPasscodeSettingsViewController.h" +#import "TOPasscodeView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TOPasscodeViewController; + +/** + A delegate object in charge of validating the passcodes that the user has entered into the passcode + view controller. + */ +@protocol TOPasscodeViewControllerDelegate + +@optional + +/** + Return YES if the user entered the expected PIN code. Return NO if it was incorrect. + (For security reasons, it is safer to fetch the saved PIN code only when this method is called, and + then discard it immediately. This is why the view controller does not directly store it.) +*/ +- (BOOL)passcodeViewController:(TOPasscodeViewController *)passcodeViewController isCorrectCode:(NSString *)code; + +/** The user tapped the 'Cancel' button. Any dismissing of confidential content should be done in here. */ +- (void)didTapCancelInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; + +/** The user successfully entered the correct code, as validated by `isCorrectCode:` */ +- (void)didInputCorrectPasscodeInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; + +/** When available, the user tapped the 'Touch ID' button, or the view controller itself automatically initiated + the Touch ID request on display. This method is where you should implement your + own Touch ID validation logic. For security reasons, this controller does not implement the Touch ID logic itself. */ + +- (void)didPerformBiometricValidationRequestInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; + +/** Called when the pin view was resized as a result of the view controller being resized. + You can use this to resize your custom header view if necessary. + */ +- (void)passcodeViewController:(TOPasscodeViewController *)passcodeViewController didResizePasscodeViewToWidth:(CGFloat)width; + +@end + + +/** + A view controller that displays an interface for entering a user passcode. + It may be presented modally over another view controller, requiring the user to enter + the passcode correctly before they are able to proceed inside the application. + */ +@interface TOPasscodeViewController : UIViewController + +/** A delegate object, in charge of verifying the PIN code entered by the user */ +@property (nonatomic, weak, nullable) id delegate; + +/** The type of passcode that is expected to be entered. */ +@property (nonatomic, readonly) TOPasscodeType passcodeType; + +/** Will show a 'Touch ID' or 'Face ID' (depending on `biometricType`) button if the user is allowed to log in that way. (Default is NO) */ +@property (nonatomic, assign) BOOL allowBiometricValidation; + +/** Will handle delete button press as delete last symbol (Default is YES) */ +@property (nonatomic, assign) BOOL handleDeletePress; + +/** Set the type of biometrics for this device to update the title of the biometrics button properly. */ +@property (nonatomic, assign) TOPasscodeBiometryType biometryType; + +/** If biometrics are available, automatically ask for it upon presentation (Default is NO) */ +@property (nonatomic, assign) BOOL automaticallyPromptForBiometricValidation; + +/** Optionally change the color of the title text label. */ +@property (nonatomic, strong, nullable) UIColor *titleLabelColor; + +/** Optionally change the tint color of the UI element that indicates input progress (eg the row of circles) */ +@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; + +/** Optionally enable or disable showing the lettering label of all keypad circle buttons. **/ +@property (nonatomic, assign) BOOL keypadButtonShowLettering; + +/** If the style isn't translucent, changes the tint color of the keypad circle button outlines. */ +@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundTintColor; + +/** The color of the text elements in each keypad button */ +@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; + +/** Optionally, the text color of the keypad button text when tapped. Animates back to the base color. */ +@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; + +/** The tint button of the accessory button views at the bottom of the keypad (ie 'Cance' etc) */ +@property (nonatomic, strong, nullable) UIColor *accessoryButtonTintColor; + +/** Controls the transluceny of the PIN background when the style has been set to translucent. */ +@property (nonatomic, readonly) UIVisualEffectView *backgroundEffectView; + +/** Opaque, background view when the style is opaque */ +@property (nonatomic, readonly) UIView *backgroundView; + +/** The keypad and accessory views that are displayed in the center of this view */ +@property (nonatomic, readonly) TOPasscodeView *passcodeView; + +/** The Touch ID button, visible if biometrics is enabled and `leftAccessoryButton` is nil. */ +@property (nonatomic, readonly) UIButton *biometricButton; + +/** The Cancel, visible if `rightAccessoryButton` is nil. */ +@property (nonatomic, readonly) UIButton *cancelButton; + +/** The left accessory button. Setting this will override the 'Touch ID' button. */ +@property (nonatomic, strong, nullable) UIButton *leftAccessoryButton; + +/** The right accessory button. Setting this will override the 'Cancel' button. */ +@property (nonatomic, strong, nullable) UIButton *rightAccessoryButton; + +@property (nonatomic, assign) CGFloat accessoryButtonsVerticalInset; + +/** Whether all of the content views are hidden or not, but the background translucent view remains. + Useful for obscuring the content while the app is suspended. */ +@property (nonatomic, assign) BOOL contentHidden; + +/** + Create a new instance of this view controller with the preset style and passcode type. + + @param type The type of passcode to enter (6-digit/numeric) + */ +- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel; + +/** + Hide everything except the background translucency view. + + @param hidden Whether the content is hidden or not. + @param animated The content will play a crossfade animation. + */ +- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END + +//! Project version number for TOPasscodeViewController. +FOUNDATION_EXPORT double TOPasscodeViewControllerVersionNumber; + +//! Project version string for TOPasscodeViewController. +FOUNDATION_EXPORT const unsigned char TOPasscodeViewControllerVersionString[]; diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m new file mode 100755 index 0000000000..34d81370ac --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m @@ -0,0 +1,710 @@ +// +// TOPasscodeViewController.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeViewController.h" +#import "TOPasscodeView.h" +#import "TOPasscodeViewControllerAnimatedTransitioning.h" +#import "TOPasscodeKeypadView.h" +#import "TOPasscodeInputField.h" + +@interface TOPasscodeViewController () + +/* State */ +@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; +@property (nonatomic, assign) CGFloat keyboardHeight; +@property (nonatomic, assign) BOOL passcodeSuccess; +@property (nonatomic, readonly) UIView *leftButton; +@property (nonatomic, readonly) UIView *rightButton; + +/* Views */ +@property (nonatomic, strong, readwrite) UIVisualEffectView *backgroundEffectView; +@property (nonatomic, strong, readwrite) UIView *backgroundView; +@property (nonatomic, strong, readwrite) TOPasscodeView *passcodeView; +@property (nonatomic, strong, readwrite) UIButton *biometricButton; +@property (nonatomic, strong, readwrite) UIButton *cancelButton; + +/* Style */ +@property (nonatomic, assign) TOPasscodeViewStyle style; +@property (nonatomic, assign) BOOL allowCancel; + +@end + +@implementation TOPasscodeViewController + +#pragma mark - Instance Creation - + +- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel +{ + if (self = [super initWithNibName:nil bundle:nil]) { + _passcodeType = type; + _allowCancel = cancel; + [self setUp]; + } + + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { + [self setUp]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +#pragma mark - View Setup - + +- (void)setUp +{ + self.transitioningDelegate = self; + self.automaticallyPromptForBiometricValidation = NO; + self.handleDeletePress = YES; + + if (@available(iOS 13.0, *)) { + if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { + self.style = TOPasscodeViewStyleTranslucentDark; + } else { + self.style = TOPasscodeViewStyleTranslucentLight; + } + } else { + self.style = TOPasscodeViewStyleTranslucentLight; + } + + if (TOPasscodeViewStyleIsTranslucent(self.style)) { + self.modalPresentationStyle = UIModalPresentationOverFullScreen; + } + else { + self.modalPresentationStyle = UIModalPresentationFullScreen; + } + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) + name:UIKeyboardWillChangeFrameNotification object:nil]; +} + +- (void)setUpBackgroundEffectViewForStyle:(TOPasscodeViewStyle)style +{ + BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); + + // Return if it already exists when it should + if (translucent && self.backgroundEffectView) { return; } + + // Return if it doesn't exist when it shouldn't + if (!translucent && !self.backgroundEffectView) { return; } + + // Remove it if we're now opaque + if (!translucent) { + [self.backgroundEffectView removeFromSuperview]; + self.backgroundEffectView = nil; + return; + } + + // Create it otherwise + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:[self blurEffectStyleForStyle:style]]; + self.backgroundEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; + self.backgroundEffectView.frame = self.view.bounds; + self.backgroundEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view insertSubview:self.backgroundEffectView atIndex:0]; +} + +- (void)setUpBackgroundViewForStyle:(TOPasscodeViewStyle)style +{ + BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); + + if (!translucent && self.backgroundView) { return; } + + if (translucent && !self.backgroundView) { return; } + + if (translucent) { + [self.backgroundView removeFromSuperview]; + self.backgroundView = nil; + return; + } + + self.backgroundView = [[UIView alloc] initWithFrame:self.view.bounds]; + self.backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view insertSubview:self.backgroundView atIndex:0]; +} + +- (UIBlurEffectStyle)blurEffectStyleForStyle:(TOPasscodeViewStyle)style +{ + switch (self.style) { + case TOPasscodeViewStyleTranslucentDark: return UIBlurEffectStyleDark; + case TOPasscodeViewStyleTranslucentLight: return UIBlurEffectStyleExtraLight; + default: return 0; + } + + return 0; +} + +- (void)setUpAccessoryButtons +{ + UIFont *buttonFont = [UIFont systemFontOfSize:16.0f]; + BOOL isPad = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad; + + if (!self.leftAccessoryButton && self.allowBiometricValidation && !self.biometricButton) { + self.biometricButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(self.biometryType) forState:UIControlStateNormal]; + [self.biometricButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + + if (isPad) { + self.passcodeView.leftButton = self.biometricButton; + } + else { + [self.view addSubview:self.biometricButton]; + } + } + else { + if (self.leftAccessoryButton) { + [self.biometricButton removeFromSuperview]; + self.biometricButton = nil; + } + } + + if (!self.rightAccessoryButton && !self.cancelButton) { + self.cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.cancelButton setTitle:NSLocalizedString(@"Cancel", @"Cancel") forState:UIControlStateNormal]; + self.cancelButton.titleLabel.font = buttonFont; + [self.cancelButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + // If cancelling is disabled, we hide the cancel button but we still create it, because it can + // transition to backspace after user input. + self.cancelButton.hidden = !self.allowCancel; + if (isPad) { + self.passcodeView.rightButton = self.cancelButton; + } + else { + [self.view addSubview:self.cancelButton]; + } + } + else { + if (self.rightAccessoryButton) { + [self.cancelButton removeFromSuperview]; + self.cancelButton = nil; + } + } + + [self updateAccessoryButtonFontsForSize:self.view.bounds.size]; +} + +#pragma mark - View Management - +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.view.backgroundColor = [UIColor clearColor]; + self.view.layer.allowsGroupOpacity = NO; + [self setUpBackgroundEffectViewForStyle:self.style]; + [self setUpBackgroundViewForStyle:self.style]; + [self setUpAccessoryButtons]; + [self applyThemeForStyle:self.style]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + + // Automatically trigger biometric validation if available + if (self.allowBiometricValidation && self.automaticallyPromptForBiometricValidation) { + [self accessoryButtonTapped:self.biometricButton]; + } +} + +- (void)viewDidLayoutSubviews +{ + CGSize bounds = self.view.bounds.size; + CGSize maxSize = bounds; + if (@available(iOS 11.0, *)) { + UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; + if (safeAreaInsets.bottom > 0) { + maxSize.height -= safeAreaInsets.bottom; + } + if (safeAreaInsets.left > 0) { + maxSize.width -= safeAreaInsets.left; + } + if (safeAreaInsets.right > 0) { + maxSize.width -= safeAreaInsets.right; + } + } + + // Resize the pin view to scale to the new size + [self.passcodeView sizeToFitSize:maxSize]; + + // Re-center the pin view + CGRect frame = self.passcodeView.frame; + frame.origin.x = (bounds.width - frame.size.width) * 0.5f; + frame.origin.y = ((bounds.height - self.keyboardHeight) - frame.size.height) * 0.5f; + self.passcodeView.frame = CGRectIntegral(frame); + + // -------------------------------------------------- + + // Update the accessory button sizes + [self updateAccessoryButtonFontsForSize:maxSize]; + + // Re-layout the accessory buttons + [self layoutAccessoryButtonsForSize:maxSize]; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + [self setNeedsStatusBarAppearanceUpdate]; + + // Force an initial layout if the view hasn't been presented yet + [UIView performWithoutAnimation:^{ + [self.view setNeedsLayout]; + [self.view layoutIfNeeded]; + }]; + + // Show the keyboard if we're entering alphanumeric characters + if (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { + [self.passcodeView.inputField becomeFirstResponder]; + } +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + // Dismiss the keyboard if it is visible + if (self.passcodeView.inputField.isFirstResponder) { + [self.passcodeView.inputField resignFirstResponder]; + } +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + return TOPasscodeViewStyleIsDark(self.style) ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; +} + +#pragma mark - View Rotations - +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + // We don't need to do anything special on iPad or if we're using character input + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad || self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { return; } + + // Work out if we need to transition to horizontal + BOOL horizontalLayout = size.height < size.width; + + // Perform layout animation + [self.passcodeView setHorizontalLayout:horizontalLayout animated:coordinator.animated duration:coordinator.transitionDuration]; +} + +#pragma mark - View Styling - +- (void)applyThemeForStyle:(TOPasscodeViewStyle)style +{ + BOOL isDark = TOPasscodeViewStyleIsDark(style); + + // Apply the tint color to the accessory buttons + UIColor *accessoryTintColor = self.accessoryButtonTintColor; + if (!accessoryTintColor) { + accessoryTintColor = isDark ? [UIColor whiteColor] : nil; + } + + self.biometricButton.tintColor = accessoryTintColor; + self.cancelButton.tintColor = accessoryTintColor; + self.leftAccessoryButton.tintColor = accessoryTintColor; + self.rightAccessoryButton.tintColor = accessoryTintColor; + + self.backgroundView.backgroundColor = isDark ? [UIColor colorWithWhite:0.1f alpha:1.0f] : [UIColor whiteColor]; +} + +- (void)updateAccessoryButtonFontsForSize:(CGSize)size +{ + CGFloat width = size.width; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + width = MIN(size.width, size.height); + } + + CGFloat pointSize = 17.0f; + if (width < TOPasscodeViewContentSizeMedium) { + pointSize = 14.0f; + } + else if (width < TOPasscodeViewContentSizeDefault) { + pointSize = 16.0f; + } + + UIFont *accessoryFont = [UIFont systemFontOfSize:pointSize]; + + self.biometricButton.titleLabel.font = accessoryFont; + self.cancelButton.titleLabel.font = accessoryFont; + self.leftAccessoryButton.titleLabel.font = accessoryFont; + self.rightAccessoryButton.titleLabel.font = accessoryFont; +} + +- (void)verticalLayoutAccessoryButtonsForSize:(CGSize)size +{ + CGFloat width = MIN(size.width, size.height); + + CGFloat verticalInset = 44.0f; + if (width < TOPasscodeViewContentSizeMedium) { + verticalInset = 20.0f; + } + else if (width < TOPasscodeViewContentSizeDefault) { + verticalInset = 30.0f; + } + + if (self.accessoryButtonsVerticalInset > 0) { + verticalInset = self.accessoryButtonsVerticalInset; + } + + CGFloat inset = self.passcodeView.keypadButtonInset; + CGPoint point = (CGPoint){0.0f, (self.view.bounds.size.height - self.keyboardHeight) - verticalInset}; + if (@available(iOS 11.0, *)) { + UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; + if (safeAreaInsets.bottom > 0) { + point.y -= safeAreaInsets.bottom; + } + } + + if (self.leftButton) { + [self.leftButton sizeToFit]; + point.x = self.passcodeView.frame.origin.x + inset; + self.leftButton.center = point; + } + + if (self.rightButton) { + [self.rightButton sizeToFit]; + point.x = CGRectGetMaxX(self.passcodeView.frame) - inset; + self.rightButton.center = point; + } +} + +- (void)horizontalLayoutAccessoryButtonsForSize:(CGSize)size +{ + CGRect passcodeViewFrame = self.passcodeView.frame; + CGFloat buttonInset = self.passcodeView.keypadButtonInset; + CGFloat width = MIN(size.width, size.height); + CGFloat verticalInset = 35.0f; + if (width < TOPasscodeViewContentSizeMedium) { + verticalInset = 30.0f; + } + else if (width < TOPasscodeViewContentSizeDefault) { + verticalInset = 35.0f; + } + + if (self.leftButton) { + [self.leftButton sizeToFit]; + CGRect frame = self.leftButton.frame; + frame.origin.y = (self.view.bounds.size.height - verticalInset) - (frame.size.height * 0.5f); + frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); + self.leftButton.frame = CGRectIntegral(frame); + } + + if (self.rightButton) { + [self.rightButton sizeToFit]; + CGRect frame = self.rightButton.frame; + frame.origin.y = verticalInset - (frame.size.height * 0.5f); + frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); + self.rightButton.frame = CGRectIntegral(frame); + } + + [self.view bringSubviewToFront:self.rightButton]; + [self.view bringSubviewToFront:self.leftButton]; +} + +- (void)layoutAccessoryButtonsForSize:(CGSize)size +{ + // The buttons are always embedded in the keypad view on iPad + if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPhone) { return; } + + if (self.passcodeView.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { + [self horizontalLayoutAccessoryButtonsForSize:size]; + } + else { + [self verticalLayoutAccessoryButtonsForSize:size]; + } +} + +#pragma mark - Interactions - +- (void)accessoryButtonTapped:(id)sender +{ + if (sender == self.cancelButton) { + // When entering keyboard input, just leave the button as 'cancel' + if (self.handleDeletePress && self.passcodeType != TOPasscodeTypeCustomAlphanumeric && self.passcodeView.passcode.length > 0) { + [self.passcodeView deleteLastPasscodeCharacterAnimated:YES]; + [self keypadButtonTapped]; + return; + } + + if ([self.delegate respondsToSelector:@selector(didTapCancelInPasscodeViewController:)]) { + [self.delegate didTapCancelInPasscodeViewController:self]; + } + } + else if (sender == self.biometricButton) { + if ([self.delegate respondsToSelector:@selector(didPerformBiometricValidationRequestInPasscodeViewController:)]) { + [self.delegate didPerformBiometricValidationRequestInPasscodeViewController:self]; + } + } +} + +- (void)keypadButtonTapped +{ + NSString *title = nil; + if (self.passcodeView.passcode.length > 0) { + title = NSLocalizedString(@"Delete", @"Delete"); + } else if (self.allowCancel) { + title = NSLocalizedString(@"Cancel", @"Cancel"); + } + [UIView performWithoutAnimation:^{ + if (title != nil) { + [self.cancelButton setTitle:title forState:UIControlStateNormal]; + [self.cancelButton layoutIfNeeded]; + } + self.cancelButton.hidden = (title == nil); + }]; +} + +- (void)didCompleteEnteringPasscode:(NSString *)passcode +{ + if (![self.delegate respondsToSelector:@selector(passcodeViewController:isCorrectCode:)]) { + return; + } + + // Validate the code + BOOL isCorrect = [self.delegate passcodeViewController:self isCorrectCode:passcode]; + if (!isCorrect) { + [self.passcodeView resetPasscodeAnimated:YES playImpact:YES]; + return; + } + + // Hang onto the fact the passcode was successful to play a nicer dismissal animation + self.passcodeSuccess = YES; + + // Perform handler if correctly entered + if ([self.delegate respondsToSelector:@selector(didInputCorrectPasscodeInPasscodeViewController:)]) { + [self.delegate didInputCorrectPasscodeInPasscodeViewController:self]; + } + else { + [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; + } +} + +#pragma mark - Keyboard Handling - +- (void)keyboardWillChangeFrame:(NSNotification *)notification +{ + // Extract the keyboard information we need from the notification + CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + CGFloat animationDuration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue]; + UIViewAnimationOptions animationCurve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; + + // Work out the on-screen height of the keyboard + self.keyboardHeight = self.view.bounds.size.height - keyboardFrame.origin.y; + self.keyboardHeight = MAX(self.keyboardHeight, 0.0f); + + // Set that the view needs to be laid out + [self.view setNeedsLayout]; + + if (animationDuration < FLT_EPSILON) { + return; + } + + // Animate the content sliding up and down with the keyboard + [UIView animateWithDuration:animationDuration + delay:0.0f + options:animationCurve + animations:^{ [self.view layoutIfNeeded]; } + completion:nil]; +} + +#pragma mark - Transitioning Delegate - +- (nullable id )animationControllerForPresentedController:(UIViewController *)presented + presentingController:(UIViewController *)presenting + sourceController:(UIViewController *)source +{ + return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:NO success:NO]; +} + +- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed +{ + return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:YES success:self.passcodeSuccess]; +} + +#pragma mark - Convenience Accessors - +- (UIView *)leftButton +{ + return self.leftAccessoryButton ? self.leftAccessoryButton : self.biometricButton; +} + +- (UIView *)rightButton +{ + return self.rightAccessoryButton ? self.rightAccessoryButton : self.cancelButton; +} + +#pragma mark - Public Accessors - +- (TOPasscodeView *)passcodeView +{ + if (_passcodeView) { return _passcodeView; } + + _passcodeView = [[TOPasscodeView alloc] initWithStyle:self.style passcodeType:self.passcodeType]; + _passcodeView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | + UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [_passcodeView sizeToFit]; + _passcodeView.center = self.view.center; + [self.view addSubview:_passcodeView]; + + __weak typeof(self) weakSelf = self; + _passcodeView.passcodeCompletedHandler = ^(NSString *passcode) { + [weakSelf didCompleteEnteringPasscode:passcode]; + }; + + _passcodeView.passcodeDigitEnteredHandler = ^{ + [weakSelf keypadButtonTapped]; + }; + + // Set initial layout to horizontal if we're rotated on an iPhone + if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric && UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) { + CGSize boundsSize = self.view.bounds.size; + _passcodeView.horizontalLayout = boundsSize.width > boundsSize.height; + } + + return _passcodeView; +} + +- (void)setStyle:(TOPasscodeViewStyle)style +{ + if (style == _style) { return; } + _style = style; + + self.passcodeView.style = style; + [self setUpBackgroundEffectViewForStyle:style]; +} + +- (void)setAllowBiometricValidation:(BOOL)allowBiometricValidation +{ + if (_allowBiometricValidation == allowBiometricValidation) { + return; + } + + _allowBiometricValidation = allowBiometricValidation; + [self setUpAccessoryButtons]; + [self applyThemeForStyle:self.style]; +} + +- (void)setTitleLabelColor:(UIColor *)titleLabelColor +{ + self.passcodeView.titleLabelColor = titleLabelColor; +} + +- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor +{ + self.passcodeView.subtitleLabelColor = subtitleLabelColor; +} + +- (UIColor *)titleLabelColor { return self.passcodeView.titleLabelColor; } + +- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor +{ + self.passcodeView.inputProgressViewTintColor = inputProgressViewTintColor; +} + +- (UIColor *)inputProgressViewTintColor { return self.passcodeView.inputProgressViewTintColor; } + +- (void)setKeypadButtonBackgroundTintColor:(UIColor *)keypadButtonBackgroundTintColor +{ + self.passcodeView.keypadButtonBackgroundColor = keypadButtonBackgroundTintColor; +} + +- (void)setKeypadButtonShowLettering:(BOOL)keypadButtonShowLettering +{ + self.passcodeView.keypadView.showLettering = keypadButtonShowLettering; +} + +- (UIColor *)keypadButtonBackgroundTintColor { return self.passcodeView.keypadButtonBackgroundColor; } + +- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor +{ + self.passcodeView.keypadButtonTextColor = keypadButtonTextColor; +} + +- (UIColor *)keypadButtonTextColor { return self.passcodeView.keypadButtonTextColor; } + +- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor +{ + self.passcodeView.keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; +} + +- (UIColor *)keypadButtonHighlightedTextColor { return self.passcodeView.keypadButtonHighlightedTextColor; } + +- (void)setAccessoryButtonTintColor:(UIColor *)accessoryButtonTintColor +{ + if (accessoryButtonTintColor == _accessoryButtonTintColor) { return; } + _accessoryButtonTintColor = accessoryButtonTintColor; + [self applyThemeForStyle:self.style]; +} + +- (void)setBiometryType:(TOPasscodeBiometryType)biometryType +{ + if (_biometryType == biometryType) { return; } + + _biometryType = biometryType; + + if (self.biometricButton) { + [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(_biometryType) forState:UIControlStateNormal]; + } +} + +- (void)setContentHidden:(BOOL)contentHidden +{ + [self setContentHidden:contentHidden animated:NO]; +} + +- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated +{ + if (hidden == _contentHidden) { return; } + _contentHidden = hidden; + + void (^setViewsHiddenBlock)(BOOL) = ^(BOOL hidden) { + self.passcodeView.hidden = hidden; + self.leftButton.hidden = hidden; + self.rightButton.hidden = hidden; + }; + + void (^completionBlock)(BOOL) = ^(BOOL complete) { + setViewsHiddenBlock(hidden); + }; + + if (!animated) { + completionBlock(YES); + return; + } + + // Make sure the views are visible before the animation + setViewsHiddenBlock(NO); + + void (^animationBlock)(void) = ^{ + CGFloat alpha = hidden ? 0.0f : 1.0f; + self.passcodeView.contentAlpha = alpha; + self.leftButton.alpha = alpha; + self.rightButton.alpha = alpha; + }; + + // Animate + [UIView animateWithDuration:0.4f animations:animationBlock completion:completionBlock]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h new file mode 100644 index 0000000000..b44bfc7dc1 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h @@ -0,0 +1,81 @@ +// +// TOPasscodeCircleButton.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +@class TOPasscodeCircleView; +@class TOPasscodeButtonLabel; + +NS_ASSUME_NONNULL_BEGIN + +/** + A UI control representing a single PIN code button for the keypad, + including the number, lettering (eg 'ABC'), and circle border. + */ +@interface TOPasscodeCircleButton : UIControl + +// Alpha value that properly controls the necessary subviews +@property (nonatomic, assign) CGFloat contentAlpha; + +// Required to be set before this view can be properly rendered +@property (nonatomic, strong) UIImage *backgroundImage; +@property (nonatomic, strong) UIImage *hightlightedBackgroundImage; +@property (nonatomic, strong) UIVibrancyEffect *vibrancyEffect; + +// Properties with default values +@property (nonatomic, strong) UIColor *textColor; +@property (nonatomic, strong, nullable) UIColor *highlightedTextColor; +@property (nonatomic, strong) UIFont *numberFont; +@property (nonatomic, strong) UIFont *letteringFont; +@property (nonatomic, assign) CGFloat letteringCharacterSpacing; +@property (nonatomic, assign) CGFloat letteringVerticalSpacing; + +@property (nonatomic, readonly) NSString *numberString; +@property (nonatomic, readonly) NSString *letteringString; + +// The internal views +@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; +@property (nonatomic, readonly) TOPasscodeCircleView *circleView; +@property (nonatomic, readonly) UIVisualEffectView *vibrancyView; + +// Callback handler +@property (nonatomic, copy) void (^buttonTappedHandler)(void); + +/** + Create a new instance of the class with the supplied number and lettering string + + @param numberString The string of the number to display in this button (eg '1'). + @param letteringString The string of the lettering to display underneath. + */ +- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString; + +/** + Set the background of the button to be the filled circle instead of hollow. + + @param highlighted When YES, the circle is full, when NO, it is hollow. + @param animated When animated, the transition is a crossfade. + */ +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m new file mode 100644 index 0000000000..790bfe0db3 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m @@ -0,0 +1,236 @@ +// +// TOPasscodeCircleButton.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeCircleButton.h" +#import "TOPasscodeCircleView.h" +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeCircleButton () + +@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; +@property (nonatomic, strong, readwrite) TOPasscodeCircleView *circleView; +@property (nonatomic, strong, readwrite) UIVisualEffectView *vibrancyView; + +@property (nonatomic, readwrite, copy) NSString *numberString; +@property (nonatomic, readwrite, copy) NSString *letteringString; + +@end + +@implementation TOPasscodeCircleButton + +- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString +{ + if (self = [super init]) { + _numberString = numberString; + _letteringString = letteringString; + _contentAlpha = 1.0f; + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + self.userInteractionEnabled = YES; + + _textColor = [UIColor whiteColor]; + + [self setUpSubviews]; + [self setUpViewInteraction]; +} + +- (void)setUpSubviews +{ + if (!self.circleView) { + self.circleView = [[TOPasscodeCircleView alloc] initWithFrame:self.bounds]; + [self addSubview:self.circleView]; + } + + if (!self.buttonLabel) { + self.buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:self.bounds]; + self.buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.buttonLabel.userInteractionEnabled = NO; + self.buttonLabel.textColor = self.textColor; + self.buttonLabel.numberString = self.numberString; + self.buttonLabel.letteringString = self.letteringString; + [self addSubview:self.buttonLabel]; + } + + if (!self.vibrancyView) { + self.vibrancyView = [[UIVisualEffectView alloc] initWithEffect:nil]; + self.vibrancyView.userInteractionEnabled = NO; + [self.vibrancyView.contentView addSubview:self.circleView]; + [self addSubview:self.vibrancyView]; + } +} + +- (void)setUpViewInteraction +{ + if (self.allTargets.count) { return; } + + [self addTarget:self action:@selector(buttonDidTouchDown:) forControlEvents:UIControlEventTouchDown]; + [self addTarget:self action:@selector(buttonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; + [self addTarget:self action:@selector(buttonDidDragInside:) forControlEvents:UIControlEventTouchDragEnter]; + [self addTarget:self action:@selector(buttonDidDragOutside:) forControlEvents:UIControlEventTouchDragExit]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.vibrancyView.frame = self.bounds; + self.circleView.frame = self.vibrancyView ? self.vibrancyView.bounds : self.bounds; + self.buttonLabel.frame = self.bounds; + [self bringSubviewToFront:self.buttonLabel]; +} + +#pragma mark - User Interaction - + +- (void)buttonDidTouchDown:(id)sender +{ + if (self.buttonTappedHandler) { self.buttonTappedHandler(); } + [self setHighlighted:YES animated:NO]; +} + +- (void)buttonDidTouchUpInside:(id)sender { [self setHighlighted:NO animated:YES]; } +- (void)buttonDidDragInside:(id)sender { [self setHighlighted:YES animated:NO]; } +- (void)buttonDidDragOutside:(id)sender { [self setHighlighted:NO animated:YES]; } + +#pragma mark - Animated Accessors - + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + [self.circleView setHighlighted:highlighted animated:animated]; + + if (!self.highlightedTextColor) { return; } + + void (^textFadeBlock)(void) = ^{ + self.buttonLabel.textColor = highlighted ? self.highlightedTextColor : self.textColor; + }; + + if (!animated) { + textFadeBlock(); + return; + } + + [UIView transitionWithView:self.buttonLabel + duration:0.6f + options:UIViewAnimationOptionTransitionCrossDissolve + animations:textFadeBlock + completion:nil]; +} + +#pragma mark - Accessors - + +- (void)setBackgroundImage:(UIImage *)backgroundImage +{ + self.circleView.circleImage = backgroundImage; + CGRect frame = self.frame; + frame.size = backgroundImage.size; + self.frame = CGRectIntegral(frame); +} + +- (UIImage *)backgroundImage { return self.circleView.circleImage; } + +/***********************************************************/ + +- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect +{ + if (_vibrancyEffect == vibrancyEffect) { return; } + _vibrancyEffect = vibrancyEffect; + self.vibrancyView.effect = _vibrancyEffect; +} + +/***********************************************************/ + +- (void)setHightlightedBackgroundImage:(UIImage *)hightlightedBackgroundImage +{ + self.circleView.highlightedCircleImage = hightlightedBackgroundImage; +} + +- (UIImage *)hightlightedBackgroundImage { return self.circleView.highlightedCircleImage; } + +/***********************************************************/ + +- (void)setNumberFont:(UIFont *)numberFont +{ + self.buttonLabel.numberLabelFont = numberFont; + [self setNeedsLayout]; +} + +- (UIFont *)numberFont { return self.buttonLabel.numberLabelFont; } + +/***********************************************************/ + +- (void)setLetteringFont:(UIFont *)letteringFont +{ + self.buttonLabel.letteringLabelFont = letteringFont; + [self setNeedsLayout]; +} + +- (UIFont *)letteringFont { return self.buttonLabel.letteringLabelFont; } + +/***********************************************************/ + +- (void)setLetteringVerticalSpacing:(CGFloat)letteringVerticalSpacing +{ + self.buttonLabel.letteringVerticalSpacing = letteringVerticalSpacing; + [self.buttonLabel setNeedsLayout]; +} + +- (CGFloat)letteringVerticalSpacing { return self.buttonLabel.letteringVerticalSpacing; } + +/***********************************************************/ + +- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing +{ + self.buttonLabel.letteringCharacterSpacing = letteringCharacterSpacing; +} + +- (CGFloat)letteringCharacterSpacing { return self.buttonLabel.letteringCharacterSpacing; } + +/***********************************************************/ + +- (void)setTextColor:(UIColor *)textColor +{ + if (textColor == _textColor) { return; } + _textColor = textColor; + + self.buttonLabel.textColor = _textColor; +} + +/***********************************************************/ + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + if (_contentAlpha == contentAlpha) { + return; + } + + _contentAlpha = contentAlpha; + + self.buttonLabel.alpha = contentAlpha; + self.circleView.alpha = contentAlpha; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h new file mode 100644 index 0000000000..4db6bb9a88 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h @@ -0,0 +1,101 @@ +// +// TOPasscodeKeypadView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class TOPasscodeCircleButton; + +/** + A view encompassing 9 circle buttons, making up a keypad view for entering PIN numbers. + Can be laid out vertically or horizontally. + */ +@interface TOPasscodeKeypadView : UIView + +/** The type of layout for the buttons (Default is vertical) */ +@property (nonatomic, assign) BOOL horizontalLayout; + +/** The vibrancy effect to be applied to each button background */ +@property (nonatomic, strong, nullable) UIVibrancyEffect *vibrancyEffect; + +/** The size of each input button */ +@property (nonatomic, assign) CGFloat buttonDiameter; + +/** The stroke width of the buttons */ +@property (nonatomic, assign) CGFloat buttonStrokeWidth; + +/** The spacing between the buttons. Default is (CGSize){25,15} */ +@property (nonatomic, assign) CGSize buttonSpacing; + +/** The font of the number in each button */ +@property (nonatomic, strong) UIFont *buttonNumberFont; + +/** The font of the lettering label */ +@property (nonatomic, strong) UIFont *buttonLetteringFont; + +/** The spacing between the lettering and the number label */ +@property (nonatomic, assign) CGFloat buttonLabelSpacing; + +/** The spacing between the letters in the lettering label */ +@property (nonatomic, assign) CGFloat buttonLetteringSpacing; + +/** Show the 'ABC' lettering under the numbers */ +@property (nonatomic, assign) BOOL showLettering; + +/** The spacing in points between the letters */ +@property (nonatomic, assign) CGFloat letteringSpacing; + +/** The tint color of the button backgrounds */ +@property (nonatomic, strong) UIColor *buttonBackgroundColor; + +/** The color of the text elements in each button */ +@property (nonatomic, strong) UIColor *buttonTextColor; + +/** Optionally the color of text when it's tapped. */ +@property (nonatomic, strong, nullable) UIColor *buttonHighlightedTextColor; + +/** The alpha value of all non-translucent views */ +@property (nonatomic, assign) CGFloat contentAlpha; + +/** Accessory views placed on either side of the '0' button */ +@property (nonatomic, strong, nullable) UIView *leftAccessoryView; +@property (nonatomic, strong, nullable) UIView *rightAccessoryView; + +/** The controls making up each of the button views */ +@property (nonatomic, readonly) NSArray *keypadButtons; + +/** The block that is triggered whenever a user taps one of the buttons */ +@property (nonatomic, copy) void (^buttonTappedHandler)(NSInteger buttonNumber); + +/* + Perform an animation to transition to a new layout. + + @param horizontalLayout The content is laid out horizontally. + @param animated Whether the transition is animated + @param duration The animation length of the transition. + */ +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m new file mode 100644 index 0000000000..a9cf0bb511 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m @@ -0,0 +1,432 @@ +// +// TOPasscodeKeypadView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeKeypadView.h" +#import "TOPasscodeCircleImage.h" +#import "TOPasscodeCircleButton.h" +#import "TOPasscodeCircleView.h" +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeKeypadView() + +/* Passcode buttons */ +@property (nonatomic, strong, readwrite) NSArray *keypadButtons; + +/* The '0' button for the different layouts */ +@property (nonatomic, strong) TOPasscodeCircleButton *verticalZeroButton; +@property (nonatomic, strong) TOPasscodeCircleButton *horizontalZeroButton; + +/* Images */ +@property (nonatomic, strong) UIImage *buttonImage; +@property (nonatomic, strong) UIImage *tappedButtonImage; + +@end + +@implementation TOPasscodeKeypadView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.userInteractionEnabled = YES; + _buttonDiameter = 81.0f; + _buttonSpacing = (CGSize){25,15}; + _buttonStrokeWidth = 1.5f; + _showLettering = YES; + _buttonNumberFont = nil; + _buttonLetteringFont = nil; + _buttonLabelSpacing = FLT_MIN; + _buttonLetteringSpacing = FLT_MIN; + [self sizeToFit]; + } + + return self; +} + +- (TOPasscodeCircleButton *)makeCircleButtonWithNumber:(NSInteger)number letteringString:(NSString *)letteringString +{ + NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; + + TOPasscodeCircleButton *circleButton = [[TOPasscodeCircleButton alloc] initWithNumberString:numberString letteringString:letteringString]; + circleButton.backgroundImage = self.buttonImage; + circleButton.hightlightedBackgroundImage = self.tappedButtonImage; + circleButton.vibrancyEffect = self.vibrancyEffect; + + // Add handler for when button is tapped + __weak typeof(self) weakSelf = self; + circleButton.buttonTappedHandler = ^{ + if (weakSelf.buttonTappedHandler) { + weakSelf.buttonTappedHandler(number); + } + }; + + return circleButton; +} + +- (void)setUpButtons +{ + NSMutableArray *buttons = [NSMutableArray array]; + + NSInteger numberOfButtons = 11; // 1-9 are normal, 10 is the vertical '0', 11 is the horizontal '0' + NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", + @"MNO", @"PQRS", @"TUV", @"WXYZ"]; + + for (NSInteger i = 0; i < numberOfButtons; i++) { + // Work out the button number text + NSInteger buttonNumber = i + 1; + if (buttonNumber == 10 || buttonNumber == 11) { buttonNumber = 0; } + + // Work out the lettering text + NSString *letteringString = nil; + if (self.showLettering && i > 0 && i-1 < letteredTitles.count) { // (Skip 1 and 0) + letteringString = letteredTitles[i-1]; + } + + // Create a new button and add it to this view + TOPasscodeCircleButton *circleButton = [self makeCircleButtonWithNumber:buttonNumber letteringString:letteringString]; + [self addSubview:circleButton]; + [buttons addObject:circleButton]; + + if (!self.showLettering) { + circleButton.buttonLabel.verticallyCenterNumberLabel = YES; // Center the digit in the middle + } + + // Hang onto the 0 button if it's the vertical one + // And center the text + if (i == 9) { + self.verticalZeroButton = circleButton; + + // Hide the button if it's not vertically laid out + if (self.horizontalLayout) { + self.verticalZeroButton.contentAlpha = 0.0f; + self.verticalZeroButton.hidden = YES; + } + } + else if (i == 10) { + self.horizontalZeroButton = circleButton; + + // Hide the button if it's not horizontally laid out + if (!self.horizontalLayout) { + self.horizontalZeroButton.contentAlpha = 0.0f; + self.horizontalZeroButton.hidden = YES; + } + } + } + + _keypadButtons = [NSArray arrayWithArray:buttons]; +} + +- (void)sizeToFit +{ + CGFloat padding = 2.0f; + + CGRect frame = self.frame; + if (self.horizontalLayout) { + frame.size.width = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.width * 3); + frame.size.height = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.height * 2); + } + else { + frame.size.width = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.width * 2); + frame.size.height = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.height * 3); + } + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + NSInteger i = 0; + CGPoint origin = CGPointZero; + for (TOPasscodeCircleButton *button in self.keypadButtons) { + // Set the button frame + CGRect frame = button.frame; + frame.origin = origin; + button.frame = frame; + + // Work out the next offset + CGFloat horizontalOffset = frame.size.width + self.buttonSpacing.width; + origin.x += horizontalOffset; + + i++; + + // If we're at the end of the row, move to the next one + if (i % 3 == 0) { + origin.x = 0.0f; + origin.y = origin.y + frame.size.height + self.buttonSpacing.height; + } + } + + // Lay out the vertical button + CGRect frame = self.verticalZeroButton.frame; + frame.origin.x += (frame.size.width + self.buttonSpacing.width); + self.verticalZeroButton.frame = frame; + + // Lay out the horizontal button + frame = self.horizontalZeroButton.frame; + frame.origin.x = (frame.size.width + self.buttonSpacing.width) * 3.0f; + frame.origin.y = frame.size.height + self.buttonSpacing.height; + self.horizontalZeroButton.frame = frame; + + // Layout the accessory buttons + CGFloat midPointY = CGRectGetMidY(self.verticalZeroButton.frame); + + if (self.leftAccessoryView) { + CGRect leftButtonFrame = self.keypadButtons.firstObject.frame; + CGFloat midPointX = CGRectGetMidX(leftButtonFrame); + + [self.leftAccessoryView sizeToFit]; + self.leftAccessoryView.center = (CGPoint){midPointX, midPointY}; + } + + if (self.rightAccessoryView) { + CGRect rightButtonFrame = self.keypadButtons[2].frame; + CGFloat midPointX = CGRectGetMidX(rightButtonFrame); + + [self.rightAccessoryView sizeToFit]; + self.rightAccessoryView.center = (CGPoint){midPointX, midPointY}; + } +} + +#pragma mark - Style Accessors - +- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect +{ + if (vibrancyEffect == _vibrancyEffect) { return; } + _vibrancyEffect = vibrancyEffect; + + for (TOPasscodeCircleButton *button in self.keypadButtons) { + button.vibrancyEffect = _vibrancyEffect; + } +} + +#pragma mark - Lazy Getters - +- (UIImage *)buttonImage +{ + if (!_buttonImage) { + _buttonImage = [TOPasscodeCircleImage hollowCircleImageOfSize:self.buttonDiameter strokeWidth:self.buttonStrokeWidth padding:1.0f]; + } + + return _buttonImage; +} + +- (UIImage *)tappedButtonImage +{ + if (!_tappedButtonImage) { + _tappedButtonImage = [TOPasscodeCircleImage circleImageOfSize:self.buttonDiameter inset:self.buttonStrokeWidth * 0.5f padding:1.0f antialias:YES]; + } + + return _tappedButtonImage; +} + +- (NSArray *)keypadButtons +{ + if (_keypadButtons) { return _keypadButtons; } + [self setUpButtons]; + return _keypadButtons; +} + +#pragma mark - Audio Delegate Protocol - +- (BOOL)enableInputClicksWhenVisible +{ + return YES; +} + +#pragma mark - Public Layout Setters - + +- (void)setHorizontalLayout:(BOOL)horizontalLayout +{ + [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration +{ + if (horizontalLayout== _horizontalLayout) { + return; + } + + _horizontalLayout = horizontalLayout; + + // Resize itself now so the frame value is up to date externally + [self sizeToFit]; + + // Set initial animation state + self.verticalZeroButton.hidden = NO; + self.horizontalZeroButton.hidden = NO; + + self.verticalZeroButton.contentAlpha = _horizontalLayout ? 1.0f : 0.0f; + self.horizontalZeroButton.contentAlpha = _horizontalLayout ? 0.0f : 1.0f; + + void (^animationBlock)(void) = ^{ + self.verticalZeroButton.contentAlpha = self.horizontalLayout ? 0.0f : 1.0f; + self.horizontalZeroButton.contentAlpha = self.horizontalLayout ? 1.0f : 0.0f; + }; + + void (^completionBlock)(BOOL) = ^(BOOL complete) { + self.verticalZeroButton.hidden = self.horizontalLayout; + self.horizontalZeroButton.hidden = self.horizontalLayout; + }; + + // Don't animate if not needed + if (!animated) { + animationBlock(); + completionBlock(YES); + return; + } + + // Perform animation + [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; +} + +- (void)updateButtonsForCurrentState +{ + for (TOPasscodeCircleButton *circleButton in self.keypadButtons) { + circleButton.backgroundImage = self.buttonImage; + circleButton.hightlightedBackgroundImage = self.tappedButtonImage; + circleButton.numberFont = self.buttonNumberFont; + circleButton.letteringFont = self.buttonLetteringFont; + circleButton.letteringVerticalSpacing = self.buttonLabelSpacing; + circleButton.letteringCharacterSpacing = self.buttonLetteringSpacing; + circleButton.tintColor = self.buttonBackgroundColor; + circleButton.textColor = self.buttonTextColor; + circleButton.highlightedTextColor = self.buttonHighlightedTextColor; + if (!_showLettering) { + circleButton.buttonLabel.letteringLabel.text = nil; + circleButton.buttonLabel.verticallyCenterNumberLabel = YES; + } + } + + [self setNeedsLayout]; +} + +- (void)setButtonDiameter:(CGFloat)buttonDiameter +{ + if (_buttonDiameter == buttonDiameter) { return; } + _buttonDiameter = buttonDiameter; + _tappedButtonImage = nil; + _buttonImage = nil; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonSpacing:(CGSize)buttonSpacing +{ + if (CGSizeEqualToSize(_buttonSpacing, buttonSpacing)) { return; } + _buttonSpacing = buttonSpacing; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonStrokeWidth:(CGFloat)buttonStrokeWidth +{ + if (_buttonStrokeWidth== buttonStrokeWidth) { return; } + _buttonStrokeWidth = buttonStrokeWidth; + _tappedButtonImage = nil; + _buttonImage = nil; + [self updateButtonsForCurrentState]; +} + +- (void)setShowLettering:(BOOL)showLettering +{ + if (_showLettering == showLettering) { return; } + _showLettering = showLettering; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonNumberFont:(UIFont *)buttonNumberFont +{ + if (_buttonNumberFont == buttonNumberFont) { return; } + _buttonNumberFont = buttonNumberFont; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonLetteringFont:(UIFont *)buttonLetteringFont +{ + if (buttonLetteringFont == _buttonLetteringFont) { return; } + _buttonLetteringFont = buttonLetteringFont; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonLabelSpacing:(CGFloat)buttonLabelSpacing +{ + if (buttonLabelSpacing == _buttonLabelSpacing) { return; } + _buttonLabelSpacing = buttonLabelSpacing; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonLetteringSpacing:(CGFloat)buttonLetteringSpacing +{ + if (buttonLetteringSpacing == _buttonLetteringSpacing) { return; } + _buttonLetteringSpacing = buttonLetteringSpacing; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor +{ + if (buttonBackgroundColor == _buttonBackgroundColor) { return; } + _buttonBackgroundColor = buttonBackgroundColor; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonTextColor:(UIColor *)buttonTextColor +{ + if (buttonTextColor == _buttonTextColor) { return; } + _buttonTextColor = buttonTextColor; + [self updateButtonsForCurrentState]; +} + +- (void)setButtonHighlightedTextColor:(UIColor *)buttonHighlightedTextColor +{ + if (buttonHighlightedTextColor == _buttonHighlightedTextColor) { return; } + _buttonHighlightedTextColor = buttonHighlightedTextColor; + [self updateButtonsForCurrentState]; +} + +- (void)setLeftAccessoryView:(UIView *)leftAccessoryView +{ + if (_leftAccessoryView == leftAccessoryView) { return; } + _leftAccessoryView = leftAccessoryView; + [self addSubview:_leftAccessoryView]; + [self setNeedsLayout]; +} + +- (void)setRightAccessoryView:(UIView *)rightAccessoryView +{ + if (_rightAccessoryView == rightAccessoryView) { return; } + _rightAccessoryView = rightAccessoryView; + [self addSubview:_rightAccessoryView]; + [self setNeedsLayout]; +} + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + _contentAlpha = contentAlpha; + + for (TOPasscodeCircleButton *button in self.keypadButtons) { + // Skip whichever '0' button is not presently being used + if ((self.horizontalLayout && button == self.verticalZeroButton) || + (!self.horizontalLayout && button == self.horizontalZeroButton)) + { + continue; + } + + button.contentAlpha = contentAlpha; + } +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h new file mode 100644 index 0000000000..b027ee853b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h @@ -0,0 +1,136 @@ +// +// TOPasscodeView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" +#import "TOPasscodeKeypadView.h" + +NS_ASSUME_NONNULL_BEGIN + +@class TOPasscodeCircleButton; +@class TOPasscodeInputField; +@class TOPasscodeKeypadView; +@class TOPasscodeViewContentLayout; + +/** + The passcode view is the primary content view for the passcode view controller. + On iPad, every view except the background view is a subview of this view. + On iPhone, the auxiliary buttons ('Touch ID', 'Cancel') are managed by the view controller. + */ +@interface TOPasscodeView : UIView + +/* The visual style of the view */ +@property (nonatomic, assign) TOPasscodeViewStyle style; + +/* The type of passcode being managed by it */ +@property (nonatomic, readonly) TOPasscodeType passcodeType; + +/* Whether the content is laid out vertically or horizontally (iPhone only) */ +@property (nonatomic, assign) BOOL horizontalLayout; + +/* The text in the title view (Default is 'Enter Passcode') */ +@property (nonatomic, copy) NSString *titleText; +@property (nonatomic, copy) NSString *subtitleText; + +/* Customizable Accessory Views */ +@property (nonatomic, strong, nullable) UIView *titleView; +@property (nonatomic, strong, nullable) UIButton *leftButton; +@property (nonatomic, strong, nullable) UIButton *rightButton; + +/* The default views always shown in this view */ +@property (nonatomic, readonly) UILabel *titleLabel; +@property (nonatomic, readonly) UILabel *subtitleLabel; +@property (nonatomic, readonly) TOPasscodeInputField *inputField; +@property (nonatomic, readonly) TOPasscodeKeypadView *keypadView; + +/* Overrides for theming the various elements. */ +@property (nonatomic, strong, nullable) UIColor *titleLabelColor; +@property (nonatomic, strong, nullable) UIColor *subtitleLabelColor; +@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; + +/* Horizontal inset from edge of keypad view to button center */ +@property (nonatomic, readonly) CGFloat keypadButtonInset; + +/* An animatable property for animating the non-translucent subviews */ +@property (nonatomic, assign) CGFloat contentAlpha; + +/* The passcode currently entered into this view */ +@property (nonatomic, copy, nullable) NSString *passcode; + +/* The default layout object controlling the + sizing and placement of all this view's child elements. */ +@property (nonatomic, strong, null_resettable) TOPasscodeViewContentLayout *defaultContentLayout; + +/* As needed, additional layout objects that will be checked and used in priority over the default content layout. */ +@property (nonatomic, strong, nullable) NSArray *contentLayouts; + +/* Callback triggered each time the user taps a key */ +@property (nonatomic, copy, nullable) void (^passcodeDigitEnteredHandler)(void); + +/* Callback triggered when the user has finished entering the passcode */ +@property (nonatomic, copy, nullable) void (^passcodeCompletedHandler)(NSString *passcode); + +/* + Create a new instance with one of the style types + + @param style The visual style of the passcode view. + @param type The type of passcode to accept. + */ +- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type; + +/* + Resize the view and all subviews for the optimum size to fit a super view of the suplied width. + + @param size The size of the view to which this view. + */ +- (void)sizeToFitSize:(CGSize)size; + +/* + Reset the passcode to nil and optionally play animation / vibration to match + + @param animated Play a shaking animation to reset the passcode. + @param impact On supported devices, play a small reset vibration as well. + */ +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; + +/* + Delete the last character from the passcode + + @param animated Whether the delete operation is animated or not. + */ +- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated; + +/* + Animate the transition between horizontal and vertical layouts + + @param horizontalLayout Whether to lay out the content vertically or horizontally. + @param animated Whether the transition is animated or not. + @param duration The duration of the animation + */ +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m new file mode 100644 index 0000000000..aad42bf5f1 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m @@ -0,0 +1,690 @@ +// +// TOPasscodeView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeView.h" +#import "TOPasscodeViewContentLayout.h" +#import "TOPasscodeCircleButton.h" +#import "TOPasscodeInputField.h" +#import "TOPasscodeKeypadView.h" + +@interface TOPasscodeView () + +/* The current layout object used to configure this view */ +@property (nonatomic, weak) TOPasscodeViewContentLayout *currentLayout; + +/* The main views */ +@property (nonatomic, strong, readwrite) UILabel *titleLabel; +@property (nonatomic, strong, readwrite) UILabel *subtitleLabel; +@property (nonatomic, strong, readwrite) TOPasscodeInputField *inputField; +@property (nonatomic, strong, readwrite) TOPasscodeKeypadView *keypadView; + +/* The type of passcode we're displaying */ +@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; + +@end + +@implementation TOPasscodeView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + + return self; +} + +- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type +{ + if (self = [super initWithFrame:CGRectMake(0,0,320,393)]) { + _style = style; + _passcodeType = type; + [self setUp]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder +{ + if (self = [super initWithCoder:aDecoder]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + // Set up default properties + self.userInteractionEnabled = YES; + _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; + _currentLayout = _defaultContentLayout; + _contentLayouts = @[[TOPasscodeViewContentLayout mediumScreenContentLayout], + [TOPasscodeViewContentLayout smallScreenContentLayout]]; + _titleText = NSLocalizedString(@"Enter Passcode", @""); + + // Start configuring views + [self setUpViewForType:self.passcodeType]; + + // Set the default layout for the views + [self updateSubviewsForContentLayout:_defaultContentLayout]; + + // Configure the theme of all of the views + [self applyThemeForStyle:_style]; +} + +#pragma mark - View Layout - +- (void)verticallyLayoutSubviews +{ + CGSize viewSize = self.frame.size; + CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; + + CGRect frame = CGRectZero; + CGFloat y = 0.0f; + + // Title View + if (self.titleView) { + frame = self.titleView.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.titleView.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.titleViewBottomSpacing; + } + + // Title Label + frame = self.titleLabel.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.titleLabel.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.titleLabelBottomSpacing; + + // Circle Row View + [self.inputField sizeToFit]; + frame = self.inputField.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.inputField.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.circleRowBottomSpacing; + + // Subtitle Label + frame = self.subtitleLabel.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.subtitleLabel.frame = CGRectIntegral(frame); + + y = CGRectGetMaxY(frame) + self.currentLayout.subtitleLabelBottomSpacing; + + // PIN Pad View + if (self.keypadView) { + frame = self.keypadView.frame; + frame.origin.y = y; + frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); + self.keypadView.frame = CGRectIntegral(frame); + } + + // If the keypad view is hidden, lay out the left button manually + if (!self.keypadView && self.leftButton) { + frame = self.leftButton.frame; + frame.origin.x = 0.0f; + frame.origin.y = y; + self.leftButton.frame = frame; + } + + // If the keypad view is hidden, lay out the right button manually + if (!self.keypadView && self.rightButton) { + frame = self.rightButton.frame; + frame.origin.x = viewSize.width - frame.size.width; + frame.origin.y = y; + self.rightButton.frame = frame; + } +} + +- (void)horizontallyLayoutSubviews +{ + CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; + CGRect frame = CGRectZero; + + // Work out the y offset, assuming the input field is in the middle + frame.origin.y = midViewSize.height - (self.inputField.frame.size.height * 0.5f); + frame.origin.y -= (self.titleLabel.frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); + + // Include offset for title view if present + if (self.titleView) { + frame.origin.y -= (self.titleView.frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); + } + + // Set initial Y offset + frame.origin.y = MAX(frame.origin.y, 0.0f); + + // Set frame of title view + if (self.titleView) { + frame.size = self.titleView.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.titleView.frame = CGRectIntegral(frame); + + frame.origin.y += (frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); + } + + // Set frame of title label + frame.size = self.titleLabel.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.titleLabel.frame = CGRectIntegral(frame); + + frame.origin.y += (frame.size.height + self.currentLayout.subtitleLabelHorizontalBottomSpacing); + + // Set frame of subtitle label + frame.size = self.subtitleLabel.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.subtitleLabel.frame = CGRectIntegral(frame); + + frame.origin.y += (frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); + + // Set frame of the input field + frame.size = self.inputField.frame.size; + frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; + self.inputField.frame = CGRectIntegral(frame); + + // Set the frame of the keypad view + frame.size = self.keypadView.frame.size; + frame.origin.y = 0.0f; + frame.origin.x = self.currentLayout.titleHorizontalLayoutWidth + self.currentLayout.titleHorizontalLayoutSpacing; + self.keypadView.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + if (self.horizontalLayout) { + [self horizontallyLayoutSubviews]; + } + else { + [self verticallyLayoutSubviews]; + } +} + +- (void)sizeToFitSize:(CGSize)size +{ + CGFloat width = size.width; + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { + width = MIN(size.width, size.height); + } + + NSMutableArray *layouts = [NSMutableArray array]; + [layouts addObject:self.defaultContentLayout]; + [layouts addObjectsFromArray:self.contentLayouts]; + + // Loop through each layout (in ascending order) and pick the best one to fit this view + TOPasscodeViewContentLayout *contentLayout = self.defaultContentLayout; + for (TOPasscodeViewContentLayout *layout in layouts) { + if (width >= layout.viewWidth) { + contentLayout = layout; + break; + } + } + + // Set the new layout + self.currentLayout = contentLayout; + + // Resize the views to fit + [self sizeToFit]; +} + +- (void)verticalSizeToFit +{ + CGRect frame = self.frame; + frame.size.width = 0.0f; + frame.size.height = 0.0f; + + [self.keypadView sizeToFit]; + [self.inputField sizeToFit]; + + if (self.keypadView) { + frame.size.width = self.keypadView.frame.size.width; + } + else { + frame.size.width = self.inputField.frame.size.width; + } + + // Add height for the title view + if (self.titleView) { + frame.size.height += self.titleView.frame.size.height; + frame.size.height += self.currentLayout.titleViewBottomSpacing; + } + + // Add height for the title label + CGRect titleFrame = self.titleLabel.frame; + titleFrame.size = [self.titleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; + self.titleLabel.frame = titleFrame; + + frame.size.height += titleFrame.size.height; + frame.size.height += self.currentLayout.titleLabelBottomSpacing; + + // Add height for the subtitle label + CGRect subtitleFrame = self.subtitleLabel.frame; + subtitleFrame.size = [self.subtitleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; + self.subtitleLabel.frame = subtitleFrame; + + frame.size.height += subtitleFrame.size.height; + frame.size.height += self.currentLayout.subtitleLabelBottomSpacing; + + // Add height for the circle rows + frame.size.height += self.inputField.frame.size.height; + frame.size.height += self.currentLayout.circleRowBottomSpacing; + + // Add height for the keypad + if (self.keypadView) { + frame.size.height += self.keypadView.frame.size.height; + } + else { // If no keypad, just factor in the accessory buttons + [self.leftButton sizeToFit]; + [self.rightButton sizeToFit]; + + CGFloat maxHeight = 0.0f; + maxHeight = MAX(self.leftButton.frame.size.height, 0.0f); + maxHeight = MAX(self.rightButton.frame.size.height, maxHeight); + + frame.size.height += maxHeight; + } + + // Add extra padding at the bottom + frame.size.height += self.currentLayout.bottomPadding; + + // Set the frame back + self.frame = CGRectIntegral(frame); +} + +- (void)horizontalSizeToFit +{ + CGRect frame = self.frame; + + [self.keypadView sizeToFit]; + [self.inputField sizeToFit]; + + frame.size.width = self.currentLayout.titleHorizontalLayoutWidth; + frame.size.width += self.currentLayout.titleHorizontalLayoutSpacing; + frame.size.width += self.keypadView.frame.size.width; + + frame.size.height = self.keypadView.frame.size.height; + + self.frame = CGRectIntegral(frame); +} + +- (void)sizeToFit +{ + if (self.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { + [self horizontalSizeToFit]; + } + else { + [self verticalSizeToFit]; + } +} + +#pragma mark - View Setup - +- (void)setUpViewForType:(TOPasscodeType)type +{ + __weak typeof(self) weakSelf = self; + + self.backgroundColor = [UIColor clearColor]; + + // Set up title label + if (self.titleLabel == nil) { + self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + } + self.titleLabel.text = self.titleText; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.numberOfLines = 0; + [self.titleLabel sizeToFit]; + [self addSubview:self.titleLabel]; + + // Set up the passcode style + TOPasscodeInputFieldStyle style = TOPasscodeInputFieldStyleFixed; + if (type >= TOPasscodeTypeCustomNumeric) { + style = TOPasscodeInputFieldStyleVariable; + } + + // Set up input field + if (self.inputField == nil) { + self.inputField = [[TOPasscodeInputField alloc] initWithStyle:style]; + } + self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { + if (weakSelf.passcodeCompletedHandler) { + weakSelf.passcodeCompletedHandler(passcode); + } + }; + + // Configure the input field based on the exact passcode type + if (style == TOPasscodeInputFieldStyleFixed) { + self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; + } + else { + self.inputField.showSubmitButton = (self.passcodeType == TOPasscodeTypeCustomNumeric); + self.inputField.enabled = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric); + } + + [self addSubview:self.inputField]; + + // Set up subtitle label + if (self.subtitleLabel == nil) { + self.subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + } + self.subtitleLabel.text = self.subtitleText; + self.subtitleLabel.textAlignment = NSTextAlignmentCenter; + self.subtitleLabel.numberOfLines = 0; + [self.subtitleLabel sizeToFit]; + [self addSubview:self.subtitleLabel]; + + // Set up pad row + if (type != TOPasscodeTypeCustomAlphanumeric) { + if (self.keypadView == nil) { + self.keypadView = [[TOPasscodeKeypadView alloc] init]; + } + self.keypadView.buttonTappedHandler = ^(NSInteger button) { + NSString *numberString = [NSString stringWithFormat:@"%ld", (long)button]; + [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; + + if (weakSelf.passcodeDigitEnteredHandler) { + weakSelf.passcodeDigitEnteredHandler(); + } + }; + [self addSubview:self.keypadView]; + } + else { + [self.keypadView removeFromSuperview]; + self.keypadView = nil; + } +} + +- (void)updateSubviewsForContentLayout:(TOPasscodeViewContentLayout *)contentLayout +{ + // Title View + self.titleLabel.font = contentLayout.titleLabelFont; + + // Subtitle View + self.subtitleLabel.font = contentLayout.subtitleLabelFont; + + // Circle Row View + self.inputField.fixedInputView.circleDiameter = contentLayout.circleRowDiameter; + self.inputField.fixedInputView.circleSpacing = contentLayout.circleRowSpacing; + + // Text Field Input Row + NSInteger maximumInputLength = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) ? + contentLayout.textFieldAlphanumericCharacterLength : + contentLayout.textFieldNumericCharacterLength; + + self.inputField.variableInputView.outlineThickness = contentLayout.textFieldBorderThickness; + self.inputField.variableInputView.outlineCornerRadius = contentLayout.textFieldBorderRadius; + self.inputField.variableInputView.circleDiameter = contentLayout.textFieldCircleDiameter; + self.inputField.variableInputView.circleSpacing = contentLayout.textFieldCircleSpacing; + self.inputField.variableInputView.outlinePadding = contentLayout.textFieldBorderPadding; + self.inputField.variableInputView.maximumVisibleLength = maximumInputLength; + + // Submit button + self.inputField.submitButtonSpacing = contentLayout.submitButtonSpacing; + self.inputField.submitButtonFontSize = contentLayout.submitButtonFontSize; + + // Keypad + self.keypadView.buttonNumberFont = contentLayout.circleButtonTitleLabelFont; + self.keypadView.buttonLetteringFont = contentLayout.circleButtonLetteringLabelFont; + self.keypadView.buttonLetteringSpacing = contentLayout.circleButtonLetteringSpacing; + self.keypadView.buttonLabelSpacing = contentLayout.circleButtonLabelSpacing; + self.keypadView.buttonSpacing = contentLayout.circleButtonSpacing; + self.keypadView.buttonDiameter = contentLayout.circleButtonDiameter; +} + +- (void)applyThemeForStyle:(TOPasscodeViewStyle)style +{ + BOOL isTranslucent = TOPasscodeViewStyleIsTranslucent(style); + BOOL isDark = TOPasscodeViewStyleIsDark(style); + + // Set title label color + UIColor *titleLabelColor = self.titleLabelColor; + if (titleLabelColor == nil) { + titleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + } + self.titleLabel.textColor = titleLabelColor; + + // Set subtitle label color + UIColor *subtitleLabelColor = self.subtitleLabelColor; + if (subtitleLabelColor == nil) { + subtitleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + } + self.subtitleLabel.textColor = subtitleLabelColor; + + // Add/remove the translucency effect to the buttons + if (isTranslucent) { + UIBlurEffect *blurEffect = [self blurEffectForStyle:style]; + UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:blurEffect]; + self.inputField.visualEffectView.effect = vibrancyEffect; + self.keypadView.vibrancyEffect = vibrancyEffect; + } + else { + self.inputField.visualEffectView.effect = nil; + self.keypadView.vibrancyEffect = nil; + } + + // Set keyboard style of the input field + self.inputField.keyboardAppearance = isDark ? UIKeyboardAppearanceDark : UIKeyboardAppearanceDefault; + + UIColor *defaultTintColor = isDark ? [UIColor colorWithWhite:0.85 alpha:1.0f] : [UIColor colorWithWhite:0.3 alpha:1.0f]; + + // Set the tint color of the circle row view + UIColor *circleRowColor = self.inputProgressViewTintColor; + if (circleRowColor == nil) { + circleRowColor = defaultTintColor; + } + self.inputField.tintColor = defaultTintColor; + + // Set the tint color of the keypad buttons + UIColor *keypadButtonBackgroundColor = self.keypadButtonBackgroundColor; + if (keypadButtonBackgroundColor == nil) { + keypadButtonBackgroundColor = defaultTintColor; + } + self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; + + // Set the color of the keypad button labels + UIColor *buttonTextColor = self.keypadButtonTextColor; + if (buttonTextColor == nil) { + buttonTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + } + self.keypadView.buttonTextColor = buttonTextColor; + + // Set the highlight color of the keypad button + UIColor *buttonHighlightedTextColor = self.keypadButtonHighlightedTextColor; + if (buttonHighlightedTextColor == nil) { + if (isTranslucent) { + buttonHighlightedTextColor = isDark ? nil : [UIColor whiteColor]; + } + else { + buttonHighlightedTextColor = isDark ? [UIColor blackColor] : [UIColor whiteColor]; + } + } + self.keypadView.buttonHighlightedTextColor = buttonHighlightedTextColor; +} + +#pragma mark - Passcode Management - +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact +{ + [self.inputField resetPasscodeAnimated:animated playImpact:impact]; +} + +- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated +{ + [self.inputField deletePasscodeCharactersOfCount:1 animated:animated]; +} + +#pragma mark - Internal Style Management - +- (UIBlurEffect *)blurEffectForStyle:(TOPasscodeViewStyle)style +{ + switch (style) { + case TOPasscodeViewStyleTranslucentDark: + return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; + case TOPasscodeViewStyleTranslucentLight: + return [UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]; + default: return nil; + } + + return nil; +} + +#pragma mark - Accessors - +- (void)setHorizontalLayout:(BOOL)horizontalLayout +{ + [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration +{ + if (horizontalLayout == _horizontalLayout) { return; } + _horizontalLayout = horizontalLayout; + [self.keypadView setHorizontalLayout:horizontalLayout animated:animated duration:duration]; + [self.inputField setHorizontalLayout:horizontalLayout animated:animated duration:duration]; +} + +- (void)setDefaultContentLayout:(TOPasscodeViewContentLayout *)defaultContentLayout +{ + if (defaultContentLayout == _defaultContentLayout) { return; } + _defaultContentLayout = defaultContentLayout; + + if (!_defaultContentLayout) { + _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; + } +} + +- (void)setCurrentLayout:(TOPasscodeViewContentLayout *)currentLayout +{ + if (_currentLayout == currentLayout) { return; } + _currentLayout = currentLayout; + + // Update the views + [self updateSubviewsForContentLayout:currentLayout]; +} + +- (void)setStyle:(TOPasscodeViewStyle)style +{ + if (style == _style) { return; } + _style = style; + [self applyThemeForStyle:style]; +} + +- (void)setTitleLabelColor:(UIColor *)titleLabelColor +{ + if (titleLabelColor == _titleLabelColor) { return; } + _titleLabelColor = titleLabelColor; + self.titleLabel.textColor = titleLabelColor; +} + +- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor +{ + if (subtitleLabelColor == _subtitleLabelColor) { return; } + _subtitleLabelColor = subtitleLabelColor; + self.subtitleLabel.textColor = subtitleLabelColor; +} + +- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor +{ + if (inputProgressViewTintColor == _inputProgressViewTintColor) { return; } + _inputProgressViewTintColor = inputProgressViewTintColor; + self.inputField.tintColor = inputProgressViewTintColor; +} + +- (void)setKeypadButtonBackgroundColor:(UIColor *)keypadButtonBackgroundColor +{ + if (keypadButtonBackgroundColor == _keypadButtonBackgroundColor) { return; } + _keypadButtonBackgroundColor = keypadButtonBackgroundColor; + self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; +} + +- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor +{ + if (keypadButtonTextColor == _keypadButtonTextColor) { return; } + _keypadButtonTextColor = keypadButtonTextColor; + self.keypadView.buttonTextColor = keypadButtonTextColor; +} + +- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor +{ + if (keypadButtonHighlightedTextColor == _keypadButtonHighlightedTextColor) { return; } + _keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; + self.keypadView.buttonHighlightedTextColor = keypadButtonHighlightedTextColor; +} + +- (void)setLeftButton:(UIButton *)leftButton +{ + if (leftButton == _leftButton) { return; } + _leftButton = leftButton; + + if (self.keypadView) { + self.keypadView.leftAccessoryView = leftButton; + } + else { + [self addSubview:_leftButton]; + } +} + +- (void)setRightButton:(UIButton *)rightButton +{ + if (rightButton == _rightButton) { return; } + _rightButton = rightButton; + + if (self.keypadView) { + self.keypadView.rightAccessoryView = rightButton; + } + else { + [self addSubview:_rightButton]; + } +} + +- (CGFloat)keypadButtonInset +{ + UIView *button = self.keypadView.keypadButtons.firstObject; + return CGRectGetMidX(button.frame); +} + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + _contentAlpha = contentAlpha; + + self.titleView.alpha = contentAlpha; + self.titleLabel.alpha = contentAlpha; + self.subtitleLabel.alpha = contentAlpha; + self.inputField.contentAlpha = contentAlpha; + self.keypadView.contentAlpha = contentAlpha; + self.keypadView.leftAccessoryView.alpha = contentAlpha; + self.keypadView.rightAccessoryView.alpha = contentAlpha; + self.leftButton.alpha = contentAlpha; + self.rightButton.alpha = contentAlpha; +} + +- (void)setPasscode:(NSString *)passcode +{ + [self.inputField setPasscode:passcode]; +} + +- (NSString *)passcode +{ + return self.inputField.passcode; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h new file mode 100644 index 0000000000..8179622f61 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h @@ -0,0 +1,45 @@ +// +// TOPasscodeSettingsKeypadButton.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +@class TOPasscodeButtonLabel; + +/** + A single button view that is used in a number keypad views styled in + a pseudo-skeuomorphic style. + */ +@interface TOPasscodeSettingsKeypadButton : UIButton + +/** Background Images */ +@property (nonatomic, strong) UIImage *buttonBackgroundImage; +@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; + +/* Inset of the label view from the bottom to account for the bevel */ +@property (nonatomic, assign) CGFloat bottomInset; + +/* The button label containing the number and lettering */ +@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; + ++ (TOPasscodeSettingsKeypadButton *)button; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m new file mode 100644 index 0000000000..fa41bd4587 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m @@ -0,0 +1,103 @@ +// +// TOPasscodeSettingsKeypadButton.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsKeypadButton.h" +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeSettingsKeypadButton () + +@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; + +@end + +@implementation TOPasscodeSettingsKeypadButton + ++ (TOPasscodeSettingsKeypadButton *)button +{ + TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton buttonWithType:UIButtonTypeCustom]; + button.frame = CGRectMake(0,0,100,60); + return button; +} + +#pragma mark - Lazy Accessor - +- (TOPasscodeButtonLabel *)buttonLabel +{ + if (_buttonLabel) { return _buttonLabel; } + + CGRect frame = self.bounds; + frame.size.height -= self.bottomInset; + + _buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:frame]; + _buttonLabel.userInteractionEnabled = NO; + _buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_buttonLabel]; + + return _buttonLabel; +} + +#pragma mark - Layout Accessor - +- (void)setBottomInset:(CGFloat)bottomInset +{ + _bottomInset = bottomInset; + + CGRect frame = self.bounds; + frame.size.height -= _bottomInset; + self.buttonLabel.frame = frame; + [self setNeedsLayout]; +} + +#pragma mark - Control Accessor - +- (void)setEnabled:(BOOL)enabled +{ + [super setEnabled:enabled]; + self.buttonLabel.alpha = enabled ? 1.0f : 0.5f; +} + +#pragma mark - Background Image Accessor - + +- (void)setHighlighted:(BOOL)highlighted { + [self.layer removeAllAnimations]; + [UIView transitionWithView:self + duration:0.25 + options:UIViewAnimationOptionTransitionCrossDissolve | + UIViewAnimationOptionAllowAnimatedContent | + UIViewAnimationOptionAllowUserInteraction + animations:^{ + [super setHighlighted:highlighted]; + } completion:nil]; +} + +- (void)setButtonBackgroundImage:(UIImage *)buttonBackgroundImage +{ + [self setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal]; +} + +- (UIImage *)buttonBackgroundImage { return [self backgroundImageForState:UIControlStateNormal]; } + +- (void)setButtonTappedBackgroundImage:(UIImage *)buttonTappedBackgroundImage +{ + [self setBackgroundImage:buttonTappedBackgroundImage forState:UIControlStateHighlighted]; +} + +- (UIImage *)buttonTappedBackgroundImage { return [self backgroundImageForState:UIControlStateHighlighted]; } + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h new file mode 100644 index 0000000000..13234bd07b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h @@ -0,0 +1,74 @@ +// +// TOPasscodeSettingsKeypadView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import +#import "TOPasscodeViewControllerConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + A keypad view of 9 buttons that allow numerical input on both iPad and iPhone. + Designed to match the base system, with a pseudo-skeuomorphical styling. + */ +@interface TOPasscodeSettingsKeypadView : UIView + +/* Whether the control is allowing input */ +@property (nonatomic, assign) BOOL enabled; + +/* Whether the view is currently light mode or dark. */ +@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; + +/* The color of the separator line */ +@property (nonatomic, strong) UIColor *separatorLineColor; + +/* Labels in the buttons are laid out horizontally */ +@property (nonatomic, assign) BOOL buttonLabelHorizontalLayout; + +/* If overridden, the foreground color of the buttons */ +@property (nonatomic, assign) CGFloat keypadButtonBorderThickness; + +/* Untapped background images */ +@property (nonatomic, strong, null_resettable) UIColor *keypadButtonForegroundColor; +@property (nonatomic, strong, null_resettable) UIColor *keypadButtonBorderColor; + +/* Tapped background images */ +@property (nonatomic, strong, null_resettable) UIColor *keypadButtonTappedForegroundColor; +@property (nonatomic, strong, nullable) UIColor *keypadButtonTappedBorderColor; + +/* Button label styling */ +@property (nonatomic, strong) UIFont *keypadButtonNumberFont; +@property (nonatomic, strong) UIFont *keypadButtonLetteringFont; +@property (nonatomic, strong) UIColor *keypadButtonLabelTextColor; +@property (nonatomic, assign) CGFloat keypadButtonVerticalSpacing; +@property (nonatomic, assign) CGFloat keypadButtonHorizontalSpacing; +@property (nonatomic, assign) CGFloat keypadButtonLetteringSpacing; + +/* Callback handlers */ +@property (nonatomic, copy) void (^numberButtonTappedHandler)(NSInteger number); +@property (nonatomic, copy) void (^deleteButtonTappedHandler)(void); + +/* In really small sizes, set the keypad labels to horizontal */ +- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m new file mode 100644 index 0000000000..eda81bb61b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m @@ -0,0 +1,395 @@ +// +// TOPasscodeSettingsKeypadView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsKeypadView.h" +#import "TOPasscodeSettingsKeypadButton.h" +#import "TOPasscodeButtonLabel.h" +#import "TOSettingsKeypadImage.h" + +const CGFloat kTOPasscodeSettingsKeypadButtonInnerSpacing = 7.0f; +const CGFloat kTOPasscodeSettingsKeypadButtonOuterSpacing = 7.0f; +const CGFloat kTOPasscodeSettingsKeypadCornderRadius = 12.0f; + +@interface TOPasscodeSettingsKeypadView () + +@property (nonatomic, strong) UIView *separatorView; +@property (nonatomic, strong) NSArray *keypadButtons; +@property (nonatomic, strong) UIButton *deleteButton; + +@property (nonatomic, strong) UIImage *buttonBackgroundImage; +@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; + +@end + +@implementation TOPasscodeSettingsKeypadView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + /* Button label styling */ + _keypadButtonNumberFont = [UIFont systemFontOfSize:32.0f weight:UIFontWeightRegular]; + _keypadButtonLetteringFont = [UIFont systemFontOfSize:11.0f weight:UIFontWeightRegular]; + _keypadButtonVerticalSpacing = 2.0f; + _keypadButtonHorizontalSpacing = 3.0f; + _keypadButtonLetteringSpacing = 2.0f; + + CGSize viewSize = self.frame.size; + CGFloat height = 1.0f / [[UIScreen mainScreen] scale]; + self.separatorView = [[UIView alloc] initWithFrame:(CGRect){CGPointZero,{viewSize.width, height}}]; + self.separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [self addSubview:self.separatorView]; + + [self setUpKeypadButtons]; + [self setUpDeleteButton]; + + [self setUpDefaultValuesForStye:_style]; + [self applyTheme]; +} + +- (void)setUpKeypadButtons +{ + NSInteger numberOfButtons = 10; + NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", + @"MNO", @"PQRS", @"TUV", @"WXYZ"]; + + NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:10]; + for (NSInteger i = 0; i < numberOfButtons; i++) { + NSInteger number = (i+1) % 10; // Wrap around 0 at the end + TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton button]; + button.buttonLabel.numberString = [NSString stringWithFormat:@"%ld", (long)number]; + button.bottomInset = 2.0f; + button.tag = number; + + if (i > 0) { + NSInteger j = i - 1; + if (j < letteredTitles.count) { + button.buttonLabel.letteringString = letteredTitles[j]; + } + } + + [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchDown]; + + [self addSubview:button]; + [buttons addObject:button]; + } + + self.keypadButtons = [NSArray arrayWithArray:buttons]; +} + +- (void)setUpDeleteButton +{ + UIImage *deleteIcon = [TOSettingsKeypadImage deleteIcon]; + self.deleteButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.deleteButton setImage:deleteIcon forState:UIControlStateNormal]; + self.deleteButton.contentMode = UIViewContentModeCenter; + self.deleteButton.frame = (CGRect){CGPointZero, deleteIcon.size}; + self.deleteButton.tintColor = [UIColor blackColor]; + [self.deleteButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self addSubview:self.deleteButton]; +} + +- (void)setUpDefaultValuesForStye:(TOPasscodeSettingsViewStyle)style +{ + BOOL isDark = style == TOPasscodeSettingsViewStyleDark; + + // Keypad label + self.keypadButtonLabelTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; + + self.keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.35f alpha:1.0f] : [UIColor whiteColor]; + self.keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.45f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; + + // Button border color + UIColor *borderColor = nil; + if (isDark) { + borderColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; + } + else { + borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; + } + self.keypadButtonBorderColor = borderColor; + + // Background Color + UIColor *backgroundColor = nil; + if (isDark) { + backgroundColor = [UIColor colorWithWhite:0.18f alpha:1.0f]; + } + else { + backgroundColor = [UIColor colorWithRed:220.0f/255.0f green:225.0f/255.0f blue:232.0f/255.0f alpha:1.0f]; + } + self.backgroundColor = backgroundColor; + + // Separator lines + UIColor *separatorColor = nil; + if (isDark) { + separatorColor = [UIColor colorWithWhite:0.25f alpha:1.0f]; + } + else { + separatorColor = [UIColor colorWithWhite:0.7f alpha:1.0f]; + } + self.separatorView.backgroundColor = separatorColor; + + self.deleteButton.tintColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; +} + +- (void)setUpImagesIfNeeded +{ + if (self.buttonBackgroundImage && self.buttonTappedBackgroundImage) { + return; + } + + if (self.buttonBackgroundImage == nil) { + self.buttonBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius + foregroundColor:self.keypadButtonForegroundColor + edgeColor:self.keypadButtonBorderColor + edgeThickness:2.0f]; + } + + if (self.buttonTappedBackgroundImage == nil) { + self.buttonTappedBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius + foregroundColor:self.keypadButtonTappedForegroundColor + edgeColor:self.keypadButtonBorderColor + edgeThickness:2.0f]; + } + + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.buttonBackgroundImage = self.buttonBackgroundImage; + button.buttonTappedBackgroundImage = self.buttonTappedBackgroundImage; + } +} + +- (void)applyTheme +{ + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.buttonLabel.textColor = self.keypadButtonLabelTextColor; + button.buttonLabel.letteringCharacterSpacing = self.keypadButtonLetteringSpacing; + button.buttonLabel.letteringVerticalSpacing = self.keypadButtonVerticalSpacing; + button.buttonLabel.letteringHorizontalSpacing = self.keypadButtonHorizontalSpacing; + button.buttonLabel.numberLabelFont = self.keypadButtonNumberFont; + button.buttonLabel.letteringLabelFont = self.keypadButtonLetteringFont; + } +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self setUpImagesIfNeeded]; + + CGFloat outerSpacing = kTOPasscodeSettingsKeypadButtonOuterSpacing; + CGFloat innerSpacing = kTOPasscodeSettingsKeypadButtonInnerSpacing; + + CGSize viewSize = self.bounds.size; + CGSize buttonSize = CGSizeZero; + + viewSize.width -= (outerSpacing * 2.0f); + viewSize.height -= (outerSpacing * 2.0f); + + // Pull the buttons up to avoid overlapping the home indicator on iPhone X + if (@available(iOS 11.0, *)) { + viewSize.height -= self.safeAreaInsets.bottom; + } + + // Four rows of three buttons + buttonSize.width = floorf((viewSize.width - (innerSpacing * 2.0f)) / 3.0f); + buttonSize.height = floorf((viewSize.height - (innerSpacing * 3.0f)) / 4.0f); + + CGPoint point = CGPointMake(outerSpacing, outerSpacing); + CGRect buttonFrame = (CGRect){point, buttonSize}; + + NSInteger i = 0; + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.frame = buttonFrame; + buttonFrame.origin.x += buttonFrame.size.width + innerSpacing; + + if (++i % 3 == 0) { + buttonFrame.origin.x = outerSpacing; + buttonFrame.origin.y += buttonFrame.size.height + innerSpacing; + } + + if (button == self.keypadButtons.lastObject) { + button.frame = buttonFrame; + } + } + + //Layout delete button + CGSize boundsSize = self.bounds.size; + + // Adjust for home indicator on iPhone X + if (@available(iOS 11.0, *)) { + boundsSize.height -= self.safeAreaInsets.bottom; + } + + CGRect frame = self.deleteButton.frame; + frame.size = buttonSize; + frame.origin.x = boundsSize.width - (outerSpacing + buttonSize.width * 0.5f); + frame.origin.x -= (CGRectGetWidth(frame) * 0.5f); + frame.origin.y = boundsSize.height - (outerSpacing + buttonSize.height * 0.5f); + frame.origin.y -= (CGRectGetHeight(frame) * 0.5f); + self.deleteButton.frame = frame; +} + +#pragma mark - Interaction - +- (void)buttonTapped:(id)sender +{ + // Handler for the delete button + if (sender == self.deleteButton) { + if (self.deleteButtonTappedHandler) { + self.deleteButtonTappedHandler(); + } + return; + } + + // Handler for the keypad buttons + UIButton *button = (UIButton *)sender; + NSInteger number = button.tag; + + [[UIDevice currentDevice] playInputClick]; + + if (self.numberButtonTappedHandler) { + self.numberButtonTappedHandler(number); + } +} + +#pragma mark - Accessors - + +- (void)setStyle:(TOPasscodeSettingsViewStyle)style +{ + if (style == _style) { + return; + } + + _style = style; + [self setUpDefaultValuesForStye:_style]; + [self applyTheme]; +} + +#pragma mark - Label Layout - +- (void)setButtonLabelHorizontalLayout:(BOOL)buttonLabelHorizontalLayout +{ + [self setButtonLabelHorizontalLayout:buttonLabelHorizontalLayout animated:NO]; +} + +- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated +{ + if (horizontal == _buttonLabelHorizontalLayout) { return; } + + _buttonLabelHorizontalLayout = horizontal; + + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + if (!animated) { + button.buttonLabel.horizontalLayout = horizontal; + continue; + } + + UIView *snapshotView = [button.buttonLabel snapshotViewAfterScreenUpdates:NO]; + snapshotView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; + [button addSubview:snapshotView]; + + button.buttonLabel.horizontalLayout = horizontal; + [button.buttonLabel setNeedsLayout]; + [button.buttonLabel layoutIfNeeded]; + + [button.buttonLabel.layer removeAllAnimations]; + for (CALayer *sublayer in button.buttonLabel.layer.sublayers) { + [sublayer removeAllAnimations]; + } + + button.buttonLabel.alpha = 0.0f; + [UIView animateWithDuration:0.4f animations:^{ + button.buttonLabel.alpha = 1.0f; + snapshotView.alpha = 0.0f; + snapshotView.center = button.buttonLabel.center; + } completion:^(BOOL complete) { + [snapshotView removeFromSuperview]; + }]; + } +} + +#pragma mark - Null Resettable Accessors - +- (void)setKeypadButtonForegroundColor:(nullable UIColor *)keypadButtonForegroundColor +{ + if (keypadButtonForegroundColor == _keypadButtonForegroundColor) { return; } + _keypadButtonForegroundColor = keypadButtonForegroundColor; + + if (_keypadButtonForegroundColor == nil) { + BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; + _keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.3f alpha:1.0f] : [UIColor whiteColor]; + } + + self.buttonBackgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setKeypadButtonBorderColor:(nullable UIColor *)keypadButtonBorderColor +{ + if (keypadButtonBorderColor == _keypadButtonBorderColor) { return; } + _keypadButtonBorderColor = keypadButtonBorderColor; + + if (_keypadButtonBorderColor == nil) { + BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; + UIColor *borderColor = nil; + if (isDark) { + borderColor = [UIColor colorWithWhite:0.2 alpha:1.0f]; + } + else { + borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; + } + _keypadButtonBorderColor = borderColor; + } + + self.buttonBackgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setKeypadButtonTappedForegroundColor:(nullable UIColor *)keypadButtonTappedForegroundColor +{ + if (keypadButtonTappedForegroundColor == _keypadButtonTappedForegroundColor) { return; } + _keypadButtonTappedForegroundColor = keypadButtonTappedForegroundColor; + + if (_keypadButtonTappedForegroundColor == nil) { + BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; + _keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.4f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; + } + + self.buttonTappedBackgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setEnabled:(BOOL)enabled +{ + _enabled = enabled; + + for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { + button.enabled = enabled; + } + + self.deleteButton.enabled = enabled; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h new file mode 100644 index 0000000000..5455a70586 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h @@ -0,0 +1,43 @@ +// +// TOPasscodeSettingsWarningLabel.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +/** + When a user enters an incorrect passcode in the settings interface, + this view is displayed to show the number of failed attempts. + */ +@interface TOPasscodeSettingsWarningLabel : UIImageView + +/** The number of incorrect passcode attempts to display */ +@property (nonatomic, assign) NSInteger numberOfWarnings; + +/** The font of the text */ +@property (nonatomic, strong) UIFont *textFont; + +/** The background color of the view */ +@property (nonatomic, strong) UIColor *backgroundColor; + +/** Set the padding around the label */ +@property (nonatomic, assign) CGSize textPadding; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m new file mode 100644 index 0000000000..266f360be6 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m @@ -0,0 +1,154 @@ +// +// TOPasscodeSettingsWarningLabel.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeSettingsWarningLabel.h" + +@interface TOPasscodeSettingsWarningLabel () +@property (nonatomic, strong) UILabel *label; +@end + +@implementation TOPasscodeSettingsWarningLabel + +@synthesize backgroundColor = __backgroundColor; + +#pragma mark - View Setup - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + + return self; +} + +- (void)setUp +{ + _numberOfWarnings = 0; + _textPadding = CGSizeMake(14.0f, 6.0f); + + self.tintColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; + + self.label = [[UILabel alloc] initWithFrame:CGRectZero]; + self.label.backgroundColor = [UIColor clearColor]; + self.label.textAlignment = NSTextAlignmentCenter; + self.label.textColor = [UIColor whiteColor]; + self.label.font = [UIFont systemFontOfSize:15.0f]; + [self setTextForCount:0]; + [self.label sizeToFit]; + [self addSubview:self.label]; +} + +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + [self setBackgroundImageIfNeeded]; +} + +#pragma mark - View Layout - + +- (void)sizeToFit +{ + [super sizeToFit]; + [self.label sizeToFit]; + + CGRect labelFrame = self.label.frame; + CGRect frame = self.frame; + + labelFrame = CGRectInset(labelFrame, -self.textPadding.width, -self.textPadding.height); + frame.size = labelFrame.size; + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + CGRect frame = self.frame; + CGRect labelFrame = self.label.frame; + + labelFrame.origin.x = (CGRectGetWidth(frame) - CGRectGetWidth(labelFrame)) * 0.5f; + labelFrame.origin.y = (CGRectGetHeight(frame) - CGRectGetHeight(labelFrame)) * 0.5f; + self.label.frame = labelFrame; +} + +#pragma mark - View State Handling - + +- (void)setTextForCount:(NSInteger)count +{ + NSString *text = nil; + if (count == 1) { + text = NSLocalizedString(@"1 Failed Passcode Attempt", @""); + } + else { + text = [NSString stringWithFormat:NSLocalizedString(@"%d Failed Passcode Attempts", @""), count]; + } + self.label.text = text; + + [self sizeToFit]; +} + +#pragma mark - Background Image Managements - + +- (void)setBackgroundImageIfNeeded +{ + // Don't bother if we're not in a view + if (self.superview == nil) { return; } + + // Compare the view height and don't proceed if + if (lround(self.image.size.height) == lround(self.frame.size.height)) { return; } + + // Create the image + self.image = [[self class] roundedBackgroundImageWithHeight:self.frame.size.height]; +} + ++ (UIImage *)roundedBackgroundImageWithHeight:(CGFloat)height +{ + UIImage *image = nil; + CGRect frame = CGRectZero; + frame.size.width = height + 1.0; + frame.size.height = height; + + UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); + { + UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:height * 0.5f]; + [[UIColor blackColor] setFill]; + [path fill]; + + image = UIGraphicsGetImageFromCurrentImageContext(); + } + UIGraphicsEndImageContext(); + + CGFloat halfHeight = height * 0.5f; + UIEdgeInsets insets = UIEdgeInsetsMake(halfHeight, halfHeight, halfHeight, halfHeight); + image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + image = [image resizableImageWithCapInsets:insets]; + return image; +} + +#pragma mark - Accessors - + +- (void)setNumberOfWarnings:(NSInteger)numberOfWarnings +{ + _numberOfWarnings = numberOfWarnings; + [self setTextForCount:_numberOfWarnings]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h new file mode 100644 index 0000000000..12b4d00fef --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h @@ -0,0 +1,61 @@ +// +// TOPasscodeButtonLabel.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A view that manages two label subviews: a larger label showing a single number + and a smaller label showing lettering as well. + */ +@interface TOPasscodeButtonLabel : UIView + +// Draws the lettering label to the side +@property (nonatomic, assign) BOOL horizontalLayout; + +// The strings of both labels +@property (nonatomic, copy) NSString *numberString; +@property (nonatomic, copy, nullable) NSString *letteringString; + +// The color of both labels +@property (nonatomic, strong) UIColor *textColor; + +// The label views +@property (nonatomic, readonly) UILabel *numberLabel; +@property (nonatomic, readonly) UILabel *letteringLabel; + +// The fonts for each label (In case they are nil) +@property (nonatomic, strong) UIFont *numberLabelFont; +@property (nonatomic, strong) UIFont *letteringLabelFont; + +// Has initial default values +@property (nonatomic, assign) CGFloat letteringCharacterSpacing; +@property (nonatomic, assign) CGFloat letteringVerticalSpacing; +@property (nonatomic, assign) CGFloat letteringHorizontalSpacing; + +// Whether the number label is centered vertically or not (NO by default) +@property (nonatomic, assign) BOOL verticallyCenterNumberLabel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m new file mode 100644 index 0000000000..d3df12ab45 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m @@ -0,0 +1,186 @@ +// +// TOPasscodeButtonLabel.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeButtonLabel.h" + +@interface TOPasscodeButtonLabel () + +@property (nonatomic, strong, readwrite) UILabel *numberLabel; +@property (nonatomic, strong, readwrite) UILabel *letteringLabel; + +@end + +@implementation TOPasscodeButtonLabel + +#pragma mark - View Setup - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _letteringVerticalSpacing = 6.0f; + _letteringCharacterSpacing = 3.0f; + _letteringHorizontalSpacing = 5.0f; + _numberLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; + _letteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; + [self setUpViews]; + } + + return self; +} + +- (void)setUpViews +{ + if (!self.numberLabel) { + self.numberLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.numberLabel.text = self.numberString; + self.numberLabel.textColor = self.textColor; + self.numberLabel.font = self.numberLabelFont; + [self.numberLabel sizeToFit]; + [self addSubview:self.numberLabel]; + } + + // Create the lettering string only if we have a lettering value for it + if (!self.letteringLabel && self.letteringString.length > 0) { + self.letteringLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + self.letteringLabel.textColor = self.textColor; + self.letteringLabel.font = self.letteringLabelFont; + [self.letteringLabel sizeToFit]; + [self addSubview:self.letteringLabel]; + [self updateLetteringLabelText]; + } +} + +#pragma mark - View Layout - + +- (void)updateLetteringLabelText +{ + if (self.letteringString.length == 0) { + return; + } + + NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString:self.letteringString]; + [attrStr addAttribute:NSKernAttributeName value:@(_letteringCharacterSpacing) range:NSMakeRange(0, attrStr.length-1)]; + self.letteringLabel.attributedText = attrStr; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGSize viewSize = self.bounds.size; + + [self.numberLabel sizeToFit]; + [self.letteringLabel sizeToFit]; + + CGFloat numberVerticalHeight = self.numberLabelFont.capHeight; + CGFloat letteringVerticalHeight = self.letteringLabelFont.capHeight; + CGFloat textTotalHeight = (numberVerticalHeight+2.0f) + self.letteringVerticalSpacing + (letteringVerticalHeight+2.0f); + + CGRect frame = self.numberLabel.frame; + frame.size.height = ceil(numberVerticalHeight) + 2.0f; + frame.origin.x = ceilf((viewSize.width - frame.size.width) * 0.5f); + + if (!self.horizontalLayout && !self.verticallyCenterNumberLabel) { + frame.origin.y = floorf((viewSize.height - textTotalHeight) * 0.5f); + } + else { + frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); + } + self.numberLabel.frame = CGRectIntegral(frame); + + if (self.letteringLabel) { + CGFloat y = CGRectGetMaxY(frame); + y += self.letteringVerticalSpacing; + + frame = self.letteringLabel.frame; + frame.size.height = ceil(letteringVerticalHeight) + 2.0f; + + if (!self.horizontalLayout) { + frame.origin.y = floorf(y); + frame.origin.x = (viewSize.width - frame.size.width) * 0.5f; + } + else { + frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); + frame.origin.x = CGRectGetMaxX(self.numberLabel.frame) + self.letteringHorizontalSpacing; + } + + self.letteringLabel.frame = CGRectIntegral(frame); + } +} + +#pragma mark - Accessors - + +- (void)setTextColor:(UIColor *)textColor +{ + if (textColor == _textColor) { return; } + _textColor = textColor; + + self.numberLabel.textColor = _textColor; + self.letteringLabel.textColor = _textColor; +} +/***********************************************************/ + +- (void)setNumberString:(NSString *)numberString +{ + self.numberLabel.text = numberString; + [self setNeedsLayout]; +} + +- (NSString *)numberString { return self.numberLabel.text; } + +/***********************************************************/ + +- (void)setLetteringString:(NSString *)letteringString +{ + _letteringString = [letteringString copy]; + [self setUpViews]; + [self updateLetteringLabelText]; + [self setNeedsLayout]; +} + +/***********************************************************/ + +- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing +{ + _letteringCharacterSpacing = letteringCharacterSpacing; + [self updateLetteringLabelText]; +} + +/***********************************************************/ + +- (void)setNumberLabelFont:(UIFont *)numberLabelFont +{ + if (_numberLabelFont == numberLabelFont) { return; } + _numberLabelFont = numberLabelFont; + self.numberLabel.font = _numberLabelFont; +} + +/***********************************************************/ + +- (void)setLetteringLabelFont:(UIFont *)letteringLabelFont +{ + if (_letteringLabelFont == letteringLabelFont) { return; } + _letteringLabelFont = letteringLabelFont; + self.letteringLabel.font = letteringLabelFont; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h new file mode 100644 index 0000000000..5dc9ca2251 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h @@ -0,0 +1,46 @@ +// +// TOPasscodeCircleView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A view containing two circle image views that can animate + between filled and hollow, whilst maintaining compatibility + with translucency views. + */ +@interface TOPasscodeCircleView : UIView + +/* The circle patterns used for neutral and highlighted states. */ +@property (nonatomic, strong) UIImage *circleImage; +@property (nonatomic, strong) UIImage *highlightedCircleImage; + +/* Whether the highlighted view is visible. */ +@property (nonatomic, assign) BOOL isHighlighted; + +/* Animate the circle to be highlighted */ +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m new file mode 100644 index 0000000000..3bb5de974a --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m @@ -0,0 +1,89 @@ +// +// TOPasscodeCircleView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeCircleView.h" + +@interface TOPasscodeCircleView () +@property (nonatomic, strong) UIImageView *bottomView; +@property (nonatomic, strong) UIImageView *topView; +@end + +@implementation TOPasscodeCircleView + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + self.userInteractionEnabled = NO; + + self.bottomView = [[UIImageView alloc] initWithFrame:self.bounds]; + self.bottomView.userInteractionEnabled = NO; + self.bottomView.contentMode = UIViewContentModeCenter; + self.bottomView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:self.bottomView]; + + self.topView = [[UIImageView alloc] initWithFrame:self.bounds]; + self.topView.userInteractionEnabled = NO; + self.topView.contentMode = UIViewContentModeCenter; + self.topView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.topView.alpha = 0.0f; + [self addSubview:self.topView]; + } + + return self; +} + +- (void)setIsHighlighted:(BOOL)isHighlighted +{ + [self setHighlighted:isHighlighted animated:NO]; +} + +- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated +{ + if (highlighted == self.isHighlighted) { return; } + + _isHighlighted = highlighted; + + void (^animationBlock)(void) = ^{ + self.topView.alpha = highlighted ? 1.0f : 0.0f; + }; + + if (!animated) { + animationBlock(); + return; + } + + [UIView animateWithDuration:0.45f animations:animationBlock]; +} + +- (void)setCircleImage:(UIImage *)circleImage +{ + _circleImage = circleImage; + self.bottomView.image = circleImage; +} + +- (void)setHighlightedCircleImage:(UIImage *)highlightedCircleImage +{ + _highlightedCircleImage = highlightedCircleImage; + self.topView.image = highlightedCircleImage; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h new file mode 100644 index 0000000000..a448d8ca0b --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h @@ -0,0 +1,54 @@ +// +// TOPasscodeFixedInputView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +@class TOPasscodeCircleView; + +/** + A basic content view showing a row of circles that can be used to represent + a fixed size passcode. + */ +@interface TOPasscodeFixedInputView : UIView + +/* The size of each circle in this view (Default is 16) */ +@property (nonatomic, assign) CGFloat circleDiameter; + +/* The spacing between each circle (Default is 25.0f) */ +@property (nonatomic, assign) CGFloat circleSpacing; + +/* The number of circles in this view (Default is 4) */ +@property (nonatomic, assign) NSInteger length; + +/* The number of highlighted circles */ +@property (nonatomic, assign) NSInteger highlightedLength; + +/* The circle views managed by this view */ +@property (nonatomic, strong, readonly) NSArray *circleViews; + +/* Init with a set number of circles */ +- (instancetype)initWithLength:(NSInteger)length; + +/* Set the number of highlighted circles */ +- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m new file mode 100644 index 0000000000..e4dac42805 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m @@ -0,0 +1,171 @@ +// +// TOPasscodeFixedInputView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeFixedInputView.h" +#import "TOPasscodeCircleView.h" +#import "TOPasscodeCircleImage.h" + +@interface TOPasscodeFixedInputView () + +@property (nonatomic, strong, readwrite) NSArray *circleViews; +@property (nonatomic, strong) UIImage *circleImage; +@property (nonatomic, strong) UIImage *highlightedCircleImage; + +@end + +@implementation TOPasscodeFixedInputView + +#pragma mark - Object Creation - + +- (instancetype)initWithLength:(NSInteger)length +{ + if (self = [self initWithFrame:CGRectZero]) { + _length = length; + } + + return self; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _circleSpacing = 25.0f; + _circleDiameter = 16.0f; + _length = 4; + } + + return self; +} + +#pragma mark - View Configuration - + +- (void)sizeToFit +{ + // Resize the view to encompass the circles + CGRect frame = self.frame; + frame.size.width = (_circleDiameter * _length) + (_circleSpacing * (_length - 1)) + 2.0f; + frame.size.height = _circleDiameter + 2.0f; + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + CGRect frame = CGRectZero; + frame.size = (CGSize){self.circleDiameter + 2.0f, self.circleDiameter + 2.0f}; + + for (TOPasscodeCircleView *circleView in self.circleViews) { + circleView.frame = frame; + frame.origin.x += self.circleDiameter + self.circleSpacing; + } +} + +#pragma mark - State Configuration - + +- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated +{ + NSInteger i = 0; + for (TOPasscodeCircleView *circleView in self.circleViews) { + [circleView setHighlighted:(i < highlightedLength) animated:animated]; + i++; + } +} + +#pragma mark - Circle View Configuration - + +- (void)setCircleViewsForLength:(NSInteger)length +{ + NSMutableArray *circleViews = [NSMutableArray array]; + if (self.circleViews) { + [circleViews addObjectsFromArray:self.circleViews]; + } + + [UIView performWithoutAnimation:^{ + while (circleViews.count != length) { + // Remove any extra circle views + if (circleViews.count > length) { + TOPasscodeCircleView *lastCircle = circleViews.lastObject; + [lastCircle removeFromSuperview]; + [circleViews removeLastObject]; + continue; + } + + // Add any new circle views + TOPasscodeCircleView *newCircleView = [[TOPasscodeCircleView alloc] init]; + [self setImagesOfCircleView:newCircleView]; + [self addSubview:newCircleView]; + [circleViews addObject:newCircleView]; + } + + self.circleViews = [NSArray arrayWithArray:circleViews]; + [self setNeedsLayout]; + [self layoutIfNeeded]; + }]; +} + +- (void)setCircleImagesForDiameter:(CGFloat)diameter +{ + self.circleImage = [TOPasscodeCircleImage hollowCircleImageOfSize:diameter strokeWidth:1.2f padding:1.0f]; + self.highlightedCircleImage = [TOPasscodeCircleImage circleImageOfSize:diameter inset:0.5f padding:1.0f antialias:YES]; + + for (TOPasscodeCircleView *circleView in self.circleViews) { + [self setImagesOfCircleView:circleView]; + } +} + +- (void)setImagesOfCircleView:(TOPasscodeCircleView *)circleView +{ + circleView.circleImage = self.circleImage; + circleView.highlightedCircleImage = self.highlightedCircleImage; +} + +#pragma mark - Accessors - + +- (NSArray *)circleViews +{ + if (_circleViews) { return _circleViews; } + _circleViews = [NSArray array]; + [self setCircleViewsForLength:self.length]; + [self setCircleImagesForDiameter:self.circleDiameter]; + return _circleViews; +} + +- (void)setCircleDiameter:(CGFloat)circleDiameter +{ + if (circleDiameter == _circleDiameter) { return; } + _circleDiameter = circleDiameter; + [self setCircleImagesForDiameter:_circleDiameter]; + [self sizeToFit]; +} + +- (void)setLength:(NSInteger)length +{ + if (_length == length) { return; } + _length = length; + [self setCircleViewsForLength:length]; +} + +- (void)setHighlightedLength:(NSInteger)highlightedLength +{ + [self setHighlightedLength:highlightedLength animated:NO]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h new file mode 100644 index 0000000000..05103b27d9 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h @@ -0,0 +1,111 @@ +// +// TOPasscodeInputField.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +#import "TOPasscodeFixedInputView.h" +#import "TOPasscodeVariableInputView.h" + + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, TOPasscodeInputFieldStyle) { + TOPasscodeInputFieldStyleFixed, // The passcode explicitly requires a specific number of characters (Shows hollow circles) + TOPasscodeInputFieldStyleVariable // The passcode can be any arbitrary number of characters (Shows an empty rectangle) +}; + +/** + An abstract input view capable of receiving different types of passcodes. + When a fixed character passcode is specified, the view shows a row of circles. + When a variable passcode is specified, a rounded rectangle is shown. + */ +@interface TOPasscodeInputField : UIView + +/* The visual effects view used to control the vibrancy of the input field */ +@property (nonatomic, strong, readonly) UIVisualEffectView *visualEffectView; + +/* The input style of this control */ +@property (nonatomic, assign) TOPasscodeInputFieldStyle style; + +/* A row of hollow circles at a preset length. Valid only when `style` is set to `fixed` */ +@property (nonatomic, readonly, nullable) TOPasscodeFixedInputView *fixedInputView; + +/* A rounded rectangle representing a passcode of arbitrary length. Valid only when `style` is set to `variable`. */ +@property (nonatomic, readonly, nullable) TOPasscodeVariableInputView *variableInputView; + +/* The 'submit' button shown when `showSubmitButton` is true. */ +@property (nonatomic, readonly, nullable) UIButton *submitButton; + +/* Shows an 'OK' button next to the view when characters have been added. */ +@property (nonatomic, assign) BOOL showSubmitButton; + +/* The amount of spacing between the 'OK' button and the passcode field */ +@property (nonatomic, assign) CGFloat submitButtonSpacing; + +/* The amount of spacing between the 'OK' button and the passcode field */ +@property (nonatomic, assign) CGFloat submitButtonVerticalSpacing; + +/* The font size of the submit button */ +@property (nonatomic, assign) CGFloat submitButtonFontSize; + +/* The current passcode entered into this view */ +@property (nonatomic, copy, nullable) NSString *passcode; + +/* If this view is directly receiving input, this can change the `UIKeyboard` appearance. */ +@property (nonatomic, assign) UIKeyboardAppearance keyboardAppearance; + +/* The type of button used for the 'Done' button in the keyboard */ +@property(nonatomic, assign) UIReturnKeyType returnKeyType; + +/* The alpha value of the views in this view (For tranclucent styling) */ +@property (nonatomic, assign) CGFloat contentAlpha; + +/* Whether the view may be tapped to enable character input (Default is NO) */ +@property (nonatomic, assign) BOOL enabled; + +/** Called when the number of digits has been entered, or the user tapped 'Done' on the keyboard */ +@property (nonatomic, copy) void (^passcodeCompletedHandler)(NSString *code); + +/** Horizontal layout. The 'OK' button will be placed under the text field */ +@property (nonatomic, assign) BOOL horizontalLayout; + +/* Init with the target length needed for this passcode */ +- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style; + +/* Replace the passcode with this one, and animate the transition. */ +- (void)setPasscode:(nullable NSString *)passcode animated:(BOOL)animated; + +/* Add additional characters to the end of the passcode, and animate if desired. */ +- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated; + +/* Delete a number of characters from the end, animated if desired. */ +- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated; + +/* Plays a shaking animation and resets the passcode back to empty */ +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; + +/* Animates the OK button changing location. */ +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m new file mode 100644 index 0000000000..0b3227c0ea --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m @@ -0,0 +1,423 @@ +// +// TOPasscodeInputField.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeInputField.h" + +#import "TOPasscodeVariableInputView.h" +#import "TOPasscodeFixedInputView.h" + +#import + +@interface TOPasscodeInputField () + +// Convenience getters +@property (nonatomic, readonly) UIView *inputField; // Returns whichever input field is currently visible +@property (nonatomic, readonly) NSInteger maximumPasscodeLength; // The mamximum number of characters allowed (0 if uncapped) + +@property (nonatomic, strong, readwrite) TOPasscodeFixedInputView *fixedInputView; +@property (nonatomic, strong, readwrite) TOPasscodeVariableInputView *variableInputView; +@property (nonatomic, strong, readwrite) UIButton *submitButton; +@property (nonatomic, strong, readwrite) UIVisualEffectView *visualEffectView; + +@end + +@implementation TOPasscodeInputField + +#pragma mark - View Set-up - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [self setUp]; + [self setUpForStyle:TOPasscodeInputFieldStyleFixed]; + } + + return self; +} + +- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style +{ + if (self = [self initWithFrame:CGRectZero]) { + _style = style; + [self setUp]; + [self setUpForStyle:style]; + } + + return self; +} + +- (void)setUp +{ + self.backgroundColor = [UIColor clearColor]; + _submitButtonSpacing = 4.0f; + _submitButtonVerticalSpacing = 5.0f; + + _visualEffectView = [[UIVisualEffectView alloc] initWithEffect:nil]; + [self addSubview:_visualEffectView]; +} + +- (void)setUpForStyle:(TOPasscodeInputFieldStyle)style +{ + if (self.inputField) { + [self.inputField removeFromSuperview]; + self.variableInputView = nil; + self.fixedInputView = nil; + } + + if (style == TOPasscodeInputFieldStyleVariable) { + self.variableInputView = [[TOPasscodeVariableInputView alloc] init]; + [self.visualEffectView.contentView addSubview:self.variableInputView]; + } + else { + self.fixedInputView = [[TOPasscodeFixedInputView alloc] init]; + [self.visualEffectView.contentView addSubview:self.fixedInputView]; + } + + // Set the frame for the currently visible input view + [self.inputField sizeToFit]; + + // Size this view to match + [self sizeToFit]; +} + +#pragma mark - View Layout - +- (void)sizeToFit +{ + // Resize the view to encompass the current input view + CGRect frame = self.frame; + [self.inputField sizeToFit]; + frame.size = self.inputField.frame.size; + if (self.horizontalLayout) { + frame.size.height += self.submitButtonVerticalSpacing + CGRectGetHeight(self.submitButton.frame); + } + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + self.visualEffectView.frame = self.inputField.bounds; + + if (!self.submitButton) { return; } + + [self.submitButton sizeToFit]; + [self bringSubviewToFront:self.submitButton]; + + CGRect frame = self.submitButton.frame; + if (!self.horizontalLayout) { + frame.origin.x = CGRectGetMaxX(self.bounds) + self.submitButtonSpacing; + frame.origin.y = (CGRectGetHeight(self.bounds) - CGRectGetHeight(frame)) * 0.5f; + } + else { + frame.origin.x = (CGRectGetWidth(self.frame) - frame.size.width) * 0.5f; + frame.origin.y = CGRectGetMaxY(self.inputField.frame) + self.submitButtonVerticalSpacing; + } + self.submitButton.frame = CGRectIntegral(frame); +} + +#pragma mark - Interaction - +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; + if (!self.enabled) { return; } + self.contentAlpha = 0.5f; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; + if (!self.enabled) { return; } + [UIView animateWithDuration:0.3f animations:^{ + self.contentAlpha = 1.0f; + }]; + [self becomeFirstResponder]; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + CGRect frame = self.bounds; + frame.size.width += self.submitButton.frame.size.width + (self.submitButtonSpacing * 2.0f); + frame.size.height += self.submitButtonVerticalSpacing; + + if (CGRectContainsPoint(frame, point)) { + return YES; + } + return NO; +} + +- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + if ([[super hitTest:point withEvent:event] isEqual:self.submitButton]) { + if (CGRectContainsPoint(self.submitButton.frame, point)) { + return self.submitButton; + } else { + return self; + } + } + + return [super hitTest:point withEvent:event]; +} + +#pragma mark - Text Input Protocol - +- (BOOL)canBecomeFirstResponder { return self.enabled; } + +- (BOOL)hasText { return self.passcode.length > 0; } + +- (void)insertText:(NSString *)text +{ + if ([text isEqualToString:@"\n"]) { + if (self.passcodeCompletedHandler) { self.passcodeCompletedHandler(self.passcode); } + return; + } + + [self appendPasscodeCharacters:text animated:NO]; +} +- (void)deleteBackward +{ + [self deletePasscodeCharactersOfCount:1 animated:YES]; +} + +- (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; } + +- (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; } + +- (UIReturnKeyType)returnKeyType { return UIReturnKeyGo; } + +- (BOOL)enablesReturnKeyAutomatically { return YES; } + +#pragma mark - Text Input - +- (void)setPasscode:(NSString *)passcode animated:(BOOL)animated +{ + if (passcode == self.passcode) { return; } + _passcode = passcode; + + BOOL passcodeIsComplete = NO; + if (self.fixedInputView) { + [self.fixedInputView setHighlightedLength:_passcode.length animated:animated]; + passcodeIsComplete = _passcode.length >= self.maximumPasscodeLength; + } + else { + [self.variableInputView setLength:_passcode.length animated:animated]; + } + + if (self.submitButton) { + self.submitButton.hidden = (_passcode.length == 0); + [self bringSubviewToFront:self.submitButton]; + } + + if (passcodeIsComplete && self.passcodeCompletedHandler) { + self.passcodeCompletedHandler(_passcode); + } + + [self reloadInputViews]; +} + +- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated +{ + if (characters == nil) { return; } + if (self.maximumPasscodeLength > 0 && self.passcode.length >= self.maximumPasscodeLength) { return; } + + if (_passcode == nil) { _passcode = @""; } + [self setPasscode:[_passcode stringByAppendingString:characters] animated:animated]; +} + +- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated +{ + if (deleteCount <= 0 || self.passcode.length <= 0) { return; } + [self setPasscode:[self.passcode substringToIndex:(self.passcode.length - 1)] animated:animated]; +} + +- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact +{ + [self setPasscode:nil animated:animated]; + + // Play a negative impact effect + if (@available(iOS 9.0, *)) { + // https://stackoverflow.com/questions/41444274/how-to-check-if-haptic-engine-uifeedbackgenerator-is-supported + if (impact) { AudioServicesPlaySystemSoundWithCompletion(1521, nil); } + } + + if (!animated) { return; } + + CGPoint center = self.center; + CGPoint offset = center; + offset.x -= self.frame.size.width * 0.3f; + + // Play the view sliding out and then springing back in + id completionBlock = ^(BOOL finished) { + [UIView animateWithDuration:1.0f + delay:0.0f + usingSpringWithDamping:0.15f + initialSpringVelocity:10.0f + options:0 animations:^{ + self.center = center; + }completion:nil]; + }; + + [UIView animateWithDuration:0.05f animations:^{ + self.center = offset; + }completion:completionBlock]; + + if (!self.submitButton) { return; } + + [UIView animateWithDuration:0.7f animations:^{ + self.submitButton.alpha = 0.0f; + } completion:^(BOOL complete) { + self.submitButton.alpha = 1.0f; + self.submitButton.hidden = YES; + }]; +} + +#pragma mark - Button Callbacks - +- (void)submitButtonTapped:(id)sender +{ + if (self.passcodeCompletedHandler) { + self.passcodeCompletedHandler(self.passcode); + } +} + +#pragma mark - Private Accessors - +- (UIView *)inputField +{ + if (self.fixedInputView) { + return (UIView *)self.fixedInputView; + } + + return (UIView *)self.variableInputView; +} + +- (NSInteger)maximumPasscodeLength +{ + if (self.style == TOPasscodeInputFieldStyleFixed) { + return self.fixedInputView.length; + } + + return 0; +} + +#pragma mark - Public Accessors - + +- (void)setShowSubmitButton:(BOOL)showSubmitButton +{ + if (_showSubmitButton == showSubmitButton) { + return; + } + + _showSubmitButton = showSubmitButton; + + if (!_showSubmitButton) { + [self.submitButton removeFromSuperview]; + self.submitButton = nil; + return; + } + + self.submitButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.submitButton setTitle:@"OK" forState:UIControlStateNormal]; + [self.submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; + [self.submitButton.titleLabel setFont:[UIFont systemFontOfSize:18.0f]]; + self.submitButton.hidden = YES; + [self addSubview:self.submitButton]; + + [self setNeedsLayout]; +} + +- (void)setSubmitButtonSpacing:(CGFloat)submitButtonSpacing +{ + if (submitButtonSpacing == _submitButtonSpacing) { return; } + _submitButtonSpacing = submitButtonSpacing; + [self setNeedsLayout]; +} + +- (void)setSubmitButtonFontSize:(CGFloat)submitButtonFontSize +{ + if (submitButtonFontSize == _submitButtonFontSize) { return; } + _submitButtonFontSize = submitButtonFontSize; + self.submitButton.titleLabel.font = [UIFont systemFontOfSize:_submitButtonFontSize]; + [self.submitButton sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setStyle:(TOPasscodeInputFieldStyle)style +{ + if (style == _style) { return; } + _style = style; + [self setUpForStyle:_style]; +} + +- (void)setPasscode:(NSString *)passcode +{ + [self setPasscode:passcode animated:NO]; +} + +- (void)setContentAlpha:(CGFloat)contentAlpha +{ + _contentAlpha = contentAlpha; + self.inputField.alpha = contentAlpha; + self.submitButton.alpha = contentAlpha; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout +{ + [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; +} + +- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration +{ + if (_horizontalLayout == horizontalLayout) { + return; + } + + UIView *snapshotView = nil; + + if (self.submitButton && self.submitButton.hidden == NO && animated) { + snapshotView = [self.submitButton snapshotViewAfterScreenUpdates:NO]; + snapshotView.frame = self.submitButton.frame; + [self addSubview:snapshotView]; + } + + _horizontalLayout = horizontalLayout; + + if (!animated || !self.submitButton) { + [self sizeToFit]; + [self setNeedsLayout]; + return; + } + + self.submitButton.alpha = 0.0f; + [self setNeedsLayout]; + [self layoutIfNeeded]; + + id animationBlock = ^{ + self.submitButton.alpha = 1.0f; + snapshotView.alpha = 0.0f; + }; + + id completionBlock = ^(BOOL complete) { + [snapshotView removeFromSuperview]; + [self bringSubviewToFront:self.submitButton]; + }; + + [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; +} + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h new file mode 100644 index 0000000000..737f76761f --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h @@ -0,0 +1,55 @@ +// +// TOPasscodeVariableInputView.h +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import + +/** + A basic content view showing a rounded rectangle containing circles + that can be used to represent a variable size passcode. + */ +@interface TOPasscodeVariableInputView : UIImageView + +/* The thickness of the stroke around the view (Default is 1.5) */ +@property (nonatomic, assign) CGFloat outlineThickness; + +/* The corner radius of the stroke (Default is 5) */ +@property (nonatomic, assign) CGFloat outlineCornerRadius; + +/* The size of each circle bullet point representing a passcoded character (Default is 10) */ +@property (nonatomic, assign) CGFloat circleDiameter; + +/* The spacing between each circle (Default is 15) */ +@property (nonatomic, assign) CGFloat circleSpacing; + +/* The padding between the circles and the outer outline (Default is {10,10}) */ +@property (nonatomic, assign) CGSize outlinePadding; + +/* The maximum number of circles to show (This will indicate the view's width) (Default is 12) */ +@property (nonatomic, assign) NSInteger maximumVisibleLength; + +/* Set the number of characters entered into this view (May be larger than `maximumVisibleLength`) */ +@property (nonatomic, assign) NSInteger length; + +/* Set the number of characters represented by this field, animated if desired */ +- (void)setLength:(NSInteger)length animated:(BOOL)animated; + +@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m new file mode 100644 index 0000000000..1dc0783b91 --- /dev/null +++ b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m @@ -0,0 +1,254 @@ +// +// TOPasscodeVariableInputView.m +// +// Copyright 2017 Timothy Oliver. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#import "TOPasscodeVariableInputView.h" +#import "TOPasscodeCircleImage.h" + +@interface TOPasscodeVariableInputView () + +@property (nonatomic, strong) UIImage *backgroundImage; // The outline image for this view +@property (nonatomic, strong) UIImage *circleImage; // The circle image representing a single character + +@property (nonatomic, strong) NSMutableArray *circleViews; + +@end + +@implementation TOPasscodeVariableInputView + +#pragma mark - Class Creation - + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + _outlineThickness = 1.0f; + _outlineCornerRadius = 5.0f; + _circleDiameter = 11.0f; + _circleSpacing = 7.0f; + _outlinePadding = (CGSize){10,10}; + _maximumVisibleLength = 12; + } + + return self; +} + +#pragma mark - View Setup - +- (void)setUpImageForCircleViews +{ + if (self.circleImage != nil) { return; } + + self.circleImage = [TOPasscodeCircleImage circleImageOfSize:_circleDiameter inset:0.0f padding:1.0f antialias:YES]; + for (UIImageView *circleView in self.circleViews) { + circleView.image = self.circleImage; + [circleView sizeToFit]; + } +} + +- (void)setUpCircleViewsForLength:(NSInteger)length +{ + // Set up the number of circle views if needed + if (self.circleViews.count == length) { return; } + + if (self.circleViews == nil) { + self.circleViews = [NSMutableArray arrayWithCapacity:_maximumVisibleLength]; + } + + // Reduce the number of views + while (self.circleViews.count > length) { + UIImageView *circleView = self.circleViews.lastObject; + [circleView removeFromSuperview]; + [self.circleViews removeLastObject]; + } + + // Increase the number of views + [UIView performWithoutAnimation:^{ + while (self.circleViews.count < length) { + UIImageView *circleView = [[UIImageView alloc] initWithImage:self.circleImage]; + circleView.alpha = 0.0f; + [self addSubview:circleView]; + [self.circleViews addObject:circleView]; + } + }]; +} + +- (void)setUpBackgroundImage +{ + if (self.backgroundImage != nil) { return; } + + self.backgroundImage = [[self class] backgroundImageWithThickness:_outlineThickness cornerRadius:_outlineCornerRadius]; + self.image = self.backgroundImage; +} + +#pragma mark - View Layout - + +- (void)sizeToFit +{ + CGRect frame = self.frame; + + // Calculate the width + frame.size.width = self.outlineThickness * 2.0f; + frame.size.width += (self.outlinePadding.width * 2.0f); + frame.size.width += (self.maximumVisibleLength * (self.circleDiameter+2.0f)); // +2 for padding + frame.size.width += ((self.maximumVisibleLength - 1) * self.circleSpacing); + + // Height + frame.size.height = self.outlineThickness * 2.0f; + frame.size.height += self.outlinePadding.height * 2.0f; + frame.size.height += self.circleDiameter; + + self.frame = CGRectIntegral(frame); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + // Genearate the background image if we don't have one yet + [self setUpBackgroundImage]; + + // Set up the circle view image + [self setUpImageForCircleViews]; + + // Set up the circle views + [self setUpCircleViewsForLength:self.maximumVisibleLength]; + + // Layout the circle views for the current length + CGRect frame = CGRectZero; + frame.size = self.circleImage.size; + frame.origin.y = CGRectGetMidY(self.bounds) - (frame.size.height * 0.5f); + frame.origin.x = self.outlinePadding.width + self.outlineThickness; + + for (UIImageView *circleView in self.circleViews) { + circleView.frame = frame; + frame.origin.x += frame.size.width + self.circleSpacing; + } +} + +#pragma mark - Accessors - + +- (void)setOutlineThickness:(CGFloat)outlineThickness +{ + if (_outlineThickness == outlineThickness) { return; } + _outlineThickness = outlineThickness; + self.backgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setOutlineCornerRadius:(CGFloat)outlineCornerRadius +{ + if (_outlineCornerRadius == outlineCornerRadius) { return; } + _outlineCornerRadius = outlineCornerRadius; + self.backgroundImage = nil; + [self setNeedsLayout]; +} + +- (void)setCircleDiameter:(CGFloat)circleDiameter +{ + if (_circleDiameter == circleDiameter) { return; } + _circleDiameter = circleDiameter; + self.circleImage = nil; + [self setUpImageForCircleViews]; +} + +- (void)setCircleSpacing:(CGFloat)circleSpacing +{ + if (_circleSpacing == circleSpacing) { return; } + _circleSpacing = circleSpacing; + [self sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setOutlinePadding:(CGSize)outlinePadding +{ + if (CGSizeEqualToSize(outlinePadding, _outlinePadding)) { return; } + _outlinePadding = outlinePadding; + [self sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setMaximumVisibleLength:(NSInteger)maximumVisibleLength +{ + if (_maximumVisibleLength == maximumVisibleLength) { return; } + _maximumVisibleLength = maximumVisibleLength; + [self setUpCircleViewsForLength:maximumVisibleLength]; + [self sizeToFit]; + [self setNeedsLayout]; +} + +- (void)setLength:(NSInteger)length +{ + [self setLength:length animated:NO]; +} + +- (void)setLength:(NSInteger)length animated:(BOOL)animated +{ + if (length == _length) { return; } + + _length = length; + + void (^animationBlock)(void) = ^{ + NSInteger i = 0; + for (UIImageView *circleView in self.circleViews) { + circleView.alpha = i < length ? 1.0f : 0.0f; + i++; + } + }; + + if (!animated) { + animationBlock(); + return; + } + + [UIView animateWithDuration:0.4f animations:animationBlock]; +} + +#pragma mark - Image Creation - + ++ (UIImage *)backgroundImageWithThickness:(CGFloat)thickness cornerRadius:(CGFloat)radius +{ + CGFloat inset = thickness / 2.0f; + CGFloat dimension = (radius * 2.0f) + 2.0f; + + CGRect frame = CGRectZero; + frame.origin = CGPointMake(inset, inset); + frame.size = CGSizeMake(dimension, dimension); + + CGSize canvasSize = frame.size; + canvasSize.width += thickness; + canvasSize.height += thickness; + + UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); + { + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius]; + path.lineWidth = thickness; + [[UIColor blackColor] setStroke]; + [path stroke]; + } + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + UIEdgeInsets insets = UIEdgeInsetsMake(radius+1, radius+1, radius+1, radius+1); + image = [image resizableImageWithCapInsets:insets]; + return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; +} + +@end diff --git a/iOSClient/Utility/ThreadSafeArray.swift b/iOSClient/Utility/ThreadSafeArray.swift index acd6f73696..17a0b0e302 100644 --- a/iOSClient/Utility/ThreadSafeArray.swift +++ b/iOSClient/Utility/ThreadSafeArray.swift @@ -23,6 +23,7 @@ // import Foundation +import UIKit /// A thread-safe array. public class ThreadSafeArray { diff --git a/iOSClient/Utility/ThreadSafeDictionary.swift b/iOSClient/Utility/ThreadSafeDictionary.swift index b7cba7ca2e..9e9e224aaa 100644 --- a/iOSClient/Utility/ThreadSafeDictionary.swift +++ b/iOSClient/Utility/ThreadSafeDictionary.swift @@ -5,6 +5,7 @@ // import Foundation +import UIKit class ThreadSafeDictionary: Collection { diff --git a/iOSClient/Viewer/NCViewer.swift b/iOSClient/Viewer/NCViewer.swift index 156529b12f..6fb405966a 100644 --- a/iOSClient/Viewer/NCViewer.swift +++ b/iOSClient/Viewer/NCViewer.swift @@ -26,16 +26,13 @@ import NextcloudKit import QuickLook class NCViewer: NSObject { - let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() + let database = NCManageDatabase.shared private var viewerQuickLook: NCViewerQuickLook? - private var metadata = tableMetadata() - private var metadatas: [tableMetadata] = [] - func view(viewController: UIViewController, metadata: tableMetadata, metadatas: [tableMetadata], imageIcon: UIImage?) { - self.metadata = metadata - self.metadatas = metadatas + func view(viewController: UIViewController, metadata: tableMetadata, ocIds: [String]? = nil, image: UIImage? = nil) { + let session = NCSession.shared.getSession(account: metadata.account) // URL if metadata.classFile == NKCommon.TypeClassFile.url.rawValue { @@ -45,7 +42,7 @@ class NCViewer: NSObject { if pathComponents.contains("call") { let talkComponents = pathComponents.last?.components(separatedBy: "#") if let roomToken = talkComponents?.first { - let urlString = "nextcloudtalk://open-conversation?server=\(appDelegate.urlBase)&user=\(appDelegate.userId)&withRoomToken=\(roomToken)" + let urlString = "nextcloudtalk://open-conversation?server=\(session.urlBase)&user=\(session.userId)&withRoomToken=\(roomToken)" if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url) return @@ -63,16 +60,17 @@ class NCViewer: NSObject { if metadata.isImage || metadata.isAudioOrVideo { if let navigationController = viewController.navigationController, let viewerMediaPageContainer: NCViewerMediaPage = UIStoryboard(name: "NCViewerMediaPage", bundle: nil).instantiateInitialViewController() as? NCViewerMediaPage { - var index = 0 - for medatasImage in metadatas { - if medatasImage.ocId == metadata.ocId { - viewerMediaPageContainer.currentIndex = index - break - } - index += 1 - } - viewerMediaPageContainer.metadatas = metadatas + viewerMediaPageContainer.delegateViewController = viewController + + if let ocIds { + viewerMediaPageContainer.currentIndex = ocIds.firstIndex(where: { $0 == metadata.ocId }) ?? 0 + viewerMediaPageContainer.ocIds = ocIds + } else { + viewerMediaPageContainer.currentIndex = 0 + viewerMediaPageContainer.ocIds = [metadata.ocId] + } + navigationController.pushViewController(viewerMediaPageContainer, animated: true) } return @@ -81,14 +79,14 @@ class NCViewer: NSObject { // DOCUMENTS if metadata.classFile == NKCommon.TypeClassFile.document.rawValue { // Set Last Opening Date - NCManageDatabase.shared.setLastOpeningDate(metadata: metadata) + self.database.setLastOpeningDate(metadata: metadata) // PDF if metadata.isPDF { if let navigationController = viewController.navigationController, let viewController: NCViewerPDF = UIStoryboard(name: "NCViewerPDF", bundle: nil).instantiateInitialViewController() as? NCViewerPDF { viewController.metadata = metadata viewController.titleView = metadata.fileNameView - viewController.imageIcon = imageIcon + viewController.imageIcon = image navigationController.pushViewController(viewController, animated: true) } return @@ -97,14 +95,14 @@ class NCViewer: NSObject { if metadata.isAvailableRichDocumentEditorView { if metadata.url.isEmpty { NCActivityIndicator.shared.start(backgroundView: viewController.view) - NextcloudKit.shared.createUrlRichdocuments(fileID: metadata.fileId, account: metadata.account) { account, url, _, error in + NextcloudKit.shared.createUrlRichdocuments(fileID: metadata.fileId, account: metadata.account) { _, url, _, error in NCActivityIndicator.shared.stop() - if error == .success && account == self.appDelegate.account && url != nil { + if error == .success, url != nil { if let navigationController = viewController.navigationController, let viewController: NCViewerRichDocument = UIStoryboard(name: "NCViewerRichdocument", bundle: nil).instantiateInitialViewController() as? NCViewerRichDocument { viewController.metadata = metadata viewController.link = url! - viewController.imageIcon = imageIcon + viewController.imageIcon = image navigationController.pushViewController(viewController, animated: true) } } else if error != .success { @@ -116,7 +114,7 @@ class NCViewer: NSObject { let viewController: NCViewerRichDocument = UIStoryboard(name: "NCViewerRichdocument", bundle: nil).instantiateInitialViewController() as? NCViewerRichDocument { viewController.metadata = metadata viewController.link = metadata.url - viewController.imageIcon = imageIcon + viewController.imageIcon = image navigationController.pushViewController(viewController, animated: true) } } @@ -135,17 +133,17 @@ class NCViewer: NSObject { options = NKRequestOptions(customUserAgent: utility.getCustomUserAgentOnlyOffice()) } if metadata.url.isEmpty { - let fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId) + let fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) NCActivityIndicator.shared.start(backgroundView: viewController.view) - NextcloudKit.shared.NCTextOpenFile(fileNamePath: fileNamePath, editor: editor, account: metadata.account, options: options) { account, url, _, error in + NextcloudKit.shared.NCTextOpenFile(fileNamePath: fileNamePath, editor: editor, account: metadata.account, options: options) { _, url, _, error in NCActivityIndicator.shared.stop() - if error == .success && account == self.appDelegate.account && url != nil { + if error == .success, url != nil { if let navigationController = viewController.navigationController, let viewController: NCViewerNextcloudText = UIStoryboard(name: "NCViewerNextcloudText", bundle: nil).instantiateInitialViewController() as? NCViewerNextcloudText { viewController.metadata = metadata viewController.editor = editor viewController.link = url! - viewController.imageIcon = imageIcon + viewController.imageIcon = image navigationController.pushViewController(viewController, animated: true) } } else if error != .success { @@ -158,7 +156,7 @@ class NCViewer: NSObject { viewController.metadata = metadata viewController.editor = editor viewController.link = metadata.url - viewController.imageIcon = imageIcon + viewController.imageIcon = image navigationController.pushViewController(viewController, animated: true) } } @@ -181,28 +179,3 @@ class NCViewer: NSObject { } } } - -// MARK: - SELECT - -extension NCViewer: NCSelectDelegate { - func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool) { - if let serverUrl = serverUrl, - let metadata = items[0] as? tableMetadata { - if move { - Task { - let error = await NCNetworking.shared.moveMetadata(metadata, serverUrlTo: serverUrl, overwrite: overwrite) - if error != .success { - NCContentPresenter().showError(error: error) - } - } - } else if copy { - Task { - let error = await NCNetworking.shared.copyMetadata(metadata, serverUrlTo: serverUrl, overwrite: overwrite) - if error != .success { - NCContentPresenter().showError(error: error) - } - } - } - } - } -} diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift index ad35ffc659..c3aac0db24 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayer.swift @@ -27,14 +27,13 @@ import UIKit import MobileVLCKit class NCPlayer: NSObject { - - internal let appDelegate = (UIApplication.shared.delegate as? AppDelegate)! internal var url: URL? internal var player = VLCMediaPlayer() internal var dialogProvider: VLCDialogProvider? internal var metadata: tableMetadata internal var singleTapGestureRecognizer: UITapGestureRecognizer? internal var activityIndicator: UIActivityIndicatorView + internal let database = NCManageDatabase.shared internal var width: Int? internal var height: Int? internal var length: Int? @@ -50,7 +49,6 @@ class NCPlayer: NSObject { // MARK: - View Life Cycle init(imageVideoContainer: UIImageView, playerToolBar: NCPlayerToolBar?, metadata: tableMetadata, viewerMediaPage: NCViewerMediaPage?) { - self.imageVideoContainer = imageVideoContainer self.playerToolBar = playerToolBar self.metadata = metadata @@ -73,14 +71,12 @@ class NCPlayer: NSObject { } deinit { - player.stop() print("deinit NCPlayer with ocId \(metadata.ocId)") NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) } func openAVPlayer(url: URL, autoplay: Bool = false) { - var position: Float = 0 let userAgent = userAgent @@ -97,7 +93,7 @@ class NCPlayer: NSObject { // player?.media?.addOption("--network-caching=500") player.media?.addOption(":http-user-agent=\(userAgent)") - if let result = NCManageDatabase.shared.getVideo(metadata: metadata), + if let result = self.database.getVideo(metadata: metadata), let resultPosition = result.position { position = resultPosition } @@ -125,7 +121,6 @@ class NCPlayer: NSObject { } func restartAVPlayer(position: Float, pauseAfterPlay: Bool) { - if let url = self.url, !player.isPlaying { player.media = VLCMedia(url: url) @@ -137,8 +132,7 @@ class NCPlayer: NSObject { if metadata.isVideo { if position == 0 { - let fileNamePreviewLocalPath = NCUtilityFileSystem().getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag) - imageVideoContainer?.image = UIImage(contentsOfFile: fileNamePreviewLocalPath) + imageVideoContainer?.image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024) } else { imageVideoContainer?.image = nil } @@ -153,7 +147,6 @@ class NCPlayer: NSObject { } func changeScreenMode() { - guard let viewerMediaPage = viewerMediaPage else { return } if viewerMediaScreenMode == .full { @@ -166,7 +159,6 @@ class NCPlayer: NSObject { // MARK: - NotificationCenter @objc func applicationDidEnterBackground(_ notification: NSNotification) { - if metadata.isVideo { playerPause() } @@ -175,15 +167,13 @@ class NCPlayer: NSObject { // MARK: - func isPlay() -> Bool { - return player.isPlaying } func playerPlay() { - playerToolBar?.playbackSliderEvent = .began - if let result = NCManageDatabase.shared.getVideo(metadata: metadata), let position = result.position { + if let result = self.database.getVideo(metadata: metadata), let position = result.position { player.position = position playerToolBar?.playbackSliderEvent = .moved } @@ -196,44 +186,37 @@ class NCPlayer: NSObject { } @objc func playerStop() { - savePosition() player.stop() } @objc func playerPause() { - savePosition() player.pause() } func playerPosition(_ position: Float) { - - NCManageDatabase.shared.addVideo(metadata: metadata, position: position) + self.database.addVideo(metadata: metadata, position: position) player.position = position } func savePosition() { - guard metadata.isVideo, isPlay() else { return } - NCManageDatabase.shared.addVideo(metadata: metadata, position: player.position) + self.database.addVideo(metadata: metadata, position: player.position) } func jumpForward(_ seconds: Int32) { - player.play() player.jumpForward(seconds) } func jumpBackward(_ seconds: Int32) { - player.play() player.jumpBackward(seconds) } } extension NCPlayer: VLCMediaPlayerDelegate { - func mediaPlayerStateChanged(_ aNotification: Notification) { if player.state == .buffering && player.isPlaying { @@ -251,7 +234,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { case .buffering: print("Played mode: BUFFERING") case .ended: - NCManageDatabase.shared.addVideo(metadata: self.metadata, position: 0) + self.database.addVideo(metadata: self.metadata, position: 0) DispatchQueue.main.asyncAfter(deadline: .now() + 1) { if let playRepeat = self.playerToolBar?.playRepeat { self.restartAVPlayer(position: 0, pauseAfterPlay: !playRepeat) @@ -274,7 +257,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { } else { playerToolBar.playButtonPause() // Set track audio/subtitle - let data = NCManageDatabase.shared.getVideo(metadata: metadata) + let data = self.database.getVideo(metadata: metadata) if let currentAudioTrackIndex = data?.currentAudioTrackIndex { player.currentAudioTrackIndex = Int32(currentAudioTrackIndex) } @@ -289,7 +272,7 @@ extension NCPlayer: VLCMediaPlayerDelegate { self.width = Int(size.width) self.height = Int(size.height) playerToolBar.updateTopToolBar(videoSubTitlesIndexes: player.videoSubTitlesIndexes, audioTrackIndexes: player.audioTrackIndexes) - NCManageDatabase.shared.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) + self.database.addVideo(metadata: metadata, width: self.width, height: self.height, length: self.length) print("Played mode: PLAYING") case .paused: playerToolBar?.playButtonPlay() @@ -305,16 +288,12 @@ extension NCPlayer: VLCMediaPlayerDelegate { } extension NCPlayer: VLCMediaThumbnailerDelegate { - func mediaThumbnailerDidTimeOut(_ mediaThumbnailer: VLCMediaThumbnailer) { } - func mediaThumbnailer(_ mediaThumbnailer: VLCMediaThumbnailer, didFinishThumbnail thumbnail: CGImage) { } } extension NCPlayer: VLCCustomDialogRendererProtocol { - func showError(withTitle error: String, message: String) { - let alert = UIAlertController(title: error, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in @@ -330,7 +309,6 @@ extension NCPlayer: VLCCustomDialogRendererProtocol { } func showQuestion(withTitle title: String, message: String, type questionType: VLCDialogQuestionType, cancel cancelString: String?, action1String: String?, action2String: String?, withReference reference: NSValue) { - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) if let action1String = action1String { diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index d9e2e9f911..4aa84be3d5 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -29,7 +29,6 @@ import AVKit import MediaPlayer import MobileVLCKit import FloatingPanel -import JGProgressHUD import Alamofire class NCPlayerToolBar: UIView { @@ -59,13 +58,14 @@ class NCPlayerToolBar: UIView { var isFullscreen: Bool = false var playRepeat: Bool = false - private let hud = JGProgressHUD() + private let hud = NCHud() private var ncplayer: NCPlayer? private var metadata: tableMetadata? private let audioSession = AVAudioSession.sharedInstance() private var pointSize: CGFloat = 0 private let utilityFileSystem = NCUtilityFileSystem() private let utility = NCUtility() + private let database = NCManageDatabase.shared private weak var viewerMediaPage: NCViewerMediaPage? private var buttonImage = UIImage() @@ -127,14 +127,12 @@ class NCPlayerToolBar: UIView { } deinit { - print("deinit NCPlayerToolBar") } // MARK: - func setBarPlayer(position: Float, ncplayer: NCPlayer? = nil, metadata: tableMetadata? = nil, viewerMediaPage: NCViewerMediaPage? = nil) { - if let ncplayer = ncplayer { self.ncplayer = ncplayer } @@ -165,9 +163,7 @@ class NCPlayerToolBar: UIView { } public func update() { - guard let ncplayer = self.ncplayer, let length = ncplayer.player.media?.length.intValue else { return } - let position = ncplayer.player.position let positionInSecond = position * Float(length / 1000) @@ -183,7 +179,6 @@ class NCPlayerToolBar: UIView { } public func updateTopToolBar(videoSubTitlesIndexes: [Any], audioTrackIndexes: [Any]) { - if let metadata = metadata, metadata.isVideo { self.subtitleButton.isEnabled = true self.audioButton.isEnabled = true @@ -193,7 +188,6 @@ class NCPlayerToolBar: UIView { // MARK: - public func show() { - UIView.animate(withDuration: 0.5, animations: { self.alpha = 1 }, completion: { (_: Bool) in @@ -202,7 +196,6 @@ class NCPlayerToolBar: UIView { } func hide() { - UIView.animate(withDuration: 0.5, animations: { self.alpha = 0 }, completion: { (_: Bool) in @@ -225,7 +218,6 @@ class NCPlayerToolBar: UIView { // MARK: - Event / Gesture @objc func playbackValChanged(slider: UISlider, event: UIEvent) { - guard let ncplayer = ncplayer else { return } let newPosition = playbackSlider.value @@ -257,7 +249,6 @@ class NCPlayerToolBar: UIView { @objc func tap(gestureRecognizer: UITapGestureRecognizer) { } @IBAction func tapFullscreen(_ sender: Any) { - isFullscreen = !isFullscreen if isFullscreen { fullscreenButton.setImage(utility.loadImage(named: "arrow.down.right.and.arrow.up.left", colors: [.white]), for: .normal) @@ -268,9 +259,7 @@ class NCPlayerToolBar: UIView { } @IBAction func tapSubTitle(_ sender: Any) { - guard let player = ncplayer?.player else { return } - let spuTracks = player.videoSubTitlesNames let spuTrackIndexes = player.videoSubTitlesIndexes @@ -278,9 +267,7 @@ class NCPlayerToolBar: UIView { } @IBAction func tapAudio(_ sender: Any) { - guard let player = ncplayer?.player else { return } - let audioTracks = player.audioTrackNames let audioTrackIndexes = player.audioTrackIndexes @@ -300,25 +287,20 @@ class NCPlayerToolBar: UIView { } @IBAction func tapForward(_ sender: Any) { - guard let ncplayer = ncplayer else { return } ncplayer.jumpForward(10) - self.viewerMediaPage?.startTimerAutoHide() } @IBAction func tapBack(_ sender: Any) { - guard let ncplayer = ncplayer else { return } ncplayer.jumpBackward(10) - self.viewerMediaPage?.startTimerAutoHide() } @IBAction func tapRepeat(_ sender: Any) { - if playRepeat { playRepeat = false repeatButton.setImage(utility.loadImage(named: "repeat", colors: [NCBrandColor.shared.iconImageColor2]), for: .normal) @@ -330,13 +312,11 @@ class NCPlayerToolBar: UIView { } extension NCPlayerToolBar { - func toggleMenuSubTitle(spuTracks: [Any], spuTrackIndexes: [Any]) { - var actions = [NCMenuAction]() var subTitleIndex: Int? - if let data = NCManageDatabase.shared.getVideo(metadata: metadata), let idx = data.currentVideoSubTitleIndex { + if let data = self.database.getVideo(metadata: metadata), let idx = data.currentVideoSubTitleIndex { subTitleIndex = idx } else if let idx = ncplayer?.player.currentVideoSubTitleIndex { subTitleIndex = Int(idx) @@ -357,7 +337,7 @@ extension NCPlayerToolBar { on: (subTitleIndex ?? -9999) == idx, action: { _ in self.ncplayer?.player.currentVideoSubTitleIndex = idx - NCManageDatabase.shared.addVideo(metadata: metadata, currentVideoSubTitleIndex: Int(idx)) + self.database.addVideo(metadata: metadata, currentVideoSubTitleIndex: Int(idx)) } ) ) @@ -387,6 +367,7 @@ extension NCPlayerToolBar { viewController.enableSelectFile = true viewController.type = "subtitle" viewController.serverUrl = metadata.serverUrl + viewController.session = NCSession.shared.getSession(account: metadata.account) self.viewerMediaPage?.present(navigationController, animated: true, completion: nil) } @@ -398,11 +379,10 @@ extension NCPlayerToolBar { } func toggleMenuAudio(audioTracks: [Any], audioTrackIndexes: [Any]) { - var actions = [NCMenuAction]() var audioIndex: Int? - if let data = NCManageDatabase.shared.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex { + if let data = self.database.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex { audioIndex = idx } else if let idx = ncplayer?.player.currentAudioTrackIndex { audioIndex = Int(idx) @@ -410,9 +390,7 @@ extension NCPlayerToolBar { if !audioTracks.isEmpty { for index in 0...audioTracks.count - 1 { - guard let title = audioTracks[index] as? String, let idx = audioTrackIndexes[index] as? Int32, let metadata = self.metadata else { return } - actions.append( NCMenuAction( title: title, @@ -423,7 +401,7 @@ extension NCPlayerToolBar { on: (audioIndex ?? -9999) == idx, action: { _ in self.ncplayer?.player.currentAudioTrackIndex = idx - NCManageDatabase.shared.addVideo(metadata: metadata, currentAudioTrackIndex: Int(idx)) + self.database.addVideo(metadata: metadata, currentAudioTrackIndex: Int(idx)) } ) ) @@ -441,7 +419,6 @@ extension NCPlayerToolBar { selected: false, on: false, action: { _ in - guard let metadata = self.metadata else { return } let storyboard = UIStoryboard(name: "NCSelect", bundle: nil) if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController, @@ -453,6 +430,7 @@ extension NCPlayerToolBar { viewController.enableSelectFile = true viewController.type = "audio" viewController.serverUrl = metadata.serverUrl + viewController.session = NCSession.shared.getSession(account: metadata.account) self.viewerMediaPage?.present(navigationController, animated: true, completion: nil) } @@ -465,11 +443,8 @@ extension NCPlayerToolBar { } extension NCPlayerToolBar: NCSelectDelegate { - - func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool) { - + func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) { if let metadata = metadata, let viewerMediaPage = viewerMediaPage { - let serverUrlFileName = metadata.serverUrl + "/" + metadata.fileName let fileNameLocalPath = NCUtilityFileSystem().getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) @@ -477,32 +452,26 @@ extension NCPlayerToolBar: NCSelectDelegate { addPlaybackSlave(type: type, metadata: metadata) } else { var downloadRequest: DownloadRequest? - hud.indicatorView = JGProgressHUDRingIndicatorView() - hud.textLabel.text = NSLocalizedString("_downloading_", comment: "") - hud.detailTextLabel.text = NSLocalizedString("_tap_to_cancel_", comment: "") - hud.detailTextLabel.textColor = NCBrandColor.shared.iconImageColor2 - if let indicatorView = hud.indicatorView as? JGProgressHUDRingIndicatorView { - indicatorView.ringWidth = 1.5 - indicatorView.ringColor = NCBrandColor.shared.brandElement - } - hud.tapOnHUDViewBlock = { _ in + hud.initHudRing(view: viewerMediaPage.view, + text: NSLocalizedString("_downloading_", comment: ""), + tapToCancelDetailText: true) { if let request = downloadRequest { request.cancel() } } - hud.show(in: viewerMediaPage.view) - NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: metadata.account ,requestHandler: { request in + NextcloudKit.shared.download(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: metadata.account, requestHandler: { request in downloadRequest = request }, taskHandler: { _ in }, progressHandler: { progress in - self.hud.progress = Float(progress.fractionCompleted) + self.hud.progress(progress.fractionCompleted) }) { _, _, _, _, _, _, error in self.hud.dismiss() if error == .success { + self.hud.success() self.addPlaybackSlave(type: type, metadata: metadata) } else if error.errorCode != 200 { - NCContentPresenter().showError(error: error) + self.hud.error(text: error.errorDescription) } } } @@ -512,7 +481,6 @@ extension NCPlayerToolBar: NCSelectDelegate { // swiftlint:disable inclusive_language func addPlaybackSlave(type: String, metadata: tableMetadata) { // swiftlint:enable inclusive_language - let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, fileNameView: metadata.fileNameView) if type == "subtitle" { @@ -526,7 +494,6 @@ extension NCPlayerToolBar: NCSelectDelegate { // https://stackoverflow.com/questions/13196263/custom-uislider-increase-hot-spot-size // class NCPlayerToolBarSlider: UISlider { - private var thumbTouchSize = CGSize(width: 100, height: 100) override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { @@ -544,15 +511,15 @@ class NCPlayerToolBarSlider: UISlider { } public func addTapGesture() { - let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) + addGestureRecognizer(tap) } @objc private func handleTap(_ sender: UITapGestureRecognizer) { - let location = sender.location(in: self) let percent = minimumValue + Float(location.x / bounds.width) * (maximumValue - minimumValue) + setValue(percent, animated: true) sendActions(for: .valueChanged) } diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib index c4621b414e..172f9daf67 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib @@ -1,9 +1,9 @@ - + - + @@ -54,18 +54,18 @@ - +