diff --git a/ios-sdk b/ios-sdk index da721dcae..4dbc4ad41 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit da721dcae69dea2f8813a36c8aeef00159a0bbae +Subproject commit 4dbc4ad4157eab1f9514aa8fc48d0148d228aeb4 diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 8bacbe3de..8d65d1079 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -487,6 +487,7 @@ DCC5E4472326564F002E5B84 /* NSObject+AnnotatedProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC5E444232654DE002E5B84 /* NSObject+AnnotatedProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC6564A20C9B7E400110A97 /* FileProviderExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC6564920C9B7E400110A97 /* FileProviderExtension.m */; }; DCC6566520C9B7E400110A97 /* ownCloud File Provider.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DCC6564620C9B7E300110A97 /* ownCloud File Provider.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DCC73F2E2B86BC960009A210 /* PasswordComposerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC73F2D2B86BC960009A210 /* PasswordComposerViewController.swift */; }; DCC832DE242C0C3700153F8C /* DisplaySleepPreventer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC832DD242C0C3700153F8C /* DisplaySleepPreventer.swift */; }; DCC832F1242CC27B00153F8C /* NotificationMessagePresenter.h in Headers */ = {isa = PBXBuildFile; fileRef = DCC832E6242CB18700153F8C /* NotificationMessagePresenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCC832F2242CC28400153F8C /* NotificationMessagePresenter.m in Sources */ = {isa = PBXBuildFile; fileRef = DCC832E7242CB18700153F8C /* NotificationMessagePresenter.m */; }; @@ -1558,6 +1559,7 @@ DCC6564920C9B7E400110A97 /* FileProviderExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileProviderExtension.m; sourceTree = ""; }; DCC6565120C9B7E400110A97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DCC6565220C9B7E400110A97 /* ownCloud_File_Provider.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ownCloud_File_Provider.entitlements; sourceTree = ""; }; + DCC73F2D2B86BC960009A210 /* PasswordComposerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordComposerViewController.swift; sourceTree = ""; }; DCC832DD242C0C3700153F8C /* DisplaySleepPreventer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySleepPreventer.swift; sourceTree = ""; }; DCC832E1242C0EAC00153F8C /* MessageSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSelector.swift; sourceTree = ""; }; DCC832E6242CB18700153F8C /* NotificationMessagePresenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationMessagePresenter.h; sourceTree = ""; }; @@ -1983,6 +1985,7 @@ DCA2EDDB279B0E5D001F04E6 /* Resource Sources */, DCE4E43424C199860051722F /* Actions */, 399EA6ED25E6544000B6FF11 /* Sharing */, + DCC73F2C2B86BC170009A210 /* Password Composer */, DCE4E42F24C1963F0051722F /* User Interface */, ); path = Client; @@ -3209,6 +3212,14 @@ path = "ownCloud File Provider"; sourceTree = ""; }; + DCC73F2C2B86BC170009A210 /* Password Composer */ = { + isa = PBXGroup; + children = ( + DCC73F2D2B86BC960009A210 /* PasswordComposerViewController.swift */, + ); + path = "Password Composer"; + sourceTree = ""; + }; DCC832D1242BB3E900153F8C /* Messages */ = { isa = PBXGroup; children = ( @@ -4822,6 +4833,7 @@ DCA2EDE4279B1789001F04E6 /* ResourceItemIcon.swift in Sources */, DC8E99E8297F3BA700594697 /* ActionTapGestureRecognizer.swift in Sources */, DCB5D56B2861BEBE004AF425 /* SearchViewController.swift in Sources */, + DCC73F2E2B86BC960009A210 /* PasswordComposerViewController.swift in Sources */, DCB1B8A729C75EBD00BFF393 /* ThemeCSS+AutoSelectors.swift in Sources */, 0287DD7D249131E000C912CA /* AppStatistics.swift in Sources */, DCB1B8A429C73DB800BFF393 /* ThemeCSSRecord.swift in Sources */, diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index 7b8e9542b..4e2f9ac5a 100644 Binary files a/ownCloud/Resources/de.lproj/Localizable.strings and b/ownCloud/Resources/de.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 8ba9448c5..f52df0440 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -536,7 +536,7 @@ "Shared with {{recipients}}" = "Shared with {{recipients}}"; "Expires {{expirationDate}}" = "Expires {{expirationDate}}"; "Share {{itemName}}" = "Share {{itemName}}"; -"Create link" = "Create link"; +"Create" = "Create"; "Invite" = "Invite"; "Invite Recipient" = "Invite Recipient"; "Recipients" = "Recipients"; @@ -558,7 +558,6 @@ "Shared with" = "Shared with"; "Remove Recipient failed" = "Remove Recipient failed"; "Remove Recipient" = "Remove Recipient"; -"Create" = "Create"; "Change" = "Change"; "Recipients can view or download contents." = "Recipients can view or download contents."; "Recipients can view, download, edit, delete and upload contents." = "Recipients can view, download, edit, delete and upload contents."; @@ -634,6 +633,12 @@ "Save changes" = "Save changes"; "Enter password" = "Enter password"; +"Change password" = "Change password"; +"Show" = "Show"; +"Hide" = "Hide"; + +"{{itemName}} ({{link}}) | password: {{password}}" = "{{itemName}} ({{link}}) | password: {{password}}"; +"{{link}} | password: {{password}}" = "{{link}} | password: {{password}}"; /* Quick Access view */ "Quick Access" = "Quick Access"; diff --git a/ownCloudAppShared/Client/Password Composer/PasswordComposerViewController.swift b/ownCloudAppShared/Client/Password Composer/PasswordComposerViewController.swift new file mode 100644 index 000000000..cdc58b62d --- /dev/null +++ b/ownCloudAppShared/Client/Password Composer/PasswordComposerViewController.swift @@ -0,0 +1,283 @@ +// +// PasswordComposerViewController.swift +// ownCloudAppShared +// +// Created by Felix Schwarz on 23.02.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import ownCloudSDK + +class PasswordComposerViewController: UIViewController { + typealias ResultHandler = (_ password: String?, _ cancelled: Bool) -> Void + + var resultHandler: ResultHandler? + + let passwordLabel = ThemeCSSLabel(withSelectors: [ .label, .secondary ]) + let passwordFieldContainer = ThemeCSSView(withSelectors: [ .cell ]) + let passwordField = ThemeCSSTextField() + + let componentToolbar = SegmentView(with: [], truncationMode: .none, scrollable: false) + + let validationReportContainerView = ThemeCSSView(withSelectors: [ .cell ]) + + lazy var showPasswordSegment: SegmentViewItem = { + return SegmentViewItem.button(title: "Show".localized, customizeButton: { _, config in + var buttonConfig = config + buttonConfig.image = OCSymbol.icon(forSymbolName: "eye")?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(scale: .small)) + buttonConfig.imagePadding = 5 + return buttonConfig + }, action: UIAction(handler: { [weak self] _ in + self?.showPassword = true + })) + }() + lazy var hidePasswordSegment: SegmentViewItem = { + return SegmentViewItem.button(title: "Hide".localized, customizeButton: { _, config in + var buttonConfig = config + buttonConfig.image = OCSymbol.icon(forSymbolName: "eye.slash")?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(scale: .small)) + buttonConfig.imagePadding = 5 + return buttonConfig + }, action: UIAction(handler: { [weak self] _ in + self?.showPassword = false + })) + }() + lazy var generatePasswordSegment: SegmentViewItem = { + return SegmentViewItem.button(title: "Generate".localized, action: UIAction(handler: { [weak self] _ in + self?.generatePassword() + })) + }() + lazy var copyPasswordSegment: SegmentViewItem = { + return SegmentViewItem.button(title: "Copy".localized, action: UIAction(handler: { [weak self] _ in + self?.copyToClipboard() + })) + }() + + var saveButton: UIBarButtonItem? + + var passwordPolicy: OCPasswordPolicy + + init(password: String, policy: OCPasswordPolicy, saveButtonTitle: String, resultHandler: @escaping ResultHandler) { + self.passwordPolicy = policy + + super.init(nibName: nil, bundle: nil) + + defer { + // Placing this in a defer block makes sure that didSet is called for the respective properties + self.password = password + self.showPassword = false + } + + self.resultHandler = resultHandler + + saveButton = UIBarButtonItem(title: saveButtonTitle, style: .done, target: self, action: #selector(save)) + + navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel".localized, style: .plain, target: self, action: #selector(cancel)) + navigationItem.rightBarButtonItem = saveButton + navigationItem.title = "Password".localized + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let rootView = ThemeCSSView(withSelectors: [ .grouped, .collection ]) + let padding = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + let labelFieldSpacing: CGFloat = 10 + let fieldToolbarSpacing: CGFloat = 15 + let toolbarValidationReportSpacing: CGFloat = 15 + + passwordLabel.translatesAutoresizingMaskIntoConstraints = false + passwordFieldContainer.translatesAutoresizingMaskIntoConstraints = false + passwordField.translatesAutoresizingMaskIntoConstraints = false + componentToolbar.translatesAutoresizingMaskIntoConstraints = false + componentToolbar.setContentHuggingPriority(.defaultHigh, for: .horizontal) + validationReportContainerView.translatesAutoresizingMaskIntoConstraints = false + + passwordFieldContainer.layer.cornerRadius = 5 + validationReportContainerView.layer.cornerRadius = 10 + + passwordField.cssSelectors = [ .cell ] + passwordFieldContainer.embed(toFillWith: passwordField, insets: NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)) + + componentToolbar.insets = .zero + componentToolbar.itemSpacing = 0 + + rootView.addSubview(passwordLabel) + rootView.addSubview(passwordFieldContainer) + rootView.addSubview(componentToolbar) + rootView.addSubview(validationReportContainerView) + + passwordLabel.text = "Password".localized + passwordLabel.font = UIFont.systemFont(ofSize: UIFont.smallSystemFontSize) + + passwordField.placeholder = "Password".localized + passwordField.clearButtonMode = .always + passwordField.addAction(UIAction(handler: { [weak self] _ in + self?.passwordChanged() + }), for: .editingChanged) + + rootView.addConstraints([ + passwordLabel.topAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.topAnchor, constant: padding.top), + passwordLabel.leadingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.leadingAnchor, constant: padding.left), + passwordLabel.trailingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.trailingAnchor, constant: -padding.right), + + passwordFieldContainer.topAnchor.constraint(equalTo: passwordLabel.bottomAnchor, constant: labelFieldSpacing), + passwordFieldContainer.leadingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.leadingAnchor, constant: padding.left), + passwordFieldContainer.trailingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.trailingAnchor, constant: -padding.right), + + componentToolbar.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: fieldToolbarSpacing), + componentToolbar.leadingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.leadingAnchor, constant: padding.left - 5), + componentToolbar.trailingAnchor.constraint(lessThanOrEqualTo: rootView.safeAreaLayoutGuide.trailingAnchor, constant: -padding.right), + + validationReportContainerView.topAnchor.constraint(equalTo: componentToolbar.bottomAnchor, constant: toolbarValidationReportSpacing), + validationReportContainerView.leadingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.leadingAnchor, constant: padding.left), + validationReportContainerView.trailingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.trailingAnchor, constant: -padding.right) + ]) + + view = rootView + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + validatePasssword() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + passwordField.becomeFirstResponder() + } + + func passwordChanged() { + password = passwordField.text ?? "" + } + + func validatePasssword() { + let report = passwordPolicy.validate(password) + var lines : [UIView] = [] + var failures: Int = 0 + + for rule in report.rules { + var ruleDescription: String? = rule.localizedDescription + + if !(rule is OCPasswordPolicyRuleCharacters), let result = report.result(for: rule) { + ruleDescription = result + } + + if let ruleDescription { + let passedValidation = report.passedValidation(for: rule) + let symbolConfiguration = UIImage.SymbolConfiguration(hierarchicalColor: passedValidation ? .systemGreen : .systemRed) + let line = SegmentView(with: [ + SegmentViewItem(with: UIImage(systemName: passedValidation ? "checkmark.circle.fill" : "xmark.circle.fill")?.withConfiguration(symbolConfiguration), iconRenderingMode: .automatic, title: ruleDescription) + ], truncationMode: .truncateTail) + line.translatesAutoresizingMaskIntoConstraints = false + line.insets = .zero + + if passedValidation { + lines.append(line) + } else { + lines.insert(line, at: failures) + failures += 1 + } + } + } + + for subview in validationReportContainerView.subviews { + subview.removeFromSuperview() + } + + validationReportContainerView.embedVertically(views: lines, insets: NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), enclosingAnchors: validationReportContainerView.safeAreaAnchorSet, centered: false) + + saveButton?.isEnabled = report.passedValidation + } + + func updateSegments() { + var items: [SegmentViewItem] = [] + + // Show/Hide password + if showPassword { + items.append(hidePasswordSegment) + } else { + items.append(showPasswordSegment) + } + + // Generate password + items.append(SegmentViewItem(title: "|", style: .label)) + items.append(generatePasswordSegment) + + // Copy password + if password.count > 0 { + items.append(SegmentViewItem(title: "|", style: .label)) + items.append(copyPasswordSegment) + } + + if componentToolbar.items != items { + componentToolbar.items = items + } + } + + var password: String { + get { + return passwordField.text ?? "" + } + + set { + passwordField.text = newValue + + updateSegments() + validatePasssword() + } + } + var showPassword: Bool = false { + didSet { + passwordField.isSecureTextEntry = !showPassword + updateSegments() + } + } + + func generatePassword() { + var generatedPassword: String? + do { + try generatedPassword = passwordPolicy.generatePassword(withMinLength: nil, maxLength: nil) + } catch let error as NSError { + Log.error("Error generating password: \(error)") + } + if let generatedPassword { + password = generatedPassword + } + } + + func copyToClipboard() { + } + + func viewControllerForPresentation() -> ThemeNavigationController { + let navigationViewController = ThemeNavigationController(rootViewController: self) + navigationViewController.cssSelectors = [ .modal ] + + return navigationViewController + } + + @objc func save() { + presentingViewController?.dismiss(animated: true, completion: { + self.resultHandler?(self.password, false) + }) + } + + @objc func cancel() { + presentingViewController?.dismiss(animated: true, completion: { + self.resultHandler?(nil, true) + }) + } +} diff --git a/ownCloudAppShared/Client/Sharing/ShareViewController.swift b/ownCloudAppShared/Client/Sharing/ShareViewController.swift index 873d9173e..58218ca9c 100644 --- a/ownCloudAppShared/Client/Sharing/ShareViewController.swift +++ b/ownCloudAppShared/Client/Sharing/ShareViewController.swift @@ -270,11 +270,15 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe navigationItem.titleLabelText = navigationTitle // Add bottom button bar - let title = (mode == .create) ? ((type == .link) ? "Create link".localized : "Invite".localized) : "Save changes".localized + let isLinkCreation = (mode == .create) && (type == .link) + let title = (mode == .create) ? ((type == .link) ? "Share".localized : "Invite".localized) : "Save changes".localized + let altTitle = isLinkCreation ? "Create".localized : nil - bottomButtonBar = BottomButtonBar(selectButtonTitle: title, cancelButtonTitle: "Cancel".localized, hasCancelButton: true, selectAction: UIAction(handler: { [weak self] _ in + bottomButtonBar = BottomButtonBar(selectButtonTitle: title, alternativeButtonTitle: altTitle, cancelButtonTitle: "Cancel".localized, hasAlternativeButton: isLinkCreation, hasCancelButton: true, selectAction: UIAction(handler: { [weak self] _ in + self?.save(andShare: isLinkCreation) + }), alternativeAction: isLinkCreation ? UIAction(handler: { [weak self] _ in self?.save() - }), cancelAction: UIAction(handler: { [weak self] _ in + }) : nil, cancelAction: UIAction(handler: { [weak self] _ in self?.complete() })) bottomButtonBar?.showActivityIndicatorWhileModalActionRunning = mode != .edit @@ -533,13 +537,18 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe // MARK: - State func updateState() { + var createIsEnabled: Bool + switch type { case .link: - bottomButtonBar?.selectButton.isEnabled = (location != nil) && (role != nil) && (permissions != nil) + createIsEnabled = (location != nil) && (role != nil) && (permissions != nil) case .share: - bottomButtonBar?.selectButton.isEnabled = (location != nil) && (recipient != nil) && (role != nil) && (permissions != nil) + createIsEnabled = (location != nil) && (recipient != nil) && (role != nil) && (permissions != nil) } + + bottomButtonBar?.selectButton.isEnabled = createIsEnabled + bottomButtonBar?.alternativeButton.isEnabled = createIsEnabled } // MARK: - Options @@ -551,6 +560,10 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe var expirationDatePicker: UIDatePicker? var expirationDate: Date? + var passwordPolicy: OCPasswordPolicy { + return clientContext?.core?.connection.capabilities?.passwordPolicy ?? OCPasswordPolicy.default + } + func updateOptions() { let hasPasswordOption = type == .link let hasExpirationOption = true @@ -601,9 +614,7 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe if passwordOption == nil { passwordOption = OptionItem(kind: .single, content: content, state: false, selectionAction: { [weak self] optionItem in - if self?.hasPassword == true { - self?.requestPassword() - } + self?.requestPassword() }) } else { passwordOption?.content = content @@ -680,23 +691,21 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe return ((share?.protectedByPassword == true) && !removePassword) || (password != nil) } func requestPassword() { - let passwordPrompt = UIAlertController(title: "Enter password".localized, message: nil, preferredStyle: .alert) - - passwordPrompt.addTextField(configurationHandler: { textField in - textField.placeholder = "Password".localized - textField.isSecureTextEntry = true + let passwordViewController = PasswordComposerViewController(password: password ?? "", policy: passwordPolicy, saveButtonTitle: "Set".localized, resultHandler: { [weak self] password, cancelled in + if !cancelled, let password { + self?.password = password + self?.updateOptions() + } }) + let navigationViewController = passwordViewController.viewControllerForPresentation() - passwordPrompt.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel)) - passwordPrompt.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: { [weak self, weak passwordPrompt] action in - self?.password = passwordPrompt?.textFields?.first?.text - self?.updateOptions() - })) + if mode == .edit, hasPassword { + passwordViewController.navigationItem.title = "Change password".localized + } - self.clientContext?.present(passwordPrompt, animated: true) + self.clientContext?.present(navigationViewController, animated: true) } func generatePassword() { - let passwordPolicy = clientContext?.core?.connection.capabilities?.passwordPolicy ?? OCPasswordPolicy.default var generatedPassword: String? do { try generatedPassword = passwordPolicy.generatePassword(withMinLength: nil, maxLength: nil) @@ -710,7 +719,9 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe } // MARK: - Save (edit + create) - func save() { + func save(andShare: Bool = false) { + let presentingViewController = UIDevice.current.isIpad ? self : self.presentingViewController + switch mode { case .create: var newShare: OCShare? @@ -755,6 +766,67 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe if let error { self.showError(error) } else { + if let url = share?.url, andShare { + let existingCompletionHandler = UIDevice.current.isIpad ? { (share) in + // On iPad, first show Share Sheet, then close ShareViewController + self.complete(with: share) + } : self.completionHandler // On iPhone, first close ShareViewController, then show Share Sheet + + let handleResultAndShowShareSheet: CompletionHandler = { (share) in + let absoluteURLString = url.absoluteString + var shareMessage: String? + + if let password = self.password { + // Message consists of Link + Password + if let displayName = self.location?.displayName(in: nil) { + shareMessage = "{{itemName}} ({{link}}) | password: {{password}}".localized([ + "itemName" : displayName, + "link" : absoluteURLString, + "password" : password + ]) + } else { + shareMessage = "{{link}} | password: {{password}}".localized([ + "link" : absoluteURLString, + "password" : password + ]) + } + } else { + // Message consists of Link only + shareMessage = absoluteURLString + } + + if let shareMessage, let presentingViewController { + // Show Share Sheet + OnMainThread { + let shareViewController = UIActivityViewController(activityItems: [shareMessage], applicationActivities:nil) + + if UIDevice.current.isIpad { + shareViewController.popoverPresentationController?.sourceView = self.bottomButtonBar?.selectButton ?? self.view + } + + shareViewController.completionWithItemsHandler = { (_, _, _, _) in + // Completed + existingCompletionHandler?(share) + } + + presentingViewController.present(shareViewController, animated: true, completion: nil) + } + } else { + // Completed + existingCompletionHandler?(share) + } + } + + if UIDevice.current.isIpad { + // On iPad, first show Share Sheet, then close ShareViewController + handleResultAndShowShareSheet(share) + return // Avoid calling self.complete(with:), called via existingCompletionHandler + } else { + // On iPhone, first close ShareViewController, then show Share Sheet + self.completionHandler = handleResultAndShowShareSheet + } + } + self.complete(with: share) } }) diff --git a/ownCloudAppShared/Client/User Interface/BottomButtonBar.swift b/ownCloudAppShared/Client/User Interface/BottomButtonBar.swift index 6419d8488..11869b00a 100644 --- a/ownCloudAppShared/Client/User Interface/BottomButtonBar.swift +++ b/ownCloudAppShared/Client/User Interface/BottomButtonBar.swift @@ -85,6 +85,7 @@ open class BottomButtonBar: ThemeCSSView { } cancelButton.isEnabled = !modalActionRunning + alternativeButton.isEnabled = !modalActionRunning selectButton.isEnabled = !modalActionRunning } } diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift index 9faf7c346..6083feaed 100644 --- a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItem.swift @@ -41,6 +41,7 @@ public class SegmentViewItem: NSObject { open var style: Style open var icon: UIImage? + open var iconRenderingMode: UIImage.RenderingMode? open var title: String? { didSet { _view = nil @@ -77,12 +78,13 @@ public class SegmentViewItem: NSObject { return _view } - public init(with icon: UIImage? = nil, title: String? = nil, style: Style = .plain, titleTextStyle: UIFont.TextStyle? = nil, titleTextWeight: UIFont.Weight? = nil, linebreakMode: NSLineBreakMode? = nil, lines: [Line]? = nil, view: UIView? = nil, representedObject: AnyObject? = nil, weakRepresentedObject: AnyObject? = nil, gestureRecognizers: [UIGestureRecognizer]? = nil) { + public init(with icon: UIImage? = nil, iconRenderingMode: UIImage.RenderingMode? = nil, title: String? = nil, style: Style = .plain, titleTextStyle: UIFont.TextStyle? = nil, titleTextWeight: UIFont.Weight? = nil, linebreakMode: NSLineBreakMode? = nil, lines: [Line]? = nil, view: UIView? = nil, representedObject: AnyObject? = nil, weakRepresentedObject: AnyObject? = nil, gestureRecognizers: [UIGestureRecognizer]? = nil) { self.style = style super.init() self.icon = icon + self.iconRenderingMode = iconRenderingMode self.title = title self.titleTextStyle = titleTextStyle self.titleTextWeight = titleTextWeight @@ -112,3 +114,22 @@ extension [SegmentViewItem] { }) } } + +extension SegmentViewItem { + public static func button(title: String, customizeButton: ((UIButton, UIButton.Configuration) -> UIButton.Configuration)? = nil, action: UIAction) -> SegmentViewItem { + var buttonConfig = UIButton.Configuration.plain() + buttonConfig.title = title + buttonConfig.contentInsets = .zero + + let button = ThemeCSSButton() + + if let customizeButton { + buttonConfig = customizeButton(button, buttonConfig) + } + + button.configuration = buttonConfig + button.addAction(action, for: .primaryActionTriggered) + + return SegmentViewItem(view: button) + } +} diff --git a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift index 084423c85..c80483baa 100644 --- a/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift +++ b/ownCloudAppShared/User Interface/SegmentView/SegmentViewItemView.swift @@ -65,7 +65,7 @@ public class SegmentViewItemView: ThemeView, ThemeCSSAutoSelector { if let icon = item.icon { iconView = UIImageView() iconView?.cssSelector = .icon - iconView?.image = icon.withRenderingMode(.alwaysTemplate) + iconView?.image = icon.withRenderingMode(item.iconRenderingMode ?? .alwaysTemplate) iconView?.contentMode = .scaleAspectFit iconView?.translatesAutoresizingMaskIntoConstraints = false iconView?.setContentHuggingPriority(.required, for: .horizontal) diff --git a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift index d61680635..bb450748e 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift @@ -327,6 +327,8 @@ public class ThemeCollection : NSObject { cellStateSet = ThemeColorStateSet.from(colorSet: cellSet, for: interfaceStyle) collectionBackgroundColor = darkBrandColor.darker(0.1) + cellSet.backgroundColor + groupedCellSet = ThemeColorSet.from(backgroundColor: darkBrandColor, tintColor: lightBrandColor, for: interfaceStyle) groupedCellStateSet = ThemeColorStateSet.from(colorSet: groupedCellSet, for: interfaceStyle) groupedCollectionBackgroundColor = useSystemColors ? .systemGroupedBackground.resolvedColor(with: styleTraitCollection) : navigationBarSet.backgroundColor.darker(0.3) @@ -505,12 +507,14 @@ public class ThemeCollection : NSObject { ThemeCSSRecord(selectors: [.collection, .selected, .selectionCheckmark], property: .stroke, value: UIColor.white), - ThemeCSSRecord(selectors: [.grouped, .collection], property: .fill, value: groupedCollectionBackgroundColor), - ThemeCSSRecord(selectors: [.insetGrouped, .collection], property: .fill, value: groupedCollectionBackgroundColor), - ThemeCSSRecord(selectors: [.grouped, .collection, .sectionHeader], property: .fill, value: groupedCollectionBackgroundColor), - ThemeCSSRecord(selectors: [.grouped, .collection, .cell, .action], property: .fill, value: cellStateSet.regular.backgroundColor), - ThemeCSSRecord(selectors: [.insetGrouped, .collection, .sectionHeader], property: .fill, value: groupedCollectionBackgroundColor), - ThemeCSSRecord(selectors: [.insetGrouped, .collection, .cell, .action], property: .fill, value: cellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.grouped, .collection], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.insetGrouped, .collection], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.grouped, .collection, .sectionHeader], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.grouped, .collection, .sectionHeader, .cell], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.grouped, .collection, .cell], property: .fill, value: groupedCellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.grouped, .collection, .cell, .action], property: .fill, value: cellStateSet.regular.backgroundColor), + ThemeCSSRecord(selectors: [.insetGrouped, .collection, .sectionHeader], property: .fill, value: groupedCollectionBackgroundColor), + ThemeCSSRecord(selectors: [.insetGrouped, .collection, .cell, .action], property: .fill, value: cellStateSet.regular.backgroundColor), // - Table View ThemeCSSRecord(selectors: [.table], property: .fill, value: cellStateSet.regular.backgroundColor),