From 4cf015e6994fc75bc0b2c310e090fcd02f529d93 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Wed, 6 Sep 2023 19:24:13 +0200 Subject: [PATCH] - add BookmarkComposer - add BookmarkSetupViewController UI for BookmarkComposer - other improvements --- ios-sdk | 2 +- ownCloud.xcodeproj/project.pbxproj | 64 +++ .../xcshareddata/xcschemes/ownCloud.xcscheme | 2 +- .../AppRootViewController.swift | 3 +- .../Bookmarks/BookmarkViewController.swift | 61 ++- .../Bookmarks/Composer/BookmarkComposer.swift | 487 ++++++++++++++++++ .../BookmarkComposerConfiguration.swift | 46 ++ .../BookmarkSetupStepViewController.swift | 228 ++++++++ .../Setup/BookmarkSetupViewController.swift | 288 +++++++++++ ...kSetupStepAuthenticateViewController.swift | 91 ++++ ...kmarkSetupStepEnterURLViewController.swift | 52 ++ ...SetupStepEnterUsernameViewController.swift | 52 ++ ...kmarkSetupStepFinishedViewController.swift | 58 +++ ...rkSetupStepPrepopulateViewController.swift | 22 + .../Setup/Steps/CertificateSummaryView.swift | 125 +++++ .../Licensing/Offers/LicenseOfferView.swift | 2 +- ownCloudAppShared/Branding/Branding+App.swift | 1 + .../ClientSidebarViewController.swift | 4 +- .../UIKit Extension/UIView+Extension.swift | 16 + .../SegmentView/UIView+EmbedAndLayout.swift | 47 +- .../Theme/ThemeCollection.swift | 4 + 21 files changed, 1607 insertions(+), 48 deletions(-) create mode 100644 ownCloud/Bookmarks/Composer/BookmarkComposer.swift create mode 100644 ownCloud/Bookmarks/Composer/BookmarkComposerConfiguration.swift create mode 100644 ownCloud/Bookmarks/Setup/BookmarkSetupStepViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/BookmarkSetupViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepAuthenticateViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterURLViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterUsernameViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepFinishedViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepPrepopulateViewController.swift create mode 100644 ownCloud/Bookmarks/Setup/Steps/CertificateSummaryView.swift diff --git a/ios-sdk b/ios-sdk index e8fa9525a..7ca86d879 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit e8fa9525aa25e44098cdd7cf238858cd295a1292 +Subproject commit 7ca86d879b42f9336ff1ba00d8ead4006eb63388 diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 38c7ad13a..cdb3d0230 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -223,6 +223,11 @@ DC20DE6B21C01B210096000B /* ownCloudUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2393697C2076110900BCE21A /* ownCloudUI.framework */; }; DC2218C62822C5B900808BCE /* OCVFSNode+FileProviderItem.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2218C52822C5B900808BCE /* OCVFSNode+FileProviderItem.m */; }; DC2218CC2823329100808BCE /* FileProviderContentEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = DC2218CB2823329100808BCE /* FileProviderContentEnumerator.m */; }; + DC2323DC2AA7B5D600BFF393 /* BookmarkComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2323DB2AA7B5D600BFF393 /* BookmarkComposer.swift */; }; + DC2323DE2AA7B6BB00BFF393 /* BookmarkComposerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2323DD2AA7B6BB00BFF393 /* BookmarkComposerConfiguration.swift */; }; + DC2323E02AA7C59400BFF393 /* BookmarkSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2323DF2AA7C59400BFF393 /* BookmarkSetupViewController.swift */; }; + DC2323E32AA85D0300BFF393 /* BookmarkSetupStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2323E22AA85D0300BFF393 /* BookmarkSetupStepViewController.swift */; }; + DC2323E62AA865A700BFF393 /* BookmarkSetupStepEnterURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2323E52AA865A700BFF393 /* BookmarkSetupStepEnterURLViewController.swift */; }; DC23D1D9238F390A00423F62 /* OCLicenseAppStoreReceipt.m in Sources */ = {isa = PBXBuildFile; fileRef = DC23D1D7238F390200423F62 /* OCLicenseAppStoreReceipt.m */; }; DC23D1DA238F391200423F62 /* OCLicenseAppStoreReceipt.h in Headers */ = {isa = PBXBuildFile; fileRef = DC23D1D6238F390200423F62 /* OCLicenseAppStoreReceipt.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC24B28725BA2A2E005783E2 /* Branding.h in Headers */ = {isa = PBXBuildFile; fileRef = DC24B27125B9DF31005783E2 /* Branding.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -305,6 +310,11 @@ DC4C575D233958B70098BAE9 /* FixedHeightImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C575C233958B70098BAE9 /* FixedHeightImageView.swift */; }; DC51FD922475715F0069AB79 /* CellularSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC51FD912475715F0069AB79 /* CellularSettingsViewController.swift */; }; DC576EC022647A070087316D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DC576EC222647A070087316D /* Localizable.strings */; }; + DC5908752AA87A1700BFF393 /* BookmarkSetupStepEnterUsernameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5908742AA87A1700BFF393 /* BookmarkSetupStepEnterUsernameViewController.swift */; }; + DC5908772AA87ABF00BFF393 /* BookmarkSetupStepAuthenticateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5908762AA87ABF00BFF393 /* BookmarkSetupStepAuthenticateViewController.swift */; }; + DC59087A2AA87F6B00BFF393 /* BookmarkSetupStepFinishedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5908792AA87F6B00BFF393 /* BookmarkSetupStepFinishedViewController.swift */; }; + DC59087D2AA8B82200BFF393 /* BookmarkSetupStepPrepopulateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59087C2AA8B82200BFF393 /* BookmarkSetupStepPrepopulateViewController.swift */; }; + DC59087F2AA8D25400BFF393 /* CertificateSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC59087E2AA8D25400BFF393 /* CertificateSummaryView.swift */; }; DC5C48A32918FB7400EBC053 /* CollectionSidebarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5C48A22918FB7400EBC053 /* CollectionSidebarViewController.swift */; }; DC5D58FF2A7166A300BFF393 /* ThemeCSS+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5D58FE2A7166A300BFF393 /* ThemeCSS+SystemColors.swift */; }; DC60F2A629802ABE00905EC8 /* UINavigationItem+NavigationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC60F2A529802ABE00905EC8 /* UINavigationItem+NavigationContent.swift */; }; @@ -1219,6 +1229,11 @@ DC2218C52822C5B900808BCE /* OCVFSNode+FileProviderItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCVFSNode+FileProviderItem.m"; sourceTree = ""; }; DC2218CA2823329100808BCE /* FileProviderContentEnumerator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FileProviderContentEnumerator.h; sourceTree = ""; }; DC2218CB2823329100808BCE /* FileProviderContentEnumerator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileProviderContentEnumerator.m; sourceTree = ""; }; + DC2323DB2AA7B5D600BFF393 /* BookmarkComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkComposer.swift; sourceTree = ""; }; + DC2323DD2AA7B6BB00BFF393 /* BookmarkComposerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkComposerConfiguration.swift; sourceTree = ""; }; + DC2323DF2AA7C59400BFF393 /* BookmarkSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupViewController.swift; sourceTree = ""; }; + DC2323E22AA85D0300BFF393 /* BookmarkSetupStepViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupStepViewController.swift; sourceTree = ""; }; + DC2323E52AA865A700BFF393 /* BookmarkSetupStepEnterURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupStepEnterURLViewController.swift; sourceTree = ""; }; DC23D1D6238F390200423F62 /* OCLicenseAppStoreReceipt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCLicenseAppStoreReceipt.h; sourceTree = ""; }; DC23D1D7238F390200423F62 /* OCLicenseAppStoreReceipt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCLicenseAppStoreReceipt.m; sourceTree = ""; }; DC243BF92317B446004FBB5C /* ThemeWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeWindow.swift; sourceTree = ""; }; @@ -1310,6 +1325,11 @@ DC4FEAE9209E48E800D4476B /* DispatchQueueTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueTools.swift; sourceTree = ""; }; DC51FD912475715F0069AB79 /* CellularSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellularSettingsViewController.swift; sourceTree = ""; }; DC576EC122647A070087316D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + DC5908742AA87A1700BFF393 /* BookmarkSetupStepEnterUsernameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupStepEnterUsernameViewController.swift; sourceTree = ""; }; + DC5908762AA87ABF00BFF393 /* BookmarkSetupStepAuthenticateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupStepAuthenticateViewController.swift; sourceTree = ""; }; + DC5908792AA87F6B00BFF393 /* BookmarkSetupStepFinishedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupStepFinishedViewController.swift; sourceTree = ""; }; + DC59087C2AA8B82200BFF393 /* BookmarkSetupStepPrepopulateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSetupStepPrepopulateViewController.swift; sourceTree = ""; }; + DC59087E2AA8D25400BFF393 /* CertificateSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateSummaryView.swift; sourceTree = ""; }; DC5C48A22918FB7400EBC053 /* CollectionSidebarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionSidebarViewController.swift; sourceTree = ""; }; DC5D58FE2A7166A300BFF393 /* ThemeCSS+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThemeCSS+SystemColors.swift"; sourceTree = ""; }; DC5D9E742496512400BFFE8E /* MessageQueueExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageQueueExample.swift; sourceTree = ""; }; @@ -2294,6 +2314,38 @@ path = "Cursor Support"; sourceTree = ""; }; + DC2323DA2AA7B5A100BFF393 /* Composer */ = { + isa = PBXGroup; + children = ( + DC2323DB2AA7B5D600BFF393 /* BookmarkComposer.swift */, + DC2323DD2AA7B6BB00BFF393 /* BookmarkComposerConfiguration.swift */, + ); + path = Composer; + sourceTree = ""; + }; + DC2323E12AA7C59900BFF393 /* Setup */ = { + isa = PBXGroup; + children = ( + DC2323DF2AA7C59400BFF393 /* BookmarkSetupViewController.swift */, + DC2323E22AA85D0300BFF393 /* BookmarkSetupStepViewController.swift */, + DC2323E72AA865DD00BFF393 /* Steps */, + ); + path = Setup; + sourceTree = ""; + }; + DC2323E72AA865DD00BFF393 /* Steps */ = { + isa = PBXGroup; + children = ( + DC2323E52AA865A700BFF393 /* BookmarkSetupStepEnterURLViewController.swift */, + DC5908742AA87A1700BFF393 /* BookmarkSetupStepEnterUsernameViewController.swift */, + DC5908762AA87ABF00BFF393 /* BookmarkSetupStepAuthenticateViewController.swift */, + DC59087C2AA8B82200BFF393 /* BookmarkSetupStepPrepopulateViewController.swift */, + DC5908792AA87F6B00BFF393 /* BookmarkSetupStepFinishedViewController.swift */, + DC59087E2AA8D25400BFF393 /* CertificateSummaryView.swift */, + ); + path = Steps; + sourceTree = ""; + }; DC23D1D0238F38DF00423F62 /* Receipt */ = { isa = PBXGroup; children = ( @@ -3423,6 +3475,8 @@ DCF4F1612051925A00189B9A /* Bookmarks */ = { isa = PBXGroup; children = ( + DC2323DA2AA7B5A100BFF393 /* Composer */, + DC2323E12AA7C59900BFF393 /* Setup */, DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */, 4CC46D202284C677009E938F /* BookmarkInfoViewController.swift */, 394B0CFB29F958200005CBFE /* AccountSettingsProvider.swift */, @@ -4312,18 +4366,23 @@ 6E4F1734217749910049A71B /* ImageDisplayViewController.swift in Sources */, DC8EB271239308E5009148F9 /* LicenseOffersViewController.swift in Sources */, 025FC742247D5004009307A7 /* MediaUploadOperation.swift in Sources */, + DC59087A2AA87F6B00BFF393 /* BookmarkSetupStepFinishedViewController.swift in Sources */, 4C11EE5B22E88D4200B84869 /* InstantMediaUploadTaskExtension.swift in Sources */, DC6CF7FB219446050013B9F9 /* LogSettingsViewController.swift in Sources */, 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */, + DC2323DC2AA7B5D600BFF393 /* BookmarkComposer.swift in Sources */, DC82D6FA23171339001551C5 /* ScanAction.swift in Sources */, DCC3700724D466D2008B0DEB /* DiagnosticManager.swift in Sources */, 23EC775D2137FB6B0032D4E6 /* WebViewDisplayViewController.swift in Sources */, 4C464BF12187AF1500D30602 /* PDFTocTableViewCell.swift in Sources */, DCE4E43E24C19C3E0051722F /* Action+UserInterface.swift in Sources */, + DC2323DE2AA7B6BB00BFF393 /* BookmarkComposerConfiguration.swift in Sources */, DC1B270C209CF34B004715E1 /* BookmarkViewController.swift in Sources */, 025FC745247EF0F1009307A7 /* BackgroundUploadsSettingsSection.swift in Sources */, DC63208321FCAC1E007EC0A8 /* ClientActivityViewController.swift in Sources */, DC9C1AEC247C76470067895A /* MessageGroupCell.swift in Sources */, + DC2323E32AA85D0300BFF393 /* BookmarkSetupStepViewController.swift in Sources */, + DC5908772AA87ABF00BFF393 /* BookmarkSetupStepAuthenticateViewController.swift in Sources */, 4C464BF62187AF1500D30602 /* PDFTocItem.swift in Sources */, 6E3A103E219D5BBA00F90C96 /* RenameAction.swift in Sources */, DCDC208F23994DFB003CFF5B /* LicenseTransactionsViewController.swift in Sources */, @@ -4340,9 +4399,11 @@ DC0030CB2350B75000BB8570 /* ScanViewController.swift in Sources */, 4C464BF42187AF1500D30602 /* PDFSearchTableViewCell.swift in Sources */, DCD1300A23A191C000255779 /* LicenseOfferButton.swift in Sources */, + DC2323E02AA7C59400BFF393 /* BookmarkSetupViewController.swift in Sources */, DCFEF90926EFA45A001DC7A4 /* VendorServices+App.swift in Sources */, 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, 399698ED260A3CEE00E5AEBA /* ImportPasteboardAction.swift in Sources */, + DC59087F2AA8D25400BFF393 /* CertificateSummaryView.swift in Sources */, 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */, DCC832DE242C0C3700153F8C /* DisplaySleepPreventer.swift in Sources */, 6E586CFC2199A72600F680C4 /* OpenInAction.swift in Sources */, @@ -4351,6 +4412,7 @@ 4C1561E8222321E0009C4EF3 /* PhotoSelectionViewController.swift in Sources */, 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */, DCE28F602433683700879DEC /* ClientSessionManager.swift in Sources */, + DC5908752AA87A1700BFF393 /* BookmarkSetupStepEnterUsernameViewController.swift in Sources */, 3968C881239C54AC00AC28AC /* ReleaseNotesHostViewController.swift in Sources */, 394B0CFC29F958200005CBFE /* AccountSettingsProvider.swift in Sources */, DC6428D02081406800493A01 /* CollapsibleProgressBar.swift in Sources */, @@ -4419,10 +4481,12 @@ DC6C0A4929239E560045FF2A /* AppRootViewController.swift in Sources */, 02DC7C9024CB354800DCB2C6 /* ProPhotoUploadSettingsSection.swift in Sources */, DCC832F6242CC5F700153F8C /* CardIssueMessagePresenter.swift in Sources */, + DC59087D2AA8B82200BFF393 /* BookmarkSetupStepPrepopulateViewController.swift in Sources */, DCD1301123A23F4E00255779 /* OCLicenseManager+AppStore.swift in Sources */, DC62514C225D254500736874 /* UploadBaseAction.swift in Sources */, 4C6B78102226B83300C5F3DB /* PhotoAlbumTableViewController.swift in Sources */, DCDA83852A9CE6C300BFF393 /* InitialSetupViewController.swift in Sources */, + DC2323E62AA865A700BFF393 /* BookmarkSetupStepEnterURLViewController.swift in Sources */, 23EC77592137F3DD0032D4E6 /* DisplayExtension.swift in Sources */, DCDF58B323CE82E100080BEB /* LicenseInAppPurchaseFeatureView.swift in Sources */, DC33939622E0747400DD3DA4 /* MakeAvailableOfflineAction.swift in Sources */, diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index 42ceaa148..56f671344 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -215,7 +215,7 @@ + isEnabled = "YES"> . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudAppShared + +class BookmarkComposer: NSObject { + // MARK: - Steps + enum Step: Equatable, Hashable { + case enterUsername + case enterURL(urlString: String?) + case authenticate(withCredentials: Bool, username: String?, password: String?) + case chooseServer(fromInstances: [OCServerInstance]) + // case name(withDefault: String) + case prepopulate + case finished + } + + var configuration: BookmarkComposerConfiguration + weak var delegate: BookmarkComposerDelegate? + + init(configuration: BookmarkComposerConfiguration, removeAuthDataFromCopy: Bool = false, delegate: BookmarkComposerDelegate?) { + self.configuration = configuration + self.delegate = delegate + + self.bookmark = configuration.bookmark?.copy() as? OCBookmark ?? OCBookmark() + + bookmark.authenticationDataStorage = .memory // Disconnect bookmark from keychain + + if bookmark.isTokenBased == true, removeAuthDataFromCopy { + bookmark.authenticationData = nil + } + + if bookmark.scanForAuthenticationMethodsRequired == true { + bookmark.authenticationMethodIdentifier = nil + bookmark.authenticationData = nil + } + } + + // MARK: - Internal storage + var bookmark: OCBookmark + + // MARK: - Connection instantiation + private var _cookieStorage : OCHTTPCookieStorage? + var cookieStorage : OCHTTPCookieStorage? { + if _cookieStorage == nil, let cookieSupportEnabled = OCCore.classSetting(forOCClassSettingsKey: .coreCookieSupportEnabled) as? Bool, cookieSupportEnabled == true { + _cookieStorage = OCHTTPCookieStorage() + Log.debug("Created cookie storage \(String(describing: _cookieStorage))") + } + + return _cookieStorage + } + + func instantiateConnection(for bmark: OCBookmark) -> OCConnection { + let connection = OCConnection(bookmark: bmark) + + connection.hostSimulator = OCHostSimulatorManager.shared.hostSimulator(forLocation: .accountSetup, for: self) + connection.cookieStorage = self.cookieStorage // Share cookie storage across all relevant connections + + return connection + } + + // MARK: - Setup steps + private var username: String? + private var password: String? + private var instances: [OCServerInstance]? + private var supportsInfinitePropfind: Bool? + private var performedInfinitePropfind: Bool = false + private var isDriveBased: Bool? + private var generationOptions: [OCAuthenticationMethodKey : Any]? + + // MARK: .enterURL + typealias Completion = (_ error: Error?, _ issue: OCIssue?, _ issueCompletionHandler: IssuesCardViewController.CompletionHandler?) -> Void + + func enterURL(_ urlString: String, completion: @escaping Completion) { + var username : NSString?, password: NSString? + var protocolWasPrepended : ObjCBool = false + + // Normalize URL + guard let serverURL = NSURL(username: &username, password: &password, afterNormalizingURLString: urlString, protocolWasPrepended: &protocolWasPrepended) as URL? else { + return + } + + // Check for zero-length host name + if (serverURL.host == nil) || ((serverURL.host != nil) && (serverURL.host?.count==0)) { + // Missing hostname + completion(nil, OCIssue(localizedTitle: "Missing hostname".localized, localizedDescription: "The entered URL does not include a hostname.".localized, level: .error), nil) + return + } + + // Save username and password for possible later use if they were part of the URL + if username != nil { + self.username = username as? String + } + + if password != nil { + self.password = password as? String + } + + // Probe URL + bookmark.url = serverURL + + let connection = instantiateConnection(for: bookmark) + + hudMessage = "Contacting server…".localized + + connection.prepareForSetup(options: nil) { [weak self] (issue, _, _, preferredAuthenticationMethods, generationOptions) in + self?.hudMessage = nil + + let continueToNextStep : () -> Void = { [weak self] in + self?.bookmark.authenticationMethodIdentifier = preferredAuthenticationMethods?.first + self?.pushUndoAction(undoAction: { composer in + composer.username = nil + composer.password = nil + + composer.bookmark.url = nil + composer.bookmark.authenticationMethodIdentifier = nil + + composer.generationOptions = nil + }) + self?.updateState() + } + + self?.generationOptions = generationOptions + + if let issue { + // Parse issue for display + if issue.prepareForDisplay().isAtLeast(level: .warning) { + // Present issues if the level is >= warning + completion(nil, issue, { [weak self, weak issue] (response) in + switch response { + case .cancel: + issue?.reject() + self?.bookmark.url = nil + + case .approve: + issue?.approve() + continueToNextStep() + + case .dismiss: + self?.bookmark.url = nil + } + }) + } else { + // Do not present issues + issue.approve() + continueToNextStep() + } + } else { + continueToNextStep() + } + } + } + + // MARK: .enterUsername + func enterUsername(_ username: String, completion: @escaping Completion) { + bookmark.serverLocationUserName = username + self.username = username + + self.pushUndoAction(undoAction: { composer in + composer.bookmark.serverLocationUserName = nil + composer.username = nil + }) + + completion(nil, nil, nil) + self.updateState() + } + + // MARK: .authenticate + func authenticate(username: String? = nil, password: String? = nil, presentingViewController: UIViewController?, completion: @escaping Completion) { + var options : [OCAuthenticationMethodKey : Any] = generationOptions ?? [:] + + let connection = instantiateConnection(for: bookmark) + + if let authMethodIdentifier = bookmark.authenticationMethodIdentifier { + if OCAuthenticationMethod.isAuthenticationMethodPassphraseBased(authMethodIdentifier as OCAuthenticationMethodIdentifier) { + options[.usernameKey] = username ?? "" + options[.passphraseKey] = password ?? "" + } + } + + options[.presentingViewControllerKey] = presentingViewController + options[.requiredUsernameKey] = bookmark.userName + + guard let bookmarkAuthenticationMethodIdentifier = bookmark.authenticationMethodIdentifier else { return } + + hudMessage = "Authenticating…".localized + + connection.generateAuthenticationData(withMethod: bookmarkAuthenticationMethodIdentifier, options: options) { (error, authMethodIdentifier, authMethodData) in + if error == nil, let authMethodIdentifier, let authMethodData { + self.bookmark.authenticationMethodIdentifier = authMethodIdentifier + self.bookmark.authenticationData = authMethodData + self.bookmark.scanForAuthenticationMethodsRequired = false + + self.hudMessage = "Fetching user information…".localized + + // Retrieve available instances for this account to chose from + connection.retrieveAvailableInstances(options: options, authenticationMethodIdentifier: authMethodIdentifier, authenticationData: authMethodData, completionHandler: { error, instances in + if error == nil, let instances, instances.count > 0 { + self.instances = instances + } + + if self.bookmark.isComplete { + self.bookmark.authenticationDataStorage = .keychain // Commit auth changes to keychain + } + + let continueCompletion : Completion = { (error, issue, issueCompletionHandler) in + completion(error,issue,issueCompletionHandler) + + if error == nil, issue == nil { + self.clearUndoStack() + self.updateState() + } + } + + if self.instances == nil { + // bookmark URL final -> retrieve server configuration right away + self.retrieveServerConfiguration(completion: continueCompletion) + } else { + // server instance needs to be chosen + if self.instances?.count == 1, let onlyInstance = self.instances?.first { + // If only one instance is returned, choose it right away + self.chooseServer(instance: onlyInstance, completion: continueCompletion) + } else { + continueCompletion(nil, nil, nil) + } + } + + Log.debug("\(connection) returned error=\(String(describing: error)) instances=\(String(describing: instances))") // Debug message also has the task to capture connection and avoid it being prematurely dropped + }) + } else { + self.hudMessage = nil + + var issue : OCIssue? + let nsError = error as NSError? + + if let embeddedIssue = nsError?.embeddedIssue() { + issue = embeddedIssue + } else if let error = error { + issue = OCIssue(forError: error, level: .error, issueHandler: nil) + } + + if nsError?.isOCError(withCode: .authorizationFailed) == true { + // Shake + completion(error, nil, nil) + } else if nsError?.isOCError(withCode: .authorizationCancelled) == true { + // User cancelled authorization, no reaction needed + } else if let issue { + completion(nil, issue, { [weak self, weak issue] (response) in + switch response { + case .cancel: + issue?.reject() + + case .approve: + issue?.approve() + self?.updateState() + + case .dismiss: break + } + }) + } + } + } + } + + func retrieveServerConfiguration(completion: @escaping Completion) { + let connection = instantiateConnection(for: bookmark) + + self.hudMessage = "Fetching server information…".localized + + connection.connect { [weak self] (error, issue) in + guard let strongSelf = self else { return } + + // Handle errors + guard error == nil, issue == nil else { + completion(error, issue, { [weak self, weak issue] (response) in + switch response { + case .cancel: + issue?.reject() + + case .approve: + issue?.approve() + self?.updateState() + + case .dismiss: break + } + }) + return + } + + // Inspect server configuration + strongSelf.bookmark.userDisplayName = connection.loggedInUser?.displayName + + strongSelf.isDriveBased = connection.capabilities?.spacesEnabled?.boolValue ?? false + strongSelf.supportsInfinitePropfind = connection.capabilities?.davPropfindSupportsDepthInfinity?.boolValue ?? false + + connection.disconnect(completionHandler: { + completion(nil, nil, nil) + }) + } + } + + // MARK: .chooseServer + func chooseServer(instance: OCServerInstance, completion: @escaping Completion) { + // Apply instance + self.bookmark.apply(instance) + + // Drop all other choices + self.instances = nil + + // Retrieve server configuration after instance changes have been applied + self.retrieveServerConfiguration(completion: completion) + } + + // MARK: .prepopulate + func prepopulate(completion: @escaping Completion) -> Progress? { + var prepopulationMethod : BookmarkPrepopulationMethod? + + // Determine prepopulation method + if prepopulationMethod == nil, let prepopulationMethodClassSetting = BookmarkViewController.classSetting(forOCClassSettingsKey: .prepopulation) as? String { + prepopulationMethod = BookmarkPrepopulationMethod(rawValue: prepopulationMethodClassSetting) + } + + if prepopulationMethod == nil, supportsInfinitePropfind == true { + prepopulationMethod = .streaming + } + + if prepopulationMethod == nil { + prepopulationMethod = .doNot + } + + if isDriveBased == true { + // Drive-based accounts do not support prepopulation yet + prepopulationMethod = .doNot + } + + // Prepopulation y/n? + if let prepopulationMethod, prepopulationMethod != .doNot { + // Perform prepopulation + var prepopulateProgress : Progress? + + // Perform prepopulation method + switch prepopulationMethod { + case .streaming: + performedInfinitePropfind = true + prepopulateProgress = bookmark.prepopulate(streamCompletionHandler: { _ in + completion(nil, nil, nil) + }) + + case .split: + performedInfinitePropfind = true + prepopulateProgress = bookmark.prepopulate(completionHandler: { _ in + completion(nil, nil, nil) + }) + + default: + completion(nil, nil, nil) + } + + // Present progress + return prepopulateProgress + } + + // No prepopulation + completion(nil, nil, nil) + return nil + } + + // MARK: .finished + func setName(_ bookmarkName: String?) { + self.bookmark.name = bookmarkName + } + + // MARK: - Undo + var undoStack: [UndoAction] = [] + + var canUndoLastStep: Bool { + return !undoStack.isEmpty + } + + func clearUndoStack() { + undoStack.removeAll() + } + + func pushUndoAction(undoAction: @escaping UndoAction) { + undoStack.append(undoAction) + } + + func undoLastStep() { + if undoStack.count > 0 { + let undoAction = undoStack.removeLast() + undoAction(self) + updateState() + } + } + + // MARK: - State + var currentStep: Step? { + didSet { + if oldValue != currentStep, let currentStep { + delegate?.present(composer: self, step: currentStep) + + Log.debug("BookmarkComposer.currentStep=\(currentStep)") + } + } + } + var hudMessage: String? { + didSet { + if hudMessage != oldValue { + delegate?.present(composer: self, hudMessage: hudMessage) + } + } + } + + typealias UndoAction = (_ composer: BookmarkComposer) -> Void + + func updateState() { + if OCServerLocator.useServerLocatorIdentifier != nil, bookmark.serverLocationUserName == nil { + currentStep = .enterUsername + } else if bookmark.url == nil { + if let absoluteURL = configuration.url?.absoluteString, !configuration.urlEditable { + enterURL(absoluteURL, completion: { [weak self] error, issue, issueCompletionHandler in + if let self { + self.delegate?.present(composer: self, error: error, issue: issue, issueCompletionHandler: issueCompletionHandler) + } + }) + } else { + currentStep = .enterURL(urlString: configuration.url?.absoluteString) + } + } else if bookmark.authenticationData == nil { + currentStep = .authenticate(withCredentials: bookmark.isTokenBased == false, username: username, password: password) + } else if let instances, instances.count > 0 { + currentStep = .chooseServer(fromInstances: instances) + } else if supportsInfinitePropfind == true, !performedInfinitePropfind { + currentStep = .prepopulate + } else { + currentStep = .finished + } + } + + // MARK: - Add or update bookmark + func addBookmark() -> OCBookmark { + OCBookmarkManager.shared.addBookmark(bookmark) + + return bookmark + } + + func updateBookmark(_ originalBookmark: OCBookmark? = nil) { + guard let originalBookmark = originalBookmark ?? configuration.bookmark else { + return + } + + originalBookmark.setValuesFrom(bookmark) + + if !OCBookmarkManager.shared.updateBookmark(originalBookmark) { + Log.error("Changes to \(originalBookmark) not saved as it's not tracked by OCBookmarkManager!") + } + } +} + +protocol BookmarkComposerDelegate : AnyObject { + func present(composer: BookmarkComposer, step: BookmarkComposer.Step) + func present(composer: BookmarkComposer, error: Error?, issue: OCIssue?, issueCompletionHandler: IssuesCardViewController.CompletionHandler?) + func present(composer: BookmarkComposer, hudMessage: String?) +} + +extension OCBookmark { + var isComplete: Bool { + return url != nil && authenticationMethodIdentifier != nil && authenticationData != nil + } +} diff --git a/ownCloud/Bookmarks/Composer/BookmarkComposerConfiguration.swift b/ownCloud/Bookmarks/Composer/BookmarkComposerConfiguration.swift new file mode 100644 index 000000000..3bfe4bd3c --- /dev/null +++ b/ownCloud/Bookmarks/Composer/BookmarkComposerConfiguration.swift @@ -0,0 +1,46 @@ +// +// BookmarkComposerConfiguration.swift +// ownCloud +// +// Created by Felix Schwarz on 05.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp +import ownCloudAppShared + +class BookmarkComposerConfiguration: NSObject { + var bookmark: OCBookmark? + + var url: URL? + var urlEditable: Bool = true + + var name: String? + var nameEditable: Bool = true + + init(bookmark: OCBookmark? = nil, url: URL? = nil, urlEditable: Bool, name: String? = nil, nameEditable: Bool) { + self.bookmark = bookmark + self.url = url + self.urlEditable = urlEditable + self.name = name + self.nameEditable = nameEditable + } +} + +extension BookmarkComposerConfiguration { + static var newBookmarkConfiguration: BookmarkComposerConfiguration { + return BookmarkComposerConfiguration(url: Branding.shared.profileURL, urlEditable: true, name: Branding.shared.profileBookmarkName, nameEditable: true) + } +} diff --git a/ownCloud/Bookmarks/Setup/BookmarkSetupStepViewController.swift b/ownCloud/Bookmarks/Setup/BookmarkSetupStepViewController.swift new file mode 100644 index 000000000..fccfed119 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/BookmarkSetupStepViewController.swift @@ -0,0 +1,228 @@ +// +// BookmarkSetupStepViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudAppShared + +class BookmarkSetupStepViewController: UIViewController { + weak var setupViewController: BookmarkSetupViewController? + var backgroundView: ThemeCSSView + var step: BookmarkComposer.Step + + init(with setupViewController: BookmarkSetupViewController, step: BookmarkComposer.Step) { + self.setupViewController = setupViewController + self.step = step + self.backgroundView = ThemeCSSView(withSelectors: [.background]) + self.backgroundView.translatesAutoresizingMaskIntoConstraints = false + + composerCompletion = { [weak setupViewController] (error, issue, issueCompletionHandler) in + if let setupViewController, let composer = setupViewController.composer { + setupViewController.present(composer: composer, error: error, issue: issue, issueCompletionHandler: issueCompletionHandler) + } + } + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var stepTitle: String? + var stepMessage: String? + var continueButtonLabelText: String? = "Proceed".localized + + var titleLabel: UILabel? + var messageLabel: UILabel? + var continueButton: UIButton = UIButton() + + var backButton: UIButton = UIButton() + + var contentContainerView: UIView? + var contentView: UIView? { + willSet { + if newValue != contentView { + contentView?.removeFromSuperview() + } + } + + didSet { + if let contentView { + contentContainerView?.embed(toFillWith: contentView) + } + } + } + + var bookmark: OCBookmark? { + return setupViewController?.composer?.bookmark + } + + var composerCompletion: BookmarkComposer.Completion + + var topViews: [UIView]? + var topViewsSpacing: CGFloat = 10 + var bottomViews: [UIView]? + var bottomViewsSpacing: CGFloat = 10 + + override func loadView() { + var views: [UIView] = topViews ?? [] + + let contentView = UIView() + contentView.cssSelectors = [.step] + + // Title & message + if let stepTitle { + titleLabel = ThemeCSSLabel(withSelectors: [.title]) + titleLabel?.translatesAutoresizingMaskIntoConstraints = false + titleLabel?.text = stepTitle + titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline, with: .bold) + titleLabel?.makeLabelWrapText() + + views.append(titleLabel!) + } + + if let stepMessage { + messageLabel = ThemeCSSLabel(withSelectors: [.message]) + messageLabel?.translatesAutoresizingMaskIntoConstraints = false + messageLabel?.text = stepMessage + messageLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline, with: .regular) + messageLabel?.makeLabelWrapText() + + views.append(messageLabel!) + } + + // Content container view + contentContainerView = UIView() + contentContainerView?.translatesAutoresizingMaskIntoConstraints = false + + if let contentContainerView { + views.append(contentContainerView) + } + + // Continue Button + var buttonConfiguration = UIButton.Configuration.borderedProminent() + buttonConfiguration.title = continueButtonLabelText + buttonConfiguration.cornerStyle = .large + + continueButton.translatesAutoresizingMaskIntoConstraints = false + continueButton.configuration = buttonConfiguration + continueButton.addAction(UIAction(handler: { [weak self] _ in + self?.handleContinue() + }), for: .primaryActionTriggered) + + views.append(continueButton) + + // Back button + if canGoBack { + buttonConfiguration = UIButton.Configuration.plain() + buttonConfiguration.title = "Back".localized + buttonConfiguration.cornerStyle = .large + + backButton.translatesAutoresizingMaskIntoConstraints = false + backButton.configuration = buttonConfiguration + backButton.addAction(UIAction(handler: { [weak self] _ in + self?.handleBack() + }), for: .primaryActionTriggered) + + views.append(backButton) + } + + // Bottom views + if let bottomViews { + views.append(contentsOf: bottomViews) + } + + // Embed background view + self.backgroundView.layer.cornerRadius = 10 + self.backgroundView.layer.masksToBounds = true + contentView.embed(toFillWith: backgroundView) + + // Layout views vertically in background view + contentView.embedVertically(views: views, insets: NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), spacingProvider: { leadingView, trailingView in + if leadingView == self.topViews?.last { + return self.topViewsSpacing + } + if trailingView == self.bottomViews?.first { + return self.bottomViewsSpacing + } + if trailingView == self.contentContainerView { + return 15 + } + if leadingView == self.contentContainerView { + return 30 + } + return 5 + }, centered: false) + + // Set view controller's view + self.view = contentView + } + + func handleContinue() { + } + + var canGoBack: Bool { + return setupViewController?.canGoBack ?? false + } + + func handleBack() { + self.setupViewController?.goBack() + } + + func present(error: Error?, issue: OCIssue?, issueCompletionHandler: IssuesCardViewController.CompletionHandler?) { + if let composer = setupViewController?.composer, error != nil || issue != nil { + self.setupViewController?.present(composer: composer, error: error, issue: issue, issueCompletionHandler: issueCompletionHandler) + } + } + + func buildTextField(withAction textChangedAction: UIAction?, forEvent actionEvent: UIControl.Event = .editingChanged, placeholder placeholderString: String = "", value textValue: String = "", secureTextEntry : Bool = false, keyboardType: UIKeyboardType = .default, autocorrectionType: UITextAutocorrectionType = .default, autocapitalizationType: UITextAutocapitalizationType = UITextAutocapitalizationType.none, enablesReturnKeyAutomatically: Bool = true, returnKeyType : UIReturnKeyType = .default, inputAccessoryView : UIView? = nil, identifier : String? = nil, accessibilityLabel: String? = nil, clearButtonMode : UITextField.ViewMode = .never, borderStyle: UITextField.BorderStyle = .none) -> UITextField { + let textField : UITextField = ThemeCSSTextField() + + textField.translatesAutoresizingMaskIntoConstraints = false + + textField.placeholder = placeholderString + textField.keyboardType = keyboardType + textField.autocorrectionType = autocorrectionType + textField.isSecureTextEntry = secureTextEntry + textField.autocapitalizationType = autocapitalizationType + textField.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically + textField.returnKeyType = returnKeyType + textField.inputAccessoryView = inputAccessoryView + textField.text = textValue + textField.accessibilityIdentifier = identifier + textField.clearButtonMode = clearButtonMode + textField.borderStyle = borderStyle + + if let textChangedAction { + textField.addAction(textChangedAction, for: actionEvent) + } + + textField.accessibilityLabel = accessibilityLabel + + textField.setContentHuggingPriority(.required, for: .vertical) + textField.setContentCompressionResistancePriority(.required, for: .horizontal) + + return textField + } + +} + +extension ThemeCSSSelector { + static let step = ThemeCSSSelector(rawValue: "step") +} diff --git a/ownCloud/Bookmarks/Setup/BookmarkSetupViewController.swift b/ownCloud/Bookmarks/Setup/BookmarkSetupViewController.swift new file mode 100644 index 000000000..6c3c7f701 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/BookmarkSetupViewController.swift @@ -0,0 +1,288 @@ +// +// BookmarkSetupViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 05.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp +import ownCloudAppShared + +class BookmarkSetupViewController: EmbeddingViewController, BookmarkComposerDelegate { + var composer: BookmarkComposer? + var configuration: BookmarkComposerConfiguration + + var stepControllerByStep : [BookmarkComposer.Step : UIViewController] = [:] + + var visibleContentContainerView: UIView = UIView() + + var headerTitle: String? + + init(configuration: BookmarkComposerConfiguration, headerTitle: String? = nil, cancelHandler: CancelHandler? = nil, doneHandler: DoneHandler? = nil) { + self.configuration = configuration + + super.init(nibName: nil, bundle: nil) + + self.headerTitle = headerTitle + self.doneHandler = doneHandler + self.cancelHandler = cancelHandler + + composer = BookmarkComposer(configuration: configuration, delegate: self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let contentView = UIView() + + visibleContentContainerView.translatesAutoresizingMaskIntoConstraints = false + + let backgroundView = ThemeCSSView(withSelectors: [.background]) + backgroundView.translatesAutoresizingMaskIntoConstraints = false + + contentView.embed(toFillWith: backgroundView) + contentView.embed(toFillWith: visibleContentContainerView, enclosingAnchors: contentView.safeAreaWithKeyboardAnchorSet) + + self.cssSelectors = [.modal, .accountSetup] + + // Add login background image + if let image = Branding.shared.brandedImageNamed(.loginBackground) { + let backgroundImageView = UIImageView(image: image) + backgroundImageView.contentMode = .scaleAspectFill + backgroundView.embed(toFillWith: backgroundImageView) + } + + // Add brand title + let logoImage = UIImage(named: "branding-login-logo") + let logoImageView = UIImageView(image: logoImage) + logoImageView.cssSelector = .icon + logoImageView.accessibilityLabel = VendorServices.shared.appName + logoImageView.contentMode = .scaleAspectFit + logoImageView.translatesAutoresizingMaskIntoConstraints = false + + if let logoImage { + // Keep aspect ratio + scale logo to 90% of available height + logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor, multiplier: (logoImage.size.width / logoImage.size.height) * 0.9).isActive = true + } + + let logoTitle = ThemeCSSLabel(withSelectors: [.title]) + logoTitle.translatesAutoresizingMaskIntoConstraints = false + logoTitle.font = .preferredFont(forTextStyle: .title2, with: .bold) + logoTitle.text = headerTitle ?? Branding.shared.appDisplayName + + let logoContainerView = UIView() + logoContainerView.translatesAutoresizingMaskIntoConstraints = false + logoContainerView.cssSelector = .header + logoContainerView.addSubview(logoImageView) + logoContainerView.addSubview(logoTitle) + + logoContainerView.embedHorizontally(views: [logoImageView, logoTitle], insets: NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) { _, _ in + return 10 + } + + contentView.addSubview(logoContainerView) + + NSLayoutConstraint.activate([ + logoContainerView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.safeAreaLayoutGuide.leadingAnchor), + logoContainerView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.safeAreaLayoutGuide.trailingAnchor), + logoContainerView.centerXAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.centerXAnchor).with(priority: .defaultHigh), + logoContainerView.topAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.topAnchor, constant: 20), + logoContainerView.heightAnchor.constraint(equalToConstant: 40) + ]) + + // Add cancel button + if cancelHandler != nil { + let cancelButton = ThemeCSSButton(withSelectors: [.cancel]) + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setTitle("Cancel".localized, for: .normal) + cancelButton.addAction(UIAction(handler: { [weak self] _ in + self?.cancel() + }), for: .primaryActionTriggered) + + contentView.addSubview(cancelButton) + + NSLayoutConstraint.activate([ + cancelButton.leadingAnchor.constraint(greaterThanOrEqualTo: logoContainerView.trailingAnchor, constant: 20), + cancelButton.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -20), + cancelButton.centerYAnchor.constraint(equalTo: logoContainerView.centerYAnchor) + ]) + } + + view = contentView + } + + override func viewDidLoad() { + super.viewDidLoad() + + composer?.updateState() + } + + // MARK: - Steps view controller and layout + func viewController(for step: BookmarkComposer.Step) -> UIViewController? { + if let stepViewController = stepControllerByStep[step] { + return stepViewController + } + + var stepViewController: UIViewController? + + switch step { + case .enterURL(urlString: _): + stepViewController = BookmarkSetupStepEnterURLViewController(with: self, step: step) + + case .enterUsername: + stepViewController = BookmarkSetupStepEnterUsernameViewController(with: self, step: step) + + case .authenticate(withCredentials: _, username: _, password: _): + stepViewController = BookmarkSetupStepAuthenticateViewController(with: self, step: step) + + case .chooseServer(fromInstances: _): + // Do not support server choice for now (also see present(composer:step:) + break + + case .prepopulate: + stepViewController = BookmarkSetupStepPrepopulateViewController(with: self, step: step) + + case .finished: + stepViewController = BookmarkSetupStepFinishedViewController(with: self, step: step) + } + + (stepViewController as? BookmarkSetupStepViewController)?.setupViewController = self + + stepControllerByStep[step] = stepViewController + + return stepViewController + } + + override func constraintsForEmbedding(contentViewController: UIViewController) -> [NSLayoutConstraint] { + return visibleContentContainerView.embed(centered: contentViewController.view!, minimumInsets: NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), constraintsOnly: true) + } + + // MARK: - HUD message + var hudMessageView: UIView? { + willSet { + hudMessageView?.removeFromSuperview() + + if newValue == nil { + contentViewController?.view.isHidden = false + } + } + + didSet { + if let hudMessageView { + visibleContentContainerView.embed(centered: hudMessageView, minimumInsets: NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)) + contentViewController?.view.isHidden = true + } + } + } + + // MARK: - History support + var canGoBack: Bool { + return composer?.canUndoLastStep ?? false + } + + func goBack() { + if canGoBack { + composer?.undoLastStep() + } + } + + // MARK: - Dismiss + typealias CancelHandler = () -> Void + var cancelHandler: CancelHandler? + + func cancel() { + cancelHandler?() + cancelHandler = nil + } + + typealias DoneHandler = (_ bookmark: OCBookmark?) -> Void + var doneHandler: DoneHandler? + + func done(bookmark: OCBookmark?) { + doneHandler?(bookmark) + doneHandler = nil + } + + // MARK: - Bookmark Composer Delegate + func present(composer: BookmarkComposer, step: BookmarkComposer.Step) { + if case let .chooseServer(fromInstances: instances) = step { + // Do not support server choice for now (also see viewController(for:) + // Pick first server from list + composer.chooseServer(instance: instances.first!, completion: { [weak self] error, issue, issueCompletionHandler in + guard let self, let composer = self.composer else { return } + self.present(composer: composer, error: error, issue: issue, issueCompletionHandler: issueCompletionHandler) + }) + } else { + OnMainThread { + let stepViewController = self.viewController(for: step) + stepViewController?.view.widthAnchor.constraint(greaterThanOrEqualToConstant: 400).with(priority: .defaultHigh).isActive = true + self.contentViewController = stepViewController + } + } + } + + func present(composer: BookmarkComposer, hudMessage: String?) { + OnMainThread { + if let hudMessage { + let indeterminateProgress = Progress.indeterminate() + indeterminateProgress.isCancellable = false + + self.hudMessageView = ComposedMessageView(elements: [ + .progressCircle(with: indeterminateProgress), + .title(hudMessage) + ]) + } else { + self.hudMessageView = nil + } + } + } + + func present(composer: BookmarkComposer, error: Error?, issue: OCIssue?, issueCompletionHandler: IssuesCardViewController.CompletionHandler?) { + OnMainThread { + var presentIssue: OCIssue? = issue + var completionHandler = issueCompletionHandler + + if presentIssue == nil, let error { + presentIssue = OCIssue(forError: error, level: .warning) + } + + if completionHandler == nil { + completionHandler = { [weak presentIssue] (response) in + switch response { + case .cancel: + presentIssue?.reject() + + case .approve: + presentIssue?.approve() + + case .dismiss: break + } + } + } + + if let presentIssue, let completionHandler { + let displayIssues = presentIssue.prepareForDisplay() + + IssuesCardViewController.present(on: self, issue: presentIssue, displayIssues: displayIssues, completion: completionHandler) + } + } + } +} + +extension ThemeCSSSelector { +} diff --git a/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepAuthenticateViewController.swift b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepAuthenticateViewController.swift new file mode 100644 index 000000000..c71121416 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepAuthenticateViewController.swift @@ -0,0 +1,91 @@ +// +// BookmarkSetupStepAuthenticateViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +class BookmarkSetupStepAuthenticateViewController: BookmarkSetupStepViewController { + var usernameField: UITextField? + var passwordField: UITextField? + + override func loadView() { + guard case let .authenticate(withCredentials: withCredentials, username: prefillUsername, password: prefillPassword) = step else { + return + } + + if withCredentials { + continueButtonLabelText = "Login".localized + } else { + stepTitle = "Login".localized + stepMessage = "If you 'Continue', you will be prompted to allow the '{{app.name}}' app to open the login page where you can enter your credentials.".localized + continueButtonLabelText = "Open login page".localized + } + + let certificateSummaryView = CertificateSummaryView(with: bookmark?.primaryCertificate, httpHostname: bookmark?.url?.host) + certificateSummaryView.translatesAutoresizingMaskIntoConstraints = false + + if withCredentials { + self.topViews = [ certificateSummaryView ] + self.topViewsSpacing = 20 + } + + super.loadView() + + if withCredentials { + usernameField = buildTextField(withAction: UIAction(handler: { [weak self] _ in + self?.updateState() + }), placeholder: "Username", value: prefillUsername ?? "", autocorrectionType: .no, autocapitalizationType: .none, accessibilityLabel: "Server Username".localized, borderStyle: .roundedRect) + usernameField?.textContentType = .username + + passwordField = buildTextField(withAction: UIAction(handler: { [weak self] _ in + self?.updateState() + }), placeholder: "Password", value: prefillPassword ?? "", secureTextEntry: true, autocorrectionType: .no, autocapitalizationType: .none, accessibilityLabel: "Server Password".localized, borderStyle: .roundedRect) + passwordField?.textContentType = .password + + let hostView = UIView() + hostView.translatesAutoresizingMaskIntoConstraints = false + + hostView.embedVertically(views: [usernameField!, passwordField!], insets: .zero, spacingProvider: { leadingView, trailingView in + return 10 + }, centered: false) + + contentView = hostView + } else { + contentView = certificateSummaryView + } + + updateState() + } + + func updateState() { + guard case let .authenticate(withCredentials: withCredentials, username: _, password: _) = step else { + return + } + + if withCredentials { + if let username = usernameField?.text, username.count > 0, let password = usernameField?.text, password.count > 0 { + continueButton.isEnabled = true + } else { + continueButton.isEnabled = false + } + } + } + + override func handleContinue() { + setupViewController?.composer?.authenticate(username: usernameField?.text, password: passwordField?.text, presentingViewController: self, completion: composerCompletion) + } +} diff --git a/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterURLViewController.swift b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterURLViewController.swift new file mode 100644 index 000000000..4a7bc4a71 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterURLViewController.swift @@ -0,0 +1,52 @@ +// +// BookmarkSetupStepEnterURLViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudAppShared + +class BookmarkSetupStepEnterURLViewController: BookmarkSetupStepViewController { + var urlTextField: UITextField? + + override func loadView() { + stepTitle = "Server URL".localized + + super.loadView() + + urlTextField = buildTextField(withAction: UIAction(handler: { [weak self] _ in + self?.updateState() + }), placeholder: "https://", keyboardType: .URL, autocorrectionType: .no, autocapitalizationType: .none, accessibilityLabel: "Server URL".localized, borderStyle: .roundedRect) + + contentView = urlTextField + + updateState() + } + + func updateState() { + if let urlString = urlTextField?.text, urlString.count > 0, NSURL(username: nil, password: nil, afterNormalizingURLString: urlString, protocolWasPrepended: nil) != nil { + continueButton.isEnabled = true + } else { + continueButton.isEnabled = false + } + } + + override func handleContinue() { + if let urlString = urlTextField?.text { + setupViewController?.composer?.enterURL(urlString, completion: composerCompletion) + } + } +} diff --git a/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterUsernameViewController.swift b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterUsernameViewController.swift new file mode 100644 index 000000000..59a4a2bb4 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepEnterUsernameViewController.swift @@ -0,0 +1,52 @@ +// +// BookmarkSetupStepEnterUsernameViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +class BookmarkSetupStepEnterUsernameViewController: BookmarkSetupStepViewController { + var usernameField: UITextField? + + override func loadView() { + stepTitle = "Username".localized + + super.loadView() + + usernameField = buildTextField(withAction: UIAction(handler: { [weak self] _ in + self?.updateState() + }), autocorrectionType: .no, autocapitalizationType: .none, accessibilityLabel: "Username".localized, borderStyle: .roundedRect) + usernameField?.textContentType = .username + + contentView = usernameField + + updateState() + } + + func updateState() { + if let username = usernameField?.text, username.count > 0 { + continueButton.isEnabled = true + } else { + continueButton.isEnabled = false + } + } + + override func handleContinue() { + if let username = usernameField?.text { + setupViewController?.composer?.enterUsername(username, completion: composerCompletion) + } + } +} diff --git a/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepFinishedViewController.swift b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepFinishedViewController.swift new file mode 100644 index 000000000..a2a48b2f4 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepFinishedViewController.swift @@ -0,0 +1,58 @@ +// +// BookmarkSetupStepFinishedViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +class BookmarkSetupStepFinishedViewController: BookmarkSetupStepViewController { + var bookmarkNameField: UITextField? + + override func loadView() { + stepTitle = "Account setup complete".localized + + if setupViewController?.configuration.nameEditable == true { + stepMessage = "If you'd like to give the account a custom name, please enter it below:".localized + } + + continueButtonLabelText = "Done".localized + + super.loadView() + + if setupViewController?.configuration.nameEditable == true { + bookmarkNameField = buildTextField(withAction: UIAction(handler: { [weak self] _ in + self?.updateName() + }), placeholder: setupViewController?.composer?.bookmark.shortName ?? "Name", value: setupViewController?.composer?.bookmark.name ?? "", autocorrectionType: .no, autocapitalizationType: .none, accessibilityLabel: "Name".localized, borderStyle: .roundedRect) + + contentView = bookmarkNameField + } + } + + func updateName() { + var name = bookmarkNameField?.text + + if name != nil, name?.count == 0 { + name = nil + } + + setupViewController?.composer?.setName(name) + } + + override func handleContinue() { + let bookmark = setupViewController?.composer?.addBookmark() + setupViewController?.done(bookmark: bookmark) + } +} diff --git a/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepPrepopulateViewController.swift b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepPrepopulateViewController.swift new file mode 100644 index 000000000..e12f16c9a --- /dev/null +++ b/ownCloud/Bookmarks/Setup/Steps/BookmarkSetupStepPrepopulateViewController.swift @@ -0,0 +1,22 @@ +// +// BookmarkSetupStepPrepopulateViewController.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit + +class BookmarkSetupStepPrepopulateViewController: BookmarkSetupStepViewController { +} diff --git a/ownCloud/Bookmarks/Setup/Steps/CertificateSummaryView.swift b/ownCloud/Bookmarks/Setup/Steps/CertificateSummaryView.swift new file mode 100644 index 000000000..c2f265893 --- /dev/null +++ b/ownCloud/Bookmarks/Setup/Steps/CertificateSummaryView.swift @@ -0,0 +1,125 @@ +// +// CertificateSummaryView.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.23. +// Copyright © 2023 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2023, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK +import ownCloudApp +import ownCloudAppShared + +class CertificateSummaryView: ThemeCSSView { + init(with certificate: OCCertificate?, httpHostname: String?) { + super.init() + + button.translatesAutoresizingMaskIntoConstraints = false + self.embed(toFillWith: button) + + button.addAction(UIAction(handler: { [weak self] _ in + self?.showCertificate() + }), for: .primaryActionTriggered) + + OnMainThread { + self.httpHostname = httpHostname + self.certificate = certificate + + self.update() + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var certificate: OCCertificate? { + didSet { + if let certificate { + certificate.validationResult(completionHandler: { (validationResult, shortDescription, _, color, _) in + OnMainThread { + self.validationResult = validationResult + self.statusText = shortDescription + self.statusColor = color + + self.update() + } + }) + } else { + OnMainThread { + self.validationResult = nil + self.statusText = nil + self.statusColor = .systemRed + + self.update() + } + } + } + } + + var httpHostname: String? + + var button: UIButton = UIButton() + + var validationResult: OCCertificateValidationResult? + var statusText: String? + var statusColor: UIColor? + + var statusImage: UIImage? { + var imageName: String? + + if certificate != nil { + if let validationResult { + + switch validationResult { + case .none, .error, .reject, .promptUser: + imageName = "lock.slash.fill" + + case .passed: + imageName = "lock.fill" + + case .userAccepted: + imageName = "exclamationmark.lock.fill" + } + } + } else { + imageName = "lock.open.fill" + } + + if let imageName { + return UIImage.init(systemName: imageName) + } + + return nil + } + + func update() { + var buttonConfiguration : UIButton.Configuration = .borderless() + buttonConfiguration.baseForegroundColor = statusColor + buttonConfiguration.image = self.statusImage + buttonConfiguration.title = certificate?.hostName ?? httpHostname + buttonConfiguration.buttonSize = .mini + + button.configuration = buttonConfiguration + } + + func showCertificate() { + if let certificate { + let certificateViewController : ThemeCertificateViewController = ThemeCertificateViewController(certificate: certificate, compare: nil) + let navigationController = ThemeNavigationController(rootViewController: certificateViewController) + + hostingViewController?.present(navigationController, animated: true, completion: nil) + } + } +} diff --git a/ownCloud/Licensing/Offers/LicenseOfferView.swift b/ownCloud/Licensing/Offers/LicenseOfferView.swift index 4e43924ee..b8e8e1808 100644 --- a/ownCloud/Licensing/Offers/LicenseOfferView.swift +++ b/ownCloud/Licensing/Offers/LicenseOfferView.swift @@ -204,7 +204,7 @@ class LicenseOfferView: UIView, Themeable { if let purchaseButton = purchaseButton { let progress = Progress.indeterminate() - progress?.isCancellable = false + progress.isCancellable = false purchaseBusyView = ProgressView() purchaseBusyView?.translatesAutoresizingMaskIntoConstraints = false diff --git a/ownCloudAppShared/Branding/Branding+App.swift b/ownCloudAppShared/Branding/Branding+App.swift index 4f3668a6d..c8a4deb24 100644 --- a/ownCloudAppShared/Branding/Branding+App.swift +++ b/ownCloudAppShared/Branding/Branding+App.swift @@ -442,4 +442,5 @@ extension Branding { public extension ThemeCSSSelector { static let welcome = ThemeCSSSelector(rawValue: "welcome") + static let accountSetup = ThemeCSSSelector(rawValue: "accountSetup") } diff --git a/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift index ce0730e25..42e345e6f 100644 --- a/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift +++ b/ownCloudAppShared/Client/View Controllers/ClientSidebarViewController.swift @@ -78,7 +78,7 @@ public class ClientSidebarViewController: CollectionSidebarViewController, Navig // Set up Collection View sectionsDataSource = combinedSectionsDatasource ?? accountsControllerSectionSource navigationItem.largeTitleDisplayMode = .never - navigationItem.titleView = self.buildNavigationLogoView() + navigationItem.titleView = ClientSidebarViewController.buildNavigationLogoView() // Add 10pt space at the top so that the first section's account doesn't "stick" to the top collectionView.contentInset.top += 10 @@ -210,7 +210,7 @@ public class ClientSidebarViewController: CollectionSidebarViewController, Navig // MARK: - Branding extension ClientSidebarViewController { - func buildNavigationLogoView() -> ThemeCSSView { + static public func buildNavigationLogoView() -> ThemeCSSView { let logoImage = UIImage(named: "branding-login-logo") let logoImageView = UIImageView(image: logoImage) logoImageView.cssSelector = .icon diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index b2d6f6b3d..b281fd66f 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -60,4 +60,20 @@ public extension UIView { return nil } + + // MARK: - View controller + var hostingViewController: UIViewController? { + var responder: UIResponder? = self + var hostViewController: UIViewController? + + while hostViewController == nil && responder != nil { + if let viewController = responder as? UIViewController { + hostViewController = viewController + } + + responder = responder?.next + } + + return hostViewController + } } diff --git a/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift b/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift index 4a74781d6..0820602e5 100644 --- a/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift +++ b/ownCloudAppShared/User Interface/SegmentView/UIView+EmbedAndLayout.swift @@ -23,19 +23,19 @@ public extension UIView { typealias ConstraintsModifier = (_ constraintSet: ConstraintSet) -> ConstraintSet struct ConstraintSet { - var firstLeadingOrTopConstraint: NSLayoutConstraint? - var lastTrailingOrBottomConstraint: NSLayoutConstraint? + public var firstLeadingOrTopConstraint: NSLayoutConstraint? + public var lastTrailingOrBottomConstraint: NSLayoutConstraint? } struct AnchorSet { - var leadingAnchor: NSLayoutXAxisAnchor - var trailingAnchor: NSLayoutXAxisAnchor + public var leadingAnchor: NSLayoutXAxisAnchor + public var trailingAnchor: NSLayoutXAxisAnchor - var topAnchor: NSLayoutYAxisAnchor - var bottomAnchor: NSLayoutYAxisAnchor + public var topAnchor: NSLayoutYAxisAnchor + public var bottomAnchor: NSLayoutYAxisAnchor - var centerXAnchor: NSLayoutXAxisAnchor - var centerYAnchor: NSLayoutYAxisAnchor + public var centerXAnchor: NSLayoutXAxisAnchor + public var centerYAnchor: NSLayoutYAxisAnchor } var defaultAnchorSet : AnchorSet { @@ -116,7 +116,7 @@ public extension UIView { return constraintSet } - @discardableResult func embedVertically(views: [UIView], insets: NSDirectionalEdgeInsets, enclosingAnchors: AnchorSet? = nil, spacingProvider: SpacingProvider? = nil, constraintsModifier: ConstraintsModifier? = nil) -> ConstraintSet { + @discardableResult func embedVertically(views: [UIView], insets: NSDirectionalEdgeInsets, enclosingAnchors: AnchorSet? = nil, spacingProvider: SpacingProvider? = nil, centered: Bool = true, constraintsModifier: ConstraintsModifier? = nil) -> ConstraintSet { var viewIdx : Int = 0 var previousView: UIView? var embedConstraints: [NSLayoutConstraint] = [] @@ -143,11 +143,18 @@ public extension UIView { } // - horizontal position + insets - embedConstraints.append(contentsOf: [ - view.centerXAnchor.constraint(equalTo: anchorSet.centerXAnchor), - view.leadingAnchor.constraint(greaterThanOrEqualTo: anchorSet.leadingAnchor, constant: insets.leading), - view.trailingAnchor.constraint(lessThanOrEqualTo: anchorSet.trailingAnchor, constant: -insets.trailing) - ]) + if centered { + embedConstraints.append(contentsOf: [ + view.centerXAnchor.constraint(equalTo: anchorSet.centerXAnchor), + view.leadingAnchor.constraint(greaterThanOrEqualTo: anchorSet.leadingAnchor, constant: insets.leading), + view.trailingAnchor.constraint(lessThanOrEqualTo: anchorSet.trailingAnchor, constant: -insets.trailing) + ]) + } else { + embedConstraints.append(contentsOf: [ + view.leadingAnchor.constraint(equalTo: anchorSet.leadingAnchor, constant: insets.leading), + view.trailingAnchor.constraint(equalTo: anchorSet.trailingAnchor, constant: -insets.trailing) + ]) + } // - bottom if viewIdx == (views.count-1) { @@ -194,10 +201,12 @@ public extension UIView { return constraints } - @discardableResult func embed(centered view: UIView, minimumInsets insets: NSDirectionalEdgeInsets = .zero, fixedSize: CGSize? = nil, minimumSize: CGSize? = nil, maximumSize: CGSize? = nil, enclosingAnchors: AnchorSet? = nil) -> [NSLayoutConstraint] { - view.translatesAutoresizingMaskIntoConstraints = false + @discardableResult func embed(centered view: UIView, minimumInsets insets: NSDirectionalEdgeInsets = .zero, fixedSize: CGSize? = nil, minimumSize: CGSize? = nil, maximumSize: CGSize? = nil, enclosingAnchors: AnchorSet? = nil, constraintsOnly: Bool = false) -> [NSLayoutConstraint] { + if !constraintsOnly { + view.translatesAutoresizingMaskIntoConstraints = false - addSubview(view) + addSubview(view) + } var constraints: [NSLayoutConstraint] let anchorSet = enclosingAnchors ?? defaultAnchorSet @@ -232,7 +241,9 @@ public extension UIView { ] } - NSLayoutConstraint.activate(constraints) + if !constraintsOnly { + NSLayoutConstraint.activate(constraints) + } return constraints } diff --git a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift index 3cf6a354c..0ffdb53ac 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift @@ -686,6 +686,10 @@ public class ThemeCollection : NSObject { ThemeCSSRecord(selectors: [.welcome, .message, .button], property: .fill, value: darkBrandColor), ThemeCSSRecord(selectors: [.welcome], property: .statusBarStyle, value: UIStatusBarStyle.lightContent), + // Account Setup + ThemeCSSRecord(selectors: [.accountSetup, .message, .title], property: .stroke, value: UIColor.white), + ThemeCSSRecord(selectors: [.accountSetup, .header, .title], property: .stroke, value: UIColor.white), + // Side Bar // - Interface Style ThemeCSSRecord(selectors: [.sidebar], property: .style, value: UIUserInterfaceStyle.light),