diff --git a/ios-sdk b/ios-sdk index adea1a552..7c7a1425e 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit adea1a55209069502d392b80019bfec908c5161e +Subproject commit 7c7a1425e0bb5d7e9eb70386430c6502012cf4ab diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 1c8a63896..1a51244a9 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -274,6 +274,8 @@ DC0A35A124C1091400FB58FC /* UserInterfaceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0A35A024C1091400FB58FC /* UserInterfaceContext.swift */; }; DC0A5C432550C70800E6674B /* class-settings-sdk in Resources */ = {isa = PBXBuildFile; fileRef = DC0A5C422550C70800E6674B /* class-settings-sdk */; }; DC0B379420514E4700189B9A /* ServerListBookmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */; }; + DC0CE19228C7DBE3009ABDFB /* OpenInWebAppAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */; }; + DC0CE19D28C89CD9009ABDFB /* CreateDocumentAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */; }; DC18898E218A773700CFB3F9 /* ownCloudMocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; DC1B270C209CF34B004715E1 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */; }; DC20DE5C21C01A3D0096000B /* ownCloudMocking.framework in EarlGrey Copy Files */ = {isa = PBXBuildFile; fileRef = DC0196A620F754CA00C41B78 /* ownCloudMocking.framework */; }; @@ -555,6 +557,7 @@ DCFC9ED128002335005D9144 /* CollectionViewCellProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ED028002335005D9144 /* CollectionViewCellProvider.swift */; }; DCFC9ED3280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ED2280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift */; }; DCFC9ED528002F33005D9144 /* CollectionViewCellConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFC9ED428002F33005D9144 /* CollectionViewCellConfiguration.swift */; }; + DCFE682728D869A400091D2A /* ClientWebAppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */; }; DCFEF90926EFA45A001DC7A4 /* VendorServices+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFEF90526EFA45A001DC7A4 /* VendorServices+App.swift */; }; DCFEFE2A236876BD009A142F /* OCLicenseManager.h in Headers */ = {isa = PBXBuildFile; fileRef = DCFEFE28236876BD009A142F /* OCLicenseManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; DCFEFE2B236876BD009A142F /* OCLicenseManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DCFEFE29236876BD009A142F /* OCLicenseManager.m */; }; @@ -1249,6 +1252,8 @@ DC0B379320514E4700189B9A /* ServerListBookmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListBookmarkCell.swift; sourceTree = ""; }; DC0B37952051541C00189B9A /* ownCloud.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ownCloud.entitlements; sourceTree = ""; }; DC0B37962051681600189B9A /* ThemeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeButton.swift; sourceTree = ""; }; + DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInWebAppAction.swift; sourceTree = ""; }; + DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDocumentAction.swift; sourceTree = ""; }; DC136581208223F000FC0F60 /* OCBookmark+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OCBookmark+Extension.swift"; sourceTree = ""; }; DC1AC7CF2319ADAE002B7892 /* ScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanViewController.swift; sourceTree = ""; }; DC1B270B209CF34B004715E1 /* BookmarkViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewController.swift; sourceTree = ""; }; @@ -1550,6 +1555,7 @@ DCFC9ED028002335005D9144 /* CollectionViewCellProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewCellProvider.swift; sourceTree = ""; }; DCFC9ED2280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionViewCellProvider+StandardImplementations.swift"; sourceTree = ""; }; DCFC9ED428002F33005D9144 /* CollectionViewCellConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewCellConfiguration.swift; sourceTree = ""; }; + DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientWebAppViewController.swift; sourceTree = ""; }; DCFED971208095E200A2D984 /* ClientItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientItemCell.swift; sourceTree = ""; }; DCFED9B920809B8900A2D984 /* ThemeTVGResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTVGResource.swift; sourceTree = ""; }; DCFEF90526EFA45A001DC7A4 /* VendorServices+App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "VendorServices+App.swift"; sourceTree = ""; }; @@ -2231,6 +2237,9 @@ 0233F45D246E9D960095A799 /* UploadCameraMediaAction.swift */, 025F063224AA163C009D8FC5 /* DisplayExifMetadataAction.swift */, 39EF06AF25D6C3FC001E1E19 /* PresentationModeAction.swift */, + DC0CE19128C7DBE3009ABDFB /* OpenInWebAppAction.swift */, + DCFE682628D869A400091D2A /* ClientWebAppViewController.swift */, + DC0CE19C28C89CD9009ABDFB /* CreateDocumentAction.swift */, ); path = "Actions+Extensions"; sourceTree = ""; @@ -4224,6 +4233,7 @@ 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */, 394E1FFF233E43F5009D2897 /* LinksAction.swift in Sources */, 392DDB1424CF024D009E5406 /* ImportFilesController.swift in Sources */, + DC0CE19228C7DBE3009ABDFB /* OpenInWebAppAction.swift in Sources */, 396C82FB2319AFDD00938262 /* CollaborateAction.swift in Sources */, 0233F45E246E9D960095A799 /* UploadCameraMediaAction.swift in Sources */, DC854936218331CF00782BA8 /* UserInterfaceSettingsSection.swift in Sources */, @@ -4285,12 +4295,14 @@ 6E5FC172221590B000F60846 /* DisplayHostViewController.swift in Sources */, 4C51727F22DE04BD001BC97F /* ScheduledTaskManager.swift in Sources */, 39BC9C3023DB831F0097C52D /* DocumentEditingAction.swift in Sources */, + DCFE682728D869A400091D2A /* ClientWebAppViewController.swift in Sources */, DCB6C4D72453A6CA00C1EAE1 /* ClientAuthenticationUpdater.swift in Sources */, 399697F5260255B100E5AEBA /* PDFGotoPageAction.swift in Sources */, 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */, DCC3701624D4D365008B0DEB /* OCScanJobActivity+DiagnosticGenerator.swift in Sources */, 39A243C424BDD9E100F4441F /* StaticLoginBundle.swift in Sources */, DC27A19D20CAB602008ACB6C /* FileProviderInterfaceManager.swift in Sources */, + DC0CE19D28C89CD9009ABDFB /* CreateDocumentAction.swift in Sources */, 4C51727D22DE04BD001BC97F /* ScheduledTaskExtension.swift in Sources */, 025F063324AA163C009D8FC5 /* DisplayExifMetadataAction.swift in Sources */, DCC085512293ED52008CC05C /* DisplaySettingsSection.swift in Sources */, diff --git a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme index 45f5993ea..958a371be 100644 --- a/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme +++ b/ownCloud.xcodeproj/xcshareddata/xcschemes/ownCloud.xcscheme @@ -360,6 +360,21 @@ value = "[com.owncloud.action.copy,com.owncloud.action.move]" isEnabled = "NO"> + + + + + + . + * + */ + +import UIKit +import WebKit +import ownCloudAppShared + +class ClientWebAppViewController: UIViewController, WKUIDelegate { + var urlRequest: URLRequest + var webView: WKWebView? + + var shouldSendCloseEvent: Bool = true + + init(with urlRequest: URLRequest) { + self.urlRequest = urlRequest + + super.init(nibName: nil, bundle: nil) + + self.isModalInPresentation = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var webViewConfiguration: WKWebViewConfiguration { + let configuration = WKWebViewConfiguration() + let webSiteDataStore = WKWebsiteDataStore.nonPersistent() + + configuration.websiteDataStore = webSiteDataStore + configuration.applicationNameForUserAgent = "MobileSafari" // Needed for some web apps that will present the desktop UI otherwise (f.ex. OnlyOffice as of 2022-09-19) + + return configuration + } + + override func loadView() { + let rootView = UIView() + + webView = WKWebView(frame: .zero, configuration: webViewConfiguration) + webView?.translatesAutoresizingMaskIntoConstraints = false + webView?.uiDelegate = self + + rootView.addSubview(webView!) + + NSLayoutConstraint.activate([ + webView!.leadingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.leadingAnchor), + webView!.trailingAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.trailingAnchor), + webView!.topAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.topAnchor), + webView!.bottomAnchor.constraint(equalTo: rootView.safeAreaLayoutGuide.bottomAnchor) + ]) + + view = rootView + } + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction(handler: { [weak self] _ in + if self?.shouldSendCloseEvent == true { + // Close via window.close(), which is calling dismissSecurely() once done + self?.closeWebWindow() + + // Call dismissOnce() after 10 seconds regardless + OnMainThread(after: 10) { + self?.dismissOnce() + } + } else { + // Close directly + self?.closeWebWindow() + } + })) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + webView?.load(urlRequest) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + // Drop web view + webView?.uiDelegate = nil + webView?.removeFromSuperview() + webView = nil + } + + private var isDismissed = false + func dismissOnce() { + if !isDismissed { + isDismissed = true + self.dismiss(animated: true) + } + } + + // window.close() handling + func closeWebWindow() { + webView?.evaluateJavaScript("window.close();") + } + + // UI delegate + func webViewDidClose(_ webView: WKWebView) { + dismissOnce() + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift new file mode 100644 index 000000000..4bc1094fe --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/CreateDocumentAction.swift @@ -0,0 +1,187 @@ +// +// CreateDocumentAction.swift +// ownCloud +// +// Created by Felix Schwarz on 07.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 CreateDocumentAction: Action { + override open class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.createDocument") } + override open class var category : ActionCategory? { return .normal } + override open class var name : String? { return "New document".localized } + override open class var locations : [OCExtensionLocationIdentifier]? { return [.folderAction, .keyboardShortcut, .emptyFolder] } + override open class var keyCommand : String? { return "N" } + override open class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .shift] } + + // MARK: - Extension matching + override open class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.items.count > 1 { + return .none + } + + if forContext.items.first?.type != OCItemType.collection { + return .none + } + + if forContext.items.first?.permissions.contains(.createFile) == false { + return .none + } + + if forContext.core?.appProvider?.types?.contains(where: { fileType in + return fileType.allowCreation + }) == true { + return .first + } + + return .none + } + + // MARK: - Action implementation + override open func run() { + guard context.items.count > 0 else { + completed(with: NSError(ocError: .itemNotFound)) + return + } + + let item = context.items.first + + guard item != nil, let itemLocation = item?.location else { + completed(with: NSError(ocError: .itemNotFound)) + return + } + + guard let viewController = context.viewController else { + completed(with: NSError(ocError: .internal)) + return + } + + guard let documentTypes = context.core?.appProvider?.types?.filter({ fileType in + return fileType.allowCreation + }).sorted(by: { (type1, type2) in + if let name1 = type1.name, let name2 = type2.name { + return name1.localizedCaseInsensitiveCompare(name2) == .orderedAscending + } + return false + }), documentTypes.count > 0 else { + completed() + return + } + + OnMainThread { + let documentTypesDataSource = OCDataSourceArray() + let documentTypesSection = CollectionViewSection(identifier: "documentTypes", dataSource: documentTypesDataSource, cellStyle: .init(with: .fillSpace), cellLayout: .fullWidth(itemHeightDimension: .estimated(54), groupHeightDimension: .estimated(54), edgeSpacing: NSCollectionLayoutEdgeSpacing(leading: .fixed(0), top: .fixed(10), trailing: .fixed(0), bottom: .fixed(10)), contentInsets: NSDirectionalEdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20)), clientContext: self.context.clientContext) + let documentPickerViewController = CollectionViewController(context: self.context.clientContext, sections: [ documentTypesSection ]) + + let navigationViewController = ThemeNavigationController(rootViewController: documentPickerViewController) + navigationViewController.modalPresentationStyle = .formSheet + viewController.present(navigationViewController, animated: true) + + documentPickerViewController.title = "New document".localized + documentPickerViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .cancel, primaryAction: UIAction(handler: { [weak navigationViewController] action in + navigationViewController?.dismiss(animated: true) + self.completed() + }), menu: nil) + + let createDocument : (OCAppProviderFileType) -> Void = { (fileType) in + guard let core = self.core, let parentItem = try? core.cachedItem(at: itemLocation) else { + self.completed() + return + } + + core.suggestUnusedNameBased(on: "New document".localized.appending((fileType.extension != nil) ? ".\(fileType.extension!)" : ""), at: itemLocation, isDirectory: true, using: .numbered, filteredBy: nil, resultHandler: { (suggestedName, _) in + guard let suggestedName = suggestedName else { return } + + OnMainThread { + let documentNameViewController = NamingViewController( with: self.core, defaultName: suggestedName, stringValidator: { name in + if name.contains("/") || name.contains("\\") { + return (false, nil, "File name cannot contain / or \\".localized) + } else { + if let item = item { + if ((try? self.core?.cachedItem(inParent: item, withName: name, isDirectory: true)) != nil) || + ((try? self.core?.cachedItem(inParent: item, withName: name, isDirectory: false)) != nil) { + return (false, "Item with same name already exists".localized, "An item with the same name already exists in this location.".localized) + } + } + + return (true, nil, nil) + } + }, completion: { newFileName, _ in + guard let newFileName = newFileName, let core = self.core else { + self.completed() + return + } + + if let progress = core.connection.createAppFile(of: fileType, in: parentItem, withName: newFileName, completionHandler: { (error, fileID, item) in + if error == nil, let query = self.context.clientContext?.query { + self.core?.reload(query) + } + + self.completed(with: error) + }) { + self.publish(progress: progress) + } + }) + + documentNameViewController.navigationItem.title = "Pick a name".localized + + navigationViewController.pushViewController(documentNameViewController, animated: true) + } + }) + } + + let fallbackIcon = UIImage(systemName: "doc")?.withRenderingMode(.alwaysTemplate) + let iconSize = CGSize(width: 36, height: 36) + + let headerItem = OCDataItemPresentable(reference: "_header" as NSString, originalDataItemType: nil, version: nil) + headerItem.title = "Pick a document type to create:".localized + headerItem.childrenDataSourceProvider = nil + + var documentTypeActions : [OCDataItem & OCDataItemVersioning] = [ headerItem ] + + for documentType in documentTypes { + if let documentTypeName = documentType.name { + var docIcon : UIImage? + + if let docTypeIcon = documentType.icon { + docIcon = docTypeIcon + } else if let mimeType = documentType.mimeType, let tvgIconName = OCItem.iconName(for: mimeType) { + docIcon = Theme.shared.image(for: tvgIconName, size: iconSize) + } + + if docIcon == nil { + docIcon = fallbackIcon + } + + docIcon = docIcon?.paddedTo(width: iconSize.width, height: iconSize.height) + + let action = OCAction(title: documentTypeName, icon: docIcon, action: { action, options, completionHandler in + createDocument(documentType) + }) + + documentTypeActions.append(action) + } + } + + documentTypesDataSource.setVersionedItems(documentTypeActions) + } + } + + override open class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(systemName: "doc.badge.plus")?.withRenderingMode(.alwaysTemplate) + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInWebAppAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInWebAppAction.swift new file mode 100644 index 000000000..f6fc62faa --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInWebAppAction.swift @@ -0,0 +1,225 @@ +// +// OpenInWebAppAction.swift +// ownCloud +// +// Created by Felix Schwarz on 06.09.22. +// Copyright © 2022 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2022, 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 + +public extension OCClassSettingsKey { + static let openInWebAppMode = OCClassSettingsKey("open-in-web-app-mode") +} + +enum OpenInWebAppActionMode: String { + case defaultBrowser = "default-browser" + case inApp = "in-app" + case inAppWithDefaultBrowserOption = "in-app-with-default-browser-option" +} + +class OpenInWebAppAction: Action { + private static var _classSettingsRegistered: Bool = false + override class var actionExtension: ActionExtension { + if !_classSettingsRegistered { + _classSettingsRegistered = true + + self.registerOCClassSettingsDefaults([ + .openInWebAppMode : OpenInWebAppActionMode.inApp.rawValue + ], metadata: [ + .openInWebAppMode : [ + .type : OCClassSettingsMetadataType.string, + .label : "Open In WebApp mode", + .description : "Determines how to open a document in a web app.", + .status : OCClassSettingsKeyStatus.advanced, + .category : "Actions", + .possibleValues : [ + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.defaultBrowser.rawValue, + OCClassSettingsMetadataKey.description : "Open in default browser app. May require user to sign in." + ], + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.inApp.rawValue, + OCClassSettingsMetadataKey.description : "Open inline in an in-app browser." + ], + [ + OCClassSettingsMetadataKey.value : OpenInWebAppActionMode.inAppWithDefaultBrowserOption.rawValue, + OCClassSettingsMetadataKey.description : "Open inline in an in-app browser, but provide a button to open the document in the default browser (may require the user to sign in)." + ] + ] + ] + ]) + } + + return super.actionExtension + } + + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openinwebapp") } + override class var category : ActionCategory? { return .normal } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreDetailItem, .contextMenuItem] } + + class open func createActionExtension(for app: OCAppProviderApp, core: OCCore) -> ActionExtension { + let objectProvider : OCExtensionObjectProvider = { (_ rawExtension, _ context, _ error) -> Any? in + if let actionExtension = rawExtension as? ActionExtension, + let actionContext = context as? ActionContext { + let action = self.init(for: actionExtension, with: actionContext) + + action.app = app + + return action + } + + return nil + } + + let appName = app.name ?? UUID().uuidString + let extensionIdentifier = OCExtensionIdentifier("\(identifier!.rawValue).\(appName.lowercased())") + let coreRunIdentifier = core.runIdentifier + + let standardMatcher = actionCustomContextMatcher + let customMatcher : OCExtensionCustomContextMatcher = { (context, priority) -> OCExtensionPriority in + // Apply standard matching + let standardPriority = standardMatcher(context, priority) + + guard standardPriority != .noMatch, let actionContext = context as? ActionContext else { + return .noMatch + } + + // Limit to specific core + guard let core = actionContext.core, core.runIdentifier == coreRunIdentifier else { + return .noMatch + } + + // Apply app matching + if self.applicablePosition(forContext: actionContext, app: app) == .none { + return .noMatch + } + + return standardPriority + } + + return ActionExtension(name: "Open in {{appName}} (web)".localized(["appName" : appName]), category: category!, identifier: extensionIdentifier, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher, keyCommand: nil, keyModifierFlags: nil) + } + + class open func applicablePosition(forContext: ActionContext, app: OCAppProviderApp) -> ActionPosition { + // OpenInWebApp only supports a single item + guard let item = forContext.items.first, forContext.items.count == 1 else { + return .none + } + + // Exclude directories + if item.type == .collection { + return .none + } + + // Ensure item is supported by the web app + if !app.supportsItem(item) { + return .none + } + + return .nearFirst + } + + var app : OCAppProviderApp? + + // MARK: - Action implementation + override func run() { + var openMode : OpenInWebAppActionMode = .inApp + + if let openInWebAppMode = classSetting(forOCClassSettingsKey: .openInWebAppMode) as? String, let configuredOpenMode = OpenInWebAppActionMode(rawValue: openInWebAppMode) { + openMode = configuredOpenMode + } + + switch openMode { + case .defaultBrowser: + openInExternalBrowser() + + case .inApp: + openInInAppBrowser(withDefaultBrowserOption: false) + + case .inAppWithDefaultBrowserOption: + openInInAppBrowser(withDefaultBrowserOption: true) + } + } + + func openInInAppBrowser(withDefaultBrowserOption defaultBrowserOption: Bool) { + guard context.items.count == 1, let item = context.items.first, let core = context.core else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + // Open in in-app browser + core.connection.open(inApp: item, with: app, viewMode: nil, completionHandler: { (error, url, method, headers, parameters, urlRequest) in + if let urlRequest = urlRequest as? URLRequest { + OnMainThread { + let webAppViewController = ClientWebAppViewController(with: urlRequest) + webAppViewController.navigationItem.title = item.name + + if defaultBrowserOption { + webAppViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: nil, image: UIImage(systemName: "safari"), primaryAction: UIAction(handler: { [weak webAppViewController] _ in + webAppViewController?.parent?.dismiss(animated: true, completion: { + self.openInExternalBrowser() + }) + }), menu: nil) + } + + let navigationController = ThemeNavigationController(rootViewController: webAppViewController) + + self.context.viewController?.present(navigationController, animated: true) + } + } + + self.completed(with: error) + }) + } + + func openInExternalBrowser() { + guard context.items.count == 1, let item = context.items.first, let core = context.core else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + // Open in external browser + core.connection.open(inWeb: item, with: app) { (error, url) in + if let url = url { + OnMainThread { + UIApplication.shared.open(url) + } + } + + self.completed(with: error) + } + } + + override var position: ActionPosition { + if let app = app { + return type(of: self).applicablePosition(forContext: context, app: app) + } + + return .none + } + + override var icon: UIImage? { + if let remoteIcon = (app?.iconResourceRequest?.resource as? OCResourceImage)?.image?.image { + return remoteIcon + } + + return super.icon + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(systemName: "globe")?.withRenderingMode(.alwaysTemplate) + } +} diff --git a/ownCloud/Client/ClientRootViewController+ItemActions.swift b/ownCloud/Client/ClientRootViewController+ItemActions.swift index 07b6910be..96fde2e40 100644 --- a/ownCloud/Client/ClientRootViewController+ItemActions.swift +++ b/ownCloud/Client/ClientRootViewController+ItemActions.swift @@ -40,7 +40,7 @@ extension ClientRootViewController : MoreItemAction { } let originatingViewController : UIViewController = context.originatingViewController ?? self let actionsLocation = OCExtensionLocation(ofType: .action, identifier: locationIdentifier) - let actionContext = ActionContext(viewController: originatingViewController, core: core, query: context.query, items: [item], location: actionsLocation, sender: sender) + let actionContext = ActionContext(viewController: originatingViewController, clientContext: context, core: core, query: context.query, items: [item], location: actionsLocation, sender: sender) if let moreViewController = Action.cardViewController(for: item, with: actionContext, progressHandler: makeActionProgressHandler(), completionHandler: nil) { originatingViewController.present(asCard: moreViewController, animated: true) diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index f484e1fbb..dac3d23cc 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -74,6 +74,8 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa var alertQueue : OCAsyncSequentialQueue = OCAsyncSequentialQueue() + var appProviderObservation: NSKeyValueObservation? + init(bookmark inBookmark: OCBookmark) { bookmark = inBookmark @@ -182,6 +184,8 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa core?.messageQueue.remove(presenter: cardMessagePresenter) } + appProviderActionExtensions = nil + if self.coreRequested { self.fpServiceStandby?.stop() OCCoreManager.shared.returnCore(for: bookmark, completionHandler: nil) @@ -211,6 +215,11 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa core?.messageQueue.add(presenter: cardMessagePresenter) } + // Observe .appProvider property + self.appProviderObservation = core?.observe(\OCCore.appProvider, options: .initial, changeHandler: { [weak self] (core, change) in + self?.appProviderChanged(to: core.appProvider) + }) + // Remove skip available offline when user opens the bookmark core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) }, completionHandler: { (core, error) in @@ -528,6 +537,62 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa return nil } + + var appProviderActionExtensions : [OCExtension]? { + willSet { + if let extensions = appProviderActionExtensions { + for ext in extensions { + OCExtensionManager.shared.removeExtension(ext) + } + } + } + + didSet { + if let extensions = appProviderActionExtensions { + for ext in extensions { + OCExtensionManager.shared.addExtension(ext) + } + } + } + } + + func appProviderChanged(to appProvider: OCAppProvider?) { + var actionExtensions : [OCExtension] = [] + + if let core = core { + if let apps = core.appProvider?.apps { + for app in apps { + // Pre-load app icon + if let appIconRequest = app.iconResourceRequest { + core.vault.resourceManager?.start(appIconRequest) + } + + // Create app-specific open-in-web-app action + let openInWebAction = OpenInWebAppAction.createActionExtension(for: app, core: core) + actionExtensions.append(openInWebAction) + } + } + + if let types = core.appProvider?.types { + let creationTypes = types.filter({ type in + return type.allowCreation + }) + + if creationTypes.count > 0 { + // Pre-load document icons + for type in creationTypes { + if let typeIconRequest = type.iconResourceRequest { + core.vault.resourceManager?.start(typeIconRequest) + } + } + + // Log.debug("Creation Types: \(String(describing: creationTypes))") + } + } + } + + appProviderActionExtensions = actionExtensions + } } extension ClientRootViewController : Themeable { diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 4e8ef97ca..d75b6b17a 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -357,9 +357,13 @@ "Folder name" = "Folder name"; "Rename" ="Rename"; "Create folder" ="Create folder"; +"New document" = "New document"; +"Pick a name" = "Pick a name"; +"Pick a document type to create:" = "Pick a document type to create:"; "Duplicate" = "Duplicate"; "Move" = "Move"; "Open in" = "Open in"; +"Open in {{appName}} (web)" = "Open in {{appName}} (web)"; "Copy" = "Copy"; "Copy here" = "Copy here"; "Cannot connect to " = "Cannot connect to "; diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift index 217f7bb01..1b8a30b38 100644 --- a/ownCloudAppShared/Client/Actions/Action.swift +++ b/ownCloudAppShared/Client/Actions/Action.swift @@ -111,6 +111,8 @@ public class ActionContext: OCExtensionContext { private var cachedParentFolders = [OCLocalID : OCItem]() private var itemStorage: [OCItem] + public var clientContext : ClientContext? + public var items: [OCItem] { get { return itemStorage @@ -152,7 +154,7 @@ public class ActionContext: OCExtensionContext { } // MARK: - Init & Deinit. - public init(viewController: UIViewController, core: OCCore, query: OCQuery? = nil, items: [OCItem], location: OCExtensionLocation, sender: AnyObject? = nil, requirements: [String : Any]? = nil, preferences: [String : Any]? = nil) { + public init(viewController: UIViewController, clientContext: ClientContext? = nil, core: OCCore, query: OCQuery? = nil, items: [OCItem], location: OCExtensionLocation, sender: AnyObject? = nil, requirements: [String : Any]? = nil, preferences: [String : Any]? = nil) { itemStorage = items @@ -163,6 +165,8 @@ public class ActionContext: OCExtensionContext { self.core = core self.location = location + self.clientContext = clientContext + self.query = query self.requirements = requirements self.preferences = preferences @@ -273,18 +277,8 @@ open class Action : NSObject { class open var features : [String : Any]? { return nil } // MARK: - Extension creation - class open var actionExtension : ActionExtension { - let objectProvider : OCExtensionObjectProvider = { (_ rawExtension, _ context, _ error) -> Any? in - if let actionExtension = rawExtension as? ActionExtension, - let actionContext = context as? ActionContext { - return self.init(for: actionExtension, with: actionContext) - } - - return nil - } - - let customMatcher : OCExtensionCustomContextMatcher = { (context, priority) -> OCExtensionPriority in - + class public var actionCustomContextMatcher : OCExtensionCustomContextMatcher { + return { (context, priority) -> OCExtensionPriority in // Make sure we have valid context and extension was not filtered out due to location mismatch guard let actionContext = context as? ActionContext, priority != .noMatch else { return priority @@ -314,8 +308,19 @@ open class Action : NSObject { // Additional filtering (f.ex. via OCClassSettings, Settings) goes here } + } + + class open var actionExtension : ActionExtension { + let objectProvider : OCExtensionObjectProvider = { (_ rawExtension, _ context, _ error) -> Any? in + if let actionExtension = rawExtension as? ActionExtension, + let actionContext = context as? ActionContext { + return self.init(for: actionExtension, with: actionContext) + } + + return nil + } - return ActionExtension(name: name!, category: category!, identifier: identifier!, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher, keyCommand: keyCommand, keyModifierFlags: keyModifierFlags) + return ActionExtension(name: name!, category: category!, identifier: identifier!, locations: locations, features: features, objectProvider: objectProvider, customMatcher: actionCustomContextMatcher, keyCommand: keyCommand, keyModifierFlags: keyModifierFlags) } // MARK: - Extension matching @@ -519,11 +524,11 @@ open class Action : NSObject { } -extension OCClassSettingsIdentifier { +public extension OCClassSettingsIdentifier { static let action = OCClassSettingsIdentifier("action") } -extension OCClassSettingsKey { +public extension OCClassSettingsKey { static let allowedActions = OCClassSettingsKey("allowed") static let disallowedActions = OCClassSettingsKey("disallowed") } diff --git a/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift b/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift index f23c8bd88..0d54bbe76 100644 --- a/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift +++ b/ownCloudAppShared/Client/Collection Views/View Controllers/ClientItemViewController.swift @@ -313,7 +313,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, let locationIdentifier: OCExtensionLocationIdentifier = .emptyFolder let originatingViewController : UIViewController = context.originatingViewController ?? self let actionsLocation = OCExtensionLocation(ofType: .action, identifier: locationIdentifier) - let actionContext = ActionContext(viewController: originatingViewController, core: core, query: context.query, items: [item], location: actionsLocation, sender: self) + let actionContext = ActionContext(viewController: originatingViewController, clientContext: clientContext, core: core, query: context.query, items: [item], location: actionsLocation, sender: self) let emptyFolderActions = Action.sortedApplicableActions(for: actionContext) let actions = emptyFolderActions.map({ action in action.provideOCAction() }) @@ -465,8 +465,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // Setup new action context if let core = clientContext?.core { let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .multiSelection) - - multiSelectionActionContext = ActionContext(viewController: self, core: core, query: query, items: [OCItem](), location: actionsLocation) + multiSelectionActionContext = ActionContext(viewController: self, clientContext: clientContext, core: core, query: query, items: [OCItem](), location: actionsLocation) } // Setup multi selection action datasource @@ -619,7 +618,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, let items = provideDropItems(from: dropSession, target: view) if items.count > 0, let core = clientContext?.core { - dropTargetsActionContext = ActionContext(viewController: self, core: core, items: items, location: OCExtensionLocation(ofType: .action, identifier: .dropAction)) + dropTargetsActionContext = ActionContext(viewController: self, clientContext: clientContext, core: core, items: items, location: OCExtensionLocation(ofType: .action, identifier: .dropAction)) if let dropTargetsActionContext = dropTargetsActionContext { let actions = Action.sortedApplicableActions(for: dropTargetsActionContext) @@ -638,7 +637,7 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, // MARK: - Reveal public func reveal(item: OCDataItem, context: ClientContext, sender: AnyObject?) -> Bool { if let revealInteraction = item as? DataItemSelectionInteraction { - if revealInteraction.revealItem?(from: self, with: clientContext, animated: true, pushViewController: true, completion: nil) != nil { + if revealInteraction.revealItem?(from: self, with: context, animated: true, pushViewController: true, completion: nil) != nil { return true } } diff --git a/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift b/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift index efb9646d8..31cfe6226 100644 --- a/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift +++ b/ownCloudAppShared/Client/Data Item Interactions/OCItem+Interactions.swift @@ -99,7 +99,7 @@ extension OCItem : DataItemSwipeInteraction { } let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .tableRow) - let actionContext = ActionContext(viewController: originatingViewController, core: core, items: [self], location: actionsLocation, sender: nil) + let actionContext = ActionContext(viewController: originatingViewController, clientContext: context, core: core, items: [self], location: actionsLocation, sender: nil) let actions = Action.sortedApplicableActions(for: actionContext) let contextualActions = actions.compactMap({ action in @@ -118,7 +118,7 @@ extension OCItem : DataItemContextMenuInteraction { } let item = self let actionsLocation = OCExtensionLocation(ofType: .action, identifier: location) // .contextMenuItem) - let actionContext = ActionContext(viewController: viewController, core: core, items: [item], location: actionsLocation, sender: nil) + let actionContext = ActionContext(viewController: viewController, clientContext: context, core: core, items: [item], location: actionsLocation, sender: nil) let actions = Action.sortedApplicableActions(for: actionContext) var actionMenuActions : [UIAction] = [] for action in actions { @@ -135,7 +135,7 @@ extension OCItem : DataItemContextMenuInteraction { // Share Items let sharingActionsLocation = OCExtensionLocation(ofType: .action, identifier: .contextMenuSharingItem) - let sharingActionContext = ActionContext(viewController: viewController, core: core, items: [item], location: sharingActionsLocation, sender: nil) + let sharingActionContext = ActionContext(viewController: viewController, clientContext: context, core: core, items: [item], location: sharingActionsLocation, sender: nil) let sharingActions = Action.sortedApplicableActions(for: sharingActionContext) for action in sharingActions { action.progressHandler = context?.actionProgressHandlerProvider?.makeActionProgressHandler()