diff --git a/CHANGELOG.md b/CHANGELOG.md index e793a41e6..5149ab361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,64 @@ ownCloud admins and users. Summary ------- +* Bugfix - Japanese Input Support: [#916](https://github.com/owncloud/ios-app/issues/916) +* Bugfix - Swiping PDF thumbnail view on the iPhone: [#918](https://github.com/owncloud/ios-app/issues/918) +* Bugfix - Passcode Settings section not refreshed: [#923](https://github.com/owncloud/ios-app/issues/923) +* Change - French Localization: [#4450](https://github.com/owncloud/enterprise/issues/4450) +* Change - Clipboard Support: [#514](https://github.com/owncloud/ios-app/pull/514) +* Change - Presentation Mode: [#704](https://github.com/owncloud/ios-app/issues/704) * Change - Six Digits Passcode: [#958](https://github.com/owncloud/ios-app/pull/958) * Change - Filename Layout: [#968](https://github.com/owncloud/ios-app/issues/968) Details ------- +* Bugfix - Japanese Input Support: [#916](https://github.com/owncloud/ios-app/issues/916) + + Fixed a problem in scan view when renaming the file name and using a Japanese keyboard layout + (2-Byte character). After entering a character inside the file name the text cursor jumped to + the end. + + https://github.com/owncloud/ios-app/issues/916 + +* Bugfix - Swiping PDF thumbnail view on the iPhone: [#918](https://github.com/owncloud/ios-app/issues/918) + + Prevent page container scrolling, when try to scroll inside the pdf thumbnail view on the + iPhone + + https://github.com/owncloud/ios-app/issues/918 + +* Bugfix - Passcode Settings section not refreshed: [#923](https://github.com/owncloud/ios-app/issues/923) + + If a passcode was enabled or disabled in the settings, the UI section was not updated. + + https://github.com/owncloud/ios-app/issues/923 + +* Change - French Localization: [#4450](https://github.com/owncloud/enterprise/issues/4450) + + Added french localization. + + https://github.com/owncloud/enterprise/issues/4450 + +* Change - Clipboard Support: [#514](https://github.com/owncloud/ios-app/pull/514) + + Clipboard support provides the following new features: - Copy: Files can be copied to the + system-wide clipboard and pasted into other apps. Folders can also be copied within the + ownCloud app. - Paste: Files can be pasted from the system-wide clipboard into the ownCloud + app. Likewise, files and folders copied within the app can be pasted. - Cut: Within an ownCloud + account, files and folders can be cut and pasted to a different path. After this action, the + items are no longer present in the original location. + + https://github.com/owncloud/ios-app/pull/514 + +* Change - Presentation Mode: [#704](https://github.com/owncloud/ios-app/issues/704) + + Added an action in detail view menu which enables presentation mode. Presentation mode + prevents the display from sleep mode as long as the detail view is closed. Furthermore the + navigation bar will be hidden. + + https://github.com/owncloud/ios-app/issues/704 + * Change - Six Digits Passcode: [#958](https://github.com/owncloud/ios-app/pull/958) Passcode lock supports to set a passcode lock with 4 or 6 digits. diff --git a/changelog/unreleased/4450 b/changelog/unreleased/4450 new file mode 100644 index 000000000..c991c2c10 --- /dev/null +++ b/changelog/unreleased/4450 @@ -0,0 +1,5 @@ +Change: French Localization + +Added french localization. + +https://github.com/owncloud/enterprise/issues/4450 \ No newline at end of file diff --git a/changelog/unreleased/514 b/changelog/unreleased/514 new file mode 100644 index 000000000..35776a1d6 --- /dev/null +++ b/changelog/unreleased/514 @@ -0,0 +1,8 @@ +Change: Clipboard Support + +Clipboard support provides the following new features: +- Copy: Files can be copied to the system-wide clipboard and pasted into other apps. Folders can also be copied within the ownCloud app. +- Paste: Files can be pasted from the system-wide clipboard into the ownCloud app. Likewise, files and folders copied within the app can be pasted. +- Cut: Within an ownCloud account, files and folders can be cut and pasted to a different path. After this action, the items are no longer present in the original location. + +https://github.com/owncloud/ios-app/pull/514 \ No newline at end of file diff --git a/changelog/unreleased/704 b/changelog/unreleased/704 new file mode 100644 index 000000000..dca98b9d1 --- /dev/null +++ b/changelog/unreleased/704 @@ -0,0 +1,6 @@ +Change: Presentation Mode + +Added an action in detail view menu which enables presentation mode. +Presentation mode prevents the display from sleep mode as long as the detail view is closed. Furthermore the navigation bar will be hidden. + +https://github.com/owncloud/ios-app/issues/704 \ No newline at end of file diff --git a/changelog/unreleased/916 b/changelog/unreleased/916 new file mode 100644 index 000000000..57cb09b5c --- /dev/null +++ b/changelog/unreleased/916 @@ -0,0 +1,6 @@ +Bugfix: Japanese Input Support + +Fixed a problem in scan view when renaming the file name and using a Japanese keyboard layout (2-Byte character). +After entering a character inside the file name the text cursor jumped to the end. + +https://github.com/owncloud/ios-app/issues/916 \ No newline at end of file diff --git a/changelog/unreleased/918 b/changelog/unreleased/918 new file mode 100644 index 000000000..01249eb42 --- /dev/null +++ b/changelog/unreleased/918 @@ -0,0 +1,5 @@ +Bugfix: Swiping PDF thumbnail view on the iPhone + +Prevent page container scrolling, when try to scroll inside the pdf thumbnail view on the iPhone + +https://github.com/owncloud/ios-app/issues/918 \ No newline at end of file diff --git a/changelog/unreleased/923 b/changelog/unreleased/923 new file mode 100644 index 000000000..3798442cf --- /dev/null +++ b/changelog/unreleased/923 @@ -0,0 +1,5 @@ +Bugfix: Passcode Settings section not refreshed + +If a passcode was enabled or disabled in the settings, the UI section was not updated. + +https://github.com/owncloud/ios-app/issues/923 \ No newline at end of file diff --git a/doc/CONFIGURATION.json b/doc/CONFIGURATION.json index 99009bd4d..d54c6f332 100644 --- a/doc/CONFIGURATION.json +++ b/doc/CONFIGURATION.json @@ -25,6 +25,10 @@ "description" : "Create folder", "value" : "com.owncloud.action.createFolder" }, + { + "description" : "Cut", + "value" : "com.owncloud.action.cutpasteboard" + }, { "description" : "Delete", "value" : "com.owncloud.action.delete" @@ -37,6 +41,10 @@ "description" : "Favorite item", "value" : "com.owncloud.action.favorite" }, + { + "description" : "Paste", + "value" : "com.owncloud.action.importpasteboard" + }, { "description" : "Links", "value" : "com.owncloud.action.links" @@ -131,6 +139,10 @@ "description" : "Create folder", "value" : "com.owncloud.action.createFolder" }, + { + "description" : "Cut", + "value" : "com.owncloud.action.cutpasteboard" + }, { "description" : "Delete", "value" : "com.owncloud.action.delete" @@ -143,6 +155,10 @@ "description" : "Favorite item", "value" : "com.owncloud.action.favorite" }, + { + "description" : "Paste", + "value" : "com.owncloud.action.importpasteboard" + }, { "description" : "Links", "value" : "com.owncloud.action.links" diff --git a/docs/modules/ROOT/pages/ios_mdm_tables.adoc b/docs/modules/ROOT/pages/ios_mdm_tables.adoc index b367c58b5..7a379bd25 100644 --- a/docs/modules/ROOT/pages/ios_mdm_tables.adoc +++ b/docs/modules/ROOT/pages/ios_mdm_tables.adoc @@ -26,6 +26,9 @@ tag::actions[] ! `com.owncloud.action.createFolder` ! Create folder +! `com.owncloud.action.cutpasteboard` +! Cut + ! `com.owncloud.action.delete` ! Delete @@ -35,6 +38,9 @@ tag::actions[] ! `com.owncloud.action.favorite` ! Favorite item +! `com.owncloud.action.importpasteboard` +! Paste + ! `com.owncloud.action.links` ! Links @@ -104,6 +110,9 @@ tag::actions[] ! `com.owncloud.action.createFolder` ! Create folder +! `com.owncloud.action.cutpasteboard` +! Cut + ! `com.owncloud.action.delete` ! Delete @@ -113,6 +122,9 @@ tag::actions[] ! `com.owncloud.action.favorite` ! Favorite item +! `com.owncloud.action.importpasteboard` +! Paste + ! `com.owncloud.action.links` ! Links diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 0d181a2da..98b7931da 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -78,6 +78,8 @@ 398393BF246D63B0001A212B /* branding-login-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 398393BD246D63B0001A212B /* branding-login-logo.png */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; 399697F5260255B100E5AEBA /* PDFGotoPageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399697F1260255B100E5AEBA /* PDFGotoPageAction.swift */; }; + 399698ED260A3CEE00E5AEBA /* ImportPasteboardAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391220922344C30F0026C290 /* ImportPasteboardAction.swift */; }; + 39969904260A3CF500E5AEBA /* CutAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391220932344C30F0026C290 /* CutAction.swift */; }; 399725E1233DF39300FC3B94 /* Calendar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399725E0233DF39300FC3B94 /* Calendar+Extension.swift */; }; 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D522411EDF00B66713 /* BorderedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D422411EDF00B66713 /* BorderedLabel.swift */; }; @@ -943,6 +945,8 @@ 39057AB1233BA7AE0008E6C0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intents.strings; sourceTree = ""; }; 39104E0A223991C8002FC02F /* UIButton+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; 3912208123436EB80026C290 /* SortMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SortMethod.swift; sourceTree = ""; }; + 391220922344C30F0026C290 /* ImportPasteboardAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportPasteboardAction.swift; sourceTree = ""; }; + 391220932344C30F0026C290 /* CutAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CutAction.swift; sourceTree = ""; }; 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListTableViewController.swift; sourceTree = ""; }; 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryTableViewController.swift; sourceTree = ""; }; 39265BB123D9987500B0C4CA /* MediaEditingAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaEditingAction.swift; sourceTree = ""; }; @@ -1148,7 +1152,7 @@ 59AAD95921F76B6800D15F07 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; 59AAD95A21F76B6800D15F07 /* cs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 59AAD97621F784CA00D15F07 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; - 59AAD97721F784CB00D15F07 /* de */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; lineEnding = 0; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 59AAD97721F784CB00D15F07 /* de */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 59AAD97A21F7854C00D15F07 /* en-GB */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-GB"; path = "en-GB.lproj/InfoPlist.strings"; sourceTree = ""; }; 59AAD97B21F7854C00D15F07 /* en-GB */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; lineEnding = 0; name = "en-GB"; path = "en-GB.lproj/Localizable.strings"; sourceTree = ""; }; 59AAD97C21F7858300D15F07 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2111,7 +2115,9 @@ 6E586CF52199A70100F680C4 /* Actions+Extensions */ = { isa = PBXGroup; children = ( + 391220932344C30F0026C290 /* CutAction.swift */, 399697F1260255B100E5AEBA /* PDFGotoPageAction.swift */, + 391220922344C30F0026C290 /* ImportPasteboardAction.swift */, 39265BB123D9987500B0C4CA /* MediaEditingAction.swift */, 39CD755123D787E400193950 /* DocumentEditingAction.swift */, 397754F22327A33500119FCB /* OpenSceneAction.swift */, @@ -3970,6 +3976,7 @@ DCD1300A23A191C000255779 /* LicenseOfferButton.swift in Sources */, 0269F589244DED02002E9D99 /* UIAlertController+UniversalLinks.swift in Sources */, 4C9BFA2323158C3F0059CA3E /* PreviewViewController.swift in Sources */, + 399698ED260A3CEE00E5AEBA /* ImportPasteboardAction.swift in Sources */, 4C3E17DB234DBF9A000D7BA8 /* PendingMediaUploadTaskExtension.swift in Sources */, DCC832DE242C0C3700153F8C /* DisplaySleepPreventer.swift in Sources */, 6E586CFC2199A72600F680C4 /* OpenInAction.swift in Sources */, @@ -4062,6 +4069,7 @@ DCE4E44D24C1D48B0051722F /* QueryFileListTableViewController+Multiselect.swift in Sources */, DCC8536023CE1AF8007BA3EB /* PurchasesSettingsSection.swift in Sources */, DCD2D40622F06ECA0071FB8F /* DataSettingsSection.swift in Sources */, + 39969904260A3CF500E5AEBA /* CutAction.swift in Sources */, 4C464BF22187AF1500D30602 /* PDFSearchViewController.swift in Sources */, DC29F09522976B9300F77349 /* LibrarySharesTableViewController.swift in Sources */, 399EA70725E654B400B6FF11 /* PendingSharesTableViewController.swift in Sources */, diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 8df8b3cde..4d401c587 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -134,6 +134,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(BackgroundFetchUpdateTaskAction.taskExtension) OCExtensionManager.shared.addExtension(InstantMediaUploadTaskExtension.taskExtension) OCExtensionManager.shared.addExtension(PendingMediaUploadTaskExtension.taskExtension) + OCExtensionManager.shared.addExtension(ImportPasteboardAction.actionExtension) + OCExtensionManager.shared.addExtension(CutAction.actionExtension) // Theming Theme.shared.activeCollection = ThemeCollection(with: ThemeStyle.preferredStyle) diff --git a/ownCloud/Client/Actions/Action.swift b/ownCloud/Client/Actions/Action.swift new file mode 100644 index 000000000..4f90c1f9e --- /dev/null +++ b/ownCloud/Client/Actions/Action.swift @@ -0,0 +1,479 @@ +// +// Action.swift +// ownCloud +// +// Created by Pablo Carrascal on 30/10/2018. +// Copyright © 2018 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit +import ownCloudSDK + +enum ActionCategory { + case normal + case destructive + case informal + case edit + case save +} + +enum ActionPosition : Int { + case none = -1 + + case first = 100 + case beforeMiddle = 200 + case middle = 300 + case afterMiddle = 400 + case last = 500 + + static func between(_ position1: ActionPosition, and position2: ActionPosition) -> ActionPosition { + return ActionPosition(rawValue: ((position1.rawValue + position2.rawValue)/2))! + } + + func shift(by offset: Int) -> ActionPosition { + return ActionPosition(rawValue: self.rawValue + offset)! + } +} + +typealias ActionCompletionHandler = ((Action, Error?) -> Void) +typealias ActionProgressHandler = ((Progress, Bool) -> Void) +typealias ActionWillRunHandler = ((@escaping () -> Void) -> Void) + +extension OCExtensionType { + static let action: OCExtensionType = OCExtensionType("app.action") +} + +extension OCExtensionLocationIdentifier { + static let tableRow: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("tableRow") //!< Present as table row action + static let moreItem: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreItem") //!< Present in "more" card view for a single item + static let moreFolder: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("moreFolder") //!< Present in "more" options for a whole folder + static let toolbar: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("toolbar") //!< Present in a toolbar + static let folderAction: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("folderAction") //!< Present in the alert sheet when the folder action bar button is pressed + static let keyboardShortcut: OCExtensionLocationIdentifier = OCExtensionLocationIdentifier("keyboardShortcut") //!< Currently used for UIKeyCommand +} + +class ActionExtension: OCExtension { + // MARK: - Custom Instance Properties. + var name: String + var category: ActionCategory + var keyCommand: String? + var keyModifierFlags: UIKeyModifierFlags? + + // MARK: - Init & Deinit + init(name: String, category: ActionCategory = .normal, identifier: OCExtensionIdentifier, locations: [OCExtensionLocationIdentifier]?, features: [String : Any]?, objectProvider: OCExtensionObjectProvider?, customMatcher: OCExtensionCustomContextMatcher?, keyCommand: String?, keyModifierFlags: UIKeyModifierFlags?) { + + self.name = name + self.category = category + self.keyCommand = keyCommand + self.keyModifierFlags = keyModifierFlags + + super.init(identifier: identifier, type: .action, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher) + } +} + +class ActionContext: OCExtensionContext { + // MARK: - Custom Instance Properties. + weak var viewController: UIViewController? + weak var core: OCCore? + weak var query: OCQuery? + var items: [OCItem] + + // MARK: - Init & Deinit. + init(viewController: UIViewController, core: OCCore, query: OCQuery? = nil, items: [OCItem], location: OCExtensionLocation, requirements: [String : Any]? = nil, preferences: [String : Any]? = nil) { + self.items = items + + super.init() + + self.viewController = viewController + self.core = core + self.location = location + + self.query = query + self.requirements = requirements + self.preferences = preferences + } +} + +class Action : NSObject { + // MARK: - Extension metadata + class var identifier : OCExtensionIdentifier? { return nil } + class var category : ActionCategory? { return .normal } + class var name : String? { return nil } + class var keyCommand : String? { return nil } + class var keyModifierFlags : UIKeyModifierFlags? { return nil } + class var locations : [OCExtensionLocationIdentifier]? { return nil } + class var features : [String : Any]? { return nil } + + // MARK: - Extension creation + class 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 + + guard let actionContext = context as? ActionContext else { + return priority + } + + if self.applicablePosition(forContext: actionContext) == .none { + // Exclude actions whose applicablePosition returns .none + return .noMatch + } + + return priority + + // Additional filtering (f.ex. via OCClassSettings, Settings) goes here + } + + return ActionExtension(name: name!, category: category!, identifier: identifier!, locations: locations, features: features, objectProvider: objectProvider, customMatcher: customMatcher, keyCommand: keyCommand, keyModifierFlags: keyModifierFlags) + } + + // MARK: - Extension matching + class func applicablePosition(forContext: ActionContext) -> ActionPosition { + return .middle + } + + // MARK: - Finding actions + class func sortedApplicableActions(for context: ActionContext) -> [Action] { + var sortedActions : [Action] = [] + + if let matches = try? OCExtensionManager.shared.provideExtensions(for: context) { + for match in matches { + if let action = match.extension.provideObject(for: context) as? Action { + sortedActions.append(action) + } + } + } + + sortedActions.sort { (action1, action2) -> Bool in + return action1.position.rawValue < action2.position.rawValue + } + + return sortedActions + } + + // MARK: - Provide Card view controller + + class func cardViewController(for item: OCItem, with context: ActionContext, progressHandler: ActionProgressHandler? = nil, completionHandler: ((Action, Error?) -> Void)? = nil) -> UIViewController? { + guard let core = context.core else { return nil } + + let tableViewController = MoreStaticTableViewController(style: .grouped) + let header = MoreViewHeader(for: item, with: core) + let moreViewController = MoreViewController(item: item, core: core, header: header, viewController: tableViewController) + + if core.connectionStatus == .online { + if core.connection.capabilities?.sharingAPIEnabled == 1 { + if item.isSharedWithUser || item.isShared { + let progressView = UIActivityIndicatorView(style: Theme.shared.activeCollection.activityIndicatorViewStyle) + progressView.startAnimating() + + let row = StaticTableViewRow(rowWithAction: nil, title: "Searching Shares…".localized, alignment: .left, accessoryView: progressView, identifier: "share-searching") + let placeholderRow = StaticTableViewRow(rowWithAction: nil, title: "", alignment: .left, identifier: "share-empty-searching") + self.updateSharingSection(sectionIdentifier: "share-section", rows: [placeholderRow, row], tableViewController: tableViewController, contentViewController: moreViewController) + + core.unifiedShares(for: item, completionHandler: { (shares) in + OnMainThread { + let shareRows = self.shareRows(shares: shares, item: item, presentingController: moreViewController, context: context) + self.updateSharingSection(sectionIdentifier: "share-section", rows: shareRows, tableViewController: tableViewController, contentViewController: moreViewController) + } + }) + } else { + var shareRows : [StaticTableViewRow] = [] + if item.isShareable { + shareRows.append(self.shareAsGroupRow(item: item, presentingController: moreViewController, context: context)) + } + if let publicLinkRow = self.shareAsPublicLinkRow(item: item, presentingController: moreViewController, context: context) { + shareRows.append(publicLinkRow) + } + if shareRows.count > 0 { + tableViewController.insertSection(StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "share-section", rows: shareRows), at: 0, animated: false) + } + } + } else { + if let publicLinkRow = self.shareAsPublicLinkRow(item: item, presentingController: moreViewController, context: context) { + tableViewController.insertSection(StaticTableViewSection(headerTitle: nil, footerTitle: nil, identifier: "share-section", rows: [publicLinkRow]), at: 0, animated: false) + } + } + } + + let title = NSAttributedString(string: "Actions".localized, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20, weight: .heavy)]) + + let actions = Action.sortedApplicableActions(for: context) + + actions.forEach({ + $0.actionWillRunHandler = { [weak moreViewController] (_ donePreparing: @escaping () -> Void) in + moreViewController?.dismiss(animated: true, completion: donePreparing) + } + + $0.progressHandler = progressHandler + + $0.completionHandler = completionHandler + }) + + let actionsRows: [StaticTableViewRow] = actions.compactMap({return $0.provideStaticRow()}) + + tableViewController.addSection(MoreStaticTableViewSection(headerAttributedTitle: title, identifier: "actions-section", rows: actionsRows)) + + return moreViewController + } + + // MARK: - Action metadata + var context : ActionContext + var actionExtension: ActionExtension + weak var core : OCCore? + + // MARK: - Action creation + required init(for actionExtension: ActionExtension, with context: ActionContext) { + self.actionExtension = actionExtension + self.context = context + self.core = context.core! + + super.init() + } + + // MARK: - Execution metadata + var progressHandler : ActionProgressHandler? // to be filled before calling run(), provideStaticRow(), provideContextualAction(), etc. if desired + var completionHandler : ActionCompletionHandler? // to be filled before calling run(), provideStaticRow(), provideContextualAction(), etc. if desired + var actionWillRunHandler: ActionWillRunHandler? // to be filled before calling run(), provideStaticRow(), provideContextualAction(), etc. if desired + + // MARK: - Action implementation + @objc func perform() { + self.willRun({ + OnMainThread { + self.run() + } + }) + } + + func willRun(_ donePreparing: @escaping () -> Void) { + + if Thread.isMainThread == false { + Log.warning("The Run method of the action \(Action.identifier!.rawValue) is not called inside the main thread") + } + + if actionWillRunHandler != nil { + actionWillRunHandler!(donePreparing) + } else { + donePreparing() + } + } + + @objc func run() { + completed() + } + + func completed(with error: Error? = nil) { + if let completionHandler = completionHandler { + completionHandler(self, error) + } + } + + func publish(progress: Progress) { + if let progressHandler = progressHandler { + progressHandler(progress, true) + } + } + + func unpublish(progress: Progress) { + if let progressHandler = progressHandler { + progressHandler(progress, false) + } + } + + // MARK: - Action UI elements + private static let staticRowImageWidth : CGFloat = 32 + + func provideStaticRow() -> StaticTableViewRow? { + return StaticTableViewRow(buttonWithAction: { (_ row, _ sender) in + self.perform() + }, title: actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .plain, image: self.icon, imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: actionExtension.identifier.rawValue) + } + + func provideContextualAction() -> UIContextualAction? { + return UIContextualAction(style: actionExtension.category == .destructive ? .destructive : .normal, title: self.actionExtension.name, handler: { (_ action, _ view, _ uiCompletionHandler) in + uiCompletionHandler(false) + self.perform() + }) + } + + func provideAlertAction() -> UIAlertAction? { + let alertAction = UIAlertAction(title: self.actionExtension.name, style: actionExtension.category == .destructive ? .destructive : .default, handler: { (_ alertAction) in + self.perform() + }) + + if let image = self.icon?.paddedTo(width: 36, height: nil) { + if alertAction.responds(to: NSSelectorFromString("setImage:")) { + alertAction.setValue(image, forKey: "image") + } + if alertAction.responds(to: NSSelectorFromString("_setTitleTextAlignment:")) { + alertAction.setValue(CATextLayerAlignmentMode.left, forKey: "titleTextAlignment") + } + } + + return alertAction + } + + // MARK: - Action metadata + class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return nil + } + + var icon : UIImage? { + if let locationIdentifier = context.location?.identifier { + return type(of: self).iconForLocation(locationIdentifier) + } + + return nil + } + + var position : ActionPosition { + return type(of: self).applicablePosition(forContext: context) + } + +} + +// MARK: - Sharing + +private extension Action { + + class func shareRows(shares: [OCShare], item: OCItem, presentingController: UIViewController, context: ActionContext) -> [StaticTableViewRow] { + var shareRows: [StaticTableViewRow] = [] + + var userTitle = "" + var linkTitle = "" + var hasUserGroupSharing = false + var hasLinkSharing = false + + if item.isSharedWithUser { + // find shares by others + if let itemOwner = item.owner, itemOwner.isRemote, let ownerName = itemOwner.displayName ?? itemOwner.userName { + // - remote shares + userTitle = String(format: "Shared by %@".localized, ownerName) + hasUserGroupSharing = true + } else { + // - local shares + for share in shares { + if let ownerName = share.itemOwner?.displayName { + userTitle = String(format: "Shared by %@".localized, ownerName) + hasUserGroupSharing = true + break + } + } + } + } else { + // find Shares by me + let privateShares = shares.filter { (share) -> Bool in + return share.type != .link + } + + if privateShares.count > 0 { + let title = ((privateShares.count > 1) ? "Recipients" : "Recipient").localized + + userTitle = "\(privateShares.count) \(title)" + hasUserGroupSharing = true + } + } + + // find Public link shares + let linkShares = shares.filter { (share) -> Bool in + return share.type == .link + } + if linkShares.count > 0 { + let title = ((linkShares.count > 1) ? "Links" : "Link").localized + + linkTitle.append("\(linkShares.count) \(title)") + hasLinkSharing = true + } + + if hasUserGroupSharing { + let addGroupRow = StaticTableViewRow(rowWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController, let core = context.core { + let sharingViewController = GroupSharingTableViewController(core: core, item: item) + sharingViewController.shares = shares + + self.dismiss(presentingController: presentingController, andPresent: sharingViewController, on: context.viewController) + } + }, title: userTitle, subtitle: nil, image: UIImage(named: "group"), imageWidth: Action.staticRowImageWidth, alignment: .left, accessoryType: .disclosureIndicator) + shareRows.append(addGroupRow) + } else if item.isShareable { + shareRows.append(self.shareAsGroupRow(item: item, presentingController: presentingController, context: context)) + } + + if hasLinkSharing, let core = context.core, core.connection.capabilities?.publicSharingEnabled == true { + let addGroupRow = StaticTableViewRow(rowWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController { + let sharingViewController = PublicLinkTableViewController(core: core, item: item) + sharingViewController.shares = shares + + self.dismiss(presentingController: presentingController, andPresent: sharingViewController, on: context.viewController) + } + }, title: linkTitle, subtitle: nil, image: UIImage(named: "link"), imageWidth: Action.staticRowImageWidth, alignment: .left, accessoryType: .disclosureIndicator) + shareRows.append(addGroupRow) + } else if let publicLinkRow = self.shareAsPublicLinkRow(item: item, presentingController: presentingController, context: context) { + shareRows.append(publicLinkRow) + } + + return shareRows + } + + private class func updateSharingSection(sectionIdentifier: String, rows: [StaticTableViewRow], tableViewController: MoreStaticTableViewController, contentViewController: MoreViewController) { + if let section = tableViewController.sectionForIdentifier(sectionIdentifier) { + tableViewController.removeSection(section) + } + if rows.count > 0 { + tableViewController.insertSection(MoreStaticTableViewSection(identifier: "share-section", rows: rows), at: 0, animated: false) + } + } + + private class func shareAsGroupRow(item : OCItem, presentingController: UIViewController, context: ActionContext) -> StaticTableViewRow { + let title = ((item.type == .collection) ? "Share this folder" : "Share this file").localized + + let addGroupRow = StaticTableViewRow(buttonWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController, let core = context.core { + self.dismiss(presentingController: presentingController, + andPresent: GroupSharingTableViewController(core: core, item: item), + on: context.viewController) + } + }, title: title, style: .plain, image: UIImage(named: "group"), imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: "share-add-group") + + return addGroupRow + } + + private class func shareAsPublicLinkRow(item : OCItem, presentingController: UIViewController, context: ActionContext) -> StaticTableViewRow? { + let addGroupRow = StaticTableViewRow(buttonWithAction: { [weak presentingController, weak context] (_, _) in + if let context = context, let presentingController = presentingController, let core = context.core { + self.dismiss(presentingController: presentingController, + andPresent: PublicLinkTableViewController(core: core, item: item), + on: context.viewController) + } + }, title: "Links".localized, style: .plain, image: UIImage(named: "link"), imageWidth: Action.staticRowImageWidth, alignment: .left, identifier: "share-add-group") + + return addGroupRow + } + + private class func dismiss(presentingController: UIViewController, andPresent viewController: UIViewController, on hostViewController: UIViewController?) { + presentingController.dismiss(animated: true) + + guard let hostViewController = hostViewController else { return } + + let navigationController = ThemeNavigationController(rootViewController: viewController) + + hostViewController.present(navigationController, animated: true, completion: nil) + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift index 880c013d2..a76725fea 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift @@ -17,16 +17,46 @@ */ import Foundation +import MobileCoreServices import ownCloudSDK import ownCloudAppShared + + +struct OCItemPasteboardValue { + var item : OCItem + var bookmarkUUID : String +} + +extension OCItemPasteboardValue { + func encode() -> Data { + let data = NSMutableData() + let archiver = NSKeyedArchiver(forWritingWith: data) + archiver.encode(item, forKey: "item") + archiver.encode(bookmarkUUID, forKey: "bookmarkUUID") + archiver.finishEncoding() + return data as Data + } + + init?(data: Data) { + let unarchiver = NSKeyedUnarchiver(forReadingWith: data) + defer { + unarchiver.finishDecoding() + } + guard let item = unarchiver.decodeObject(forKey: "item") as? OCItem else { return nil } + guard let bookmarkUUID = unarchiver.decodeObject(forKey: "bookmarkUUID") as? String else { return nil } + self.item = item + self.bookmarkUUID = bookmarkUUID + } +} + class CopyAction : Action { override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.copy") } override class var category : ActionCategory? { return .normal } override class var name : String? { return "Copy".localized } override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreDetailItem, .moreFolder, .toolbar, .keyboardShortcut, .contextMenuItem] } override class var keyCommand : String? { return "C" } - override class var keyModifierFlags: UIKeyModifierFlags? { return [.command, .alternate] } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } // MARK: - Extension matching override class func applicablePosition(forContext: ActionContext) -> ActionPosition { @@ -39,6 +69,40 @@ class CopyAction : Action { // MARK: - Action implementation override func run() { + guard context.items.count > 0, let viewController = context.viewController else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + + var presentationStyle: UIAlertController.Style = .actionSheet + if UIDevice.current.isIpad { + presentationStyle = .alert + } + + let alertController = ThemedAlertController(title: "Copy".localized, + message: nil, + preferredStyle: presentationStyle) + + alertController.addAction(UIAlertAction(title: "Choose destination directory…".localized, style: .default) { (_) in + self.showDirectoryPicker() + }) + alertController.addAction(UIAlertAction(title: "Copy to Clipboard".localized, style: .default) { (_) in + self.copyToPasteboard() + }) + alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) + + viewController.present(alertController, animated: true, completion: nil) + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem || location == .moreDetailItem || location == .moreFolder || location == .contextMenuItem { + return UIImage(named: "copy-file") + } + + return nil + } + + func showDirectoryPicker() { guard context.items.count > 0, let viewController = context.viewController, let core = self.core else { completed(with: NSError(ocError: .insufficientParameters)) return @@ -69,11 +133,110 @@ class CopyAction : Action { viewController.present(pickerNavigationController, animated: true) } - override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { - if location == .moreItem || location == .moreDetailItem || location == .moreFolder || location == .contextMenuItem { - return UIImage(named: "copy-file") + func copyToPasteboard() { + guard context.items.count > 0, let viewController = context.viewController, let core = self.core else { + completed(with: NSError(ocError: .insufficientParameters)) + return } - return nil + let items = context.items + let uuid = core.bookmark.uuid.uuidString + let globalPasteboard = UIPasteboard.general + globalPasteboard.items = [] + var itemProviderItems: [NSItemProvider] = [] + + let fileItems = context.items.filter { item in + if item.type == .file { + return true + } + + return false + } + + let hudViewController = DownloadItemsHUDViewController(core: core, downloadItems: fileItems) { [weak viewController] (error, _) in + if let error = error { + if (error as NSError).isOCError(withCode: .cancelled) { + return + } + + let appName = VendorServices.shared.appName + let alertController = ThemedAlertController(with: "Cannot connect to ".localized + appName, message: appName + " couldn't download file(s)".localized, okLabel: "OK".localized, action: nil) + + viewController?.present(alertController, animated: true) + } else { + guard let viewController = viewController else { return } + + items.forEach({ (item) in + let itemProvider = NSItemProvider() + itemProvider.suggestedName = item.name + + // Prepare Items for internal use + itemProvider.registerDataRepresentation(forTypeIdentifier: ImportPasteboardAction.InternalPasteboardCopyKey, visibility: .ownProcess) { (completionBlock) -> Progress? in + let data = OCItemPasteboardValue(item: item, bookmarkUUID: uuid).encode() + completionBlock(data, nil) + return nil + } + + // Prepare Items for globale usage + if item.type == .file { // only files can be added to the globale pasteboard + guard let itemMimeType = item.mimeType else { return } + + let mimeTypeCF = itemMimeType as CFString + guard let rawUti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeTypeCF, nil)?.takeRetainedValue() as String? else { return } + + itemProvider.registerFileRepresentation(forTypeIdentifier: rawUti, fileOptions: [], visibility: .all, loadHandler: { [weak core] (completionHandler) -> Progress? in + var progress : Progress? + + guard let core = core else { + completionHandler(nil, false, NSError(domain: OCErrorDomain, code: Int(OCError.internal.rawValue), userInfo: nil)) + return nil + } + + if let localFileURL = core.localCopy(of: item) { + // Provide local copies directly + completionHandler(localFileURL, true, nil) + } else { + // Otherwise download the file and provide it when done + progress = core.downloadItem(item, options: [ + .returnImmediatelyIfOfflineOrUnavailable : true, + .addTemporaryClaimForPurpose : OCCoreClaimPurpose.view.rawValue + ], resultHandler: { [weak self] (error, core, item, file) in + guard error == nil, let fileURL = file?.url else { + completionHandler(nil, false, error) + return + } + + completionHandler(fileURL, true, nil) + + if let claim = file?.claim, let item = item, let self = self { + self.core?.remove(claim, on: item, afterDeallocationOf: [fileURL]) + } + }) + } + + return progress + }) + } + itemProviderItems.append(itemProvider) + }) + + globalPasteboard.itemProviders = itemProviderItems + + var subtitle = "%ld Item was copied to the clipboard".localized + if itemProviderItems.count > 1 { + subtitle = "%ld Items was copied to the clipboard".localized + } + + OnMainThread { + if let navigationController = viewController.navigationController { + _ = NotificationHUDViewController(on: navigationController, title: "Copy".localized, subtitle: String(format: subtitle, itemProviderItems.count)) + } + } + } + } + + hudViewController.presentHUDOn(viewController: viewController) + + self.completed() } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/CutAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CutAction.swift new file mode 100644 index 000000000..0b6a609ce --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/CutAction.swift @@ -0,0 +1,83 @@ +// +// CutAction.swift +// ownCloud +// +// Created by Matthias Hühne on 27/09/2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import Foundation +import ownCloudSDK +import MobileCoreServices +import ownCloudAppShared + +class CutAction : Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.cutpasteboard") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Cut".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem, .moreDetailItem, .moreFolder, .toolbar, .keyboardShortcut, .contextMenuItem] } + override class var keyCommand : String? { return "X" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + if forContext.containsRoot { + return .none + } + + return .middle + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let viewController = context.viewController, let core = context.core else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + + let items = context.items + let uuid = core.bookmark.uuid.uuidString + var itemProviderItems: [NSItemProvider] = [] + let globalPasteboard = UIPasteboard.general + globalPasteboard.items = [] + + items.forEach({ (item) in + + let itemProvider = NSItemProvider() + + itemProvider.suggestedName = item.name + + itemProvider.registerDataRepresentation(forTypeIdentifier: ImportPasteboardAction.InternalPasteboardCutKey, visibility: .ownProcess) { (completionBlock) -> Progress? in + let data = OCItemPasteboardValue(item: item, bookmarkUUID: uuid).encode() + completionBlock(data, nil) + return nil + } + itemProviderItems.append(itemProvider) + + }) + globalPasteboard.itemProviders = itemProviderItems + completed() + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem || location == .moreDetailItem || location == .moreFolder || location == .contextMenuItem { + if #available(iOS 13.0, *) { + return UIImage(systemName: "scissors") + } else { + return UIImage(named: "clipboard") + } + } + + return nil + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift b/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift new file mode 100644 index 000000000..7f8e67baa --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/ImportPasteboardAction.swift @@ -0,0 +1,243 @@ +// +// ImportPasteboardAction.swift +// ownCloud +// +// Created by Matthias Hühne on 27/09/2019. +// Copyright © 2019 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2019, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import Foundation +import ownCloudSDK +import MobileCoreServices +import ownCloudAppShared + +extension String { + func trimIllegalCharacters() -> String { + return self + .trimmingCharacters(in: .illegalCharacters) + .trimmingCharacters(in: .whitespaces) + .trimmingCharacters(in: .newlines) + .filter({ $0.isASCII }) + } +} + +class ImportPasteboardAction : Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.importpasteboard") } + override class var category : ActionCategory? { return .normal } + override class var name : String? { return "Paste".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreFolder, .keyboardShortcut] } + override class var keyCommand : String? { return "V" } + override class var keyModifierFlags: UIKeyModifierFlags? { return [.command] } + + static let InternalPasteboardCopyKey = "com.owncloud.uti.ocitem.copy" + static let InternalPasteboardCutKey = "com.owncloud.uti.ocitem.cut" + static let InternalPasteboardChangedCounterKey = OCKeyValueStoreKey(rawValue: "com.owncloud.internal-pasteboard-changed-counter") + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + let pasteboard = UIPasteboard.general + if pasteboard.numberOfItems > 0 { + return .afterMiddle + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard context.items.count > 0, let core = self.core, let rootItem = context.query?.rootItem else { + completed(with: NSError(ocError: .insufficientParameters)) + return + } + let generalPasteboard = UIPasteboard.general + + if generalPasteboard.contains(pasteboardTypes: [ImportPasteboardAction.InternalPasteboardCopyKey, ImportPasteboardAction.InternalPasteboardCutKey]) { + + for item in generalPasteboard.itemProviders { + // Copy Items Internally + item.loadDataRepresentation(forTypeIdentifier: ImportPasteboardAction.InternalPasteboardCopyKey, completionHandler: { data, error in + if let data = data, let object = OCItemPasteboardValue(data: data) { + let item = object.item + let bookmarkUUID = object.bookmarkUUID + guard let name = item.name else { return } + + if core.bookmark.uuid.uuidString == bookmarkUUID { + core.copy(item, to: rootItem, withName: name, options: nil, resultHandler: { (error, _, _, _) in + if error != nil { + self.completed(with: error) + } + }) + } else { + // Move between Accounts + guard let sourceBookmark = OCBookmarkManager.shared.bookmark(forUUIDString: bookmarkUUID), let destinationItem = self.context.items.first else {return } + + OCCoreManager.shared.requestCore(for: sourceBookmark, setup: nil) { (srcCore, error) in + if error == nil { + srcCore?.downloadItem(item, options: nil, resultHandler: { (error, _, srcItem, _) in + if error == nil, let srcItem = srcItem, let localURL = srcCore?.localCopy(of: srcItem) { + core.importItemNamed(srcItem.name, at: destinationItem, from: localURL, isSecurityScoped: false, options: nil, placeholderCompletionHandler: nil) { (_, _, _, _) in + } + } + }) + } + } + } + } + }) + + // Cut Item Internally + item.loadDataRepresentation(forTypeIdentifier: ImportPasteboardAction.InternalPasteboardCutKey, completionHandler: { data, error in + if let data = data, let object = OCItemPasteboardValue(data: data) { + + let item = object.item + let bookmarkUUID = object.bookmarkUUID + guard let name = item.name else { return } + + if core.bookmark.uuid.uuidString == bookmarkUUID { + core.move(item, to: rootItem, withName: name, options: nil) { (error, _, _, _) in + if error != nil { + self.completed(with: error) + } else { + generalPasteboard.items = [] + } + } + } else { + // Move between Accounts + guard let sourceBookmark = OCBookmarkManager.shared.bookmark(forUUIDString: bookmarkUUID), let destinationItem = self.context.items.first else {return } + + OCCoreManager.shared.requestCore(for: sourceBookmark, setup: nil) { (srcCore, error) in + if error == nil { + srcCore?.downloadItem(item, options: nil, resultHandler: { (error, _, srcItem, _) in + if error == nil, let srcItem = srcItem, let localURL = srcCore?.localCopy(of: srcItem) { + core.importItemNamed(srcItem.name, at: destinationItem, from: localURL, isSecurityScoped: false, options: nil, placeholderCompletionHandler: nil) { (_, _, _, _) in + + srcCore?.delete(srcItem, requireMatch: true, resultHandler: { (error, _, _, _) in + if error != nil { + Log.log("Error \(String(describing: error)) deleting \(String(describing: item.path))") + } else { + + generalPasteboard.items = [] + } + }) + } + } + }) + } + } + } + } + }) + } + } else { + // System-wide Pasteboard Items + + for item in generalPasteboard.itemProviders { + let typeIdentifiers = item.registeredTypeIdentifiers + let preferredUTIs = [ + kUTTypeImage, + kUTTypeMovie, + kUTTypePDF, + kUTTypeText, + kUTTypeRTF, + kUTTypeHTML, + kUTTypePlainText + ] + var useUTI : String? + var useIndex : Int = Int.max + + for typeIdentifier in typeIdentifiers { + if !typeIdentifier.hasPrefix("dyn.") { + for preferredUTI in preferredUTIs { + let conforms = UTTypeConformsTo(typeIdentifier as CFString, preferredUTI) + + // Log.log("\(preferredUTI) vs \(typeIdentifier) -> \(conforms)") + + if conforms { + if let utiIndex = preferredUTIs.index(of: preferredUTI), utiIndex < useIndex { + useUTI = typeIdentifier + useIndex = utiIndex + } + } + } + } + } + + if useUTI == nil, typeIdentifiers.count == 1 { + useUTI = typeIdentifiers.first + } + + if useUTI == nil { + useUTI = kUTTypeData as String + } + + var fileName: String? + + item.loadFileRepresentation(forTypeIdentifier: useUTI!) { (url, _ error) in + guard let url = url else { return } + + let fileNameMaxLength = 16 + + if useUTI == kUTTypeUTF8PlainText as String { + fileName = try? String(String(contentsOf: url, encoding: .utf8).prefix(fileNameMaxLength) + ".txt") + } + + if useUTI == kUTTypeRTF as String { + let options = [NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.rtf] + fileName = try? String(NSAttributedString(url: url, options: options, documentAttributes: nil).string.prefix(fileNameMaxLength) + ".rtf") + } + + fileName = fileName? + .trimmingCharacters(in: .illegalCharacters) + .trimmingCharacters(in: .whitespaces) + .trimmingCharacters(in: .newlines) + .filter({ $0.isASCII }) + + if fileName == nil { + fileName = url.lastPathComponent + } + + guard let name = fileName else { return } + + self.upload(itemURL: url, rootItem: rootItem, name: name) + } + } + } + } + + open func upload(itemURL: URL, rootItem: OCItem, name: String, completionHandler: ClientActionCompletionHandler? = nil) { + core?.importItemNamed(name, at: rootItem, from: itemURL, isSecurityScoped: false, options: [ + OCCoreOption.importByCopying : false, + OCCoreOption.automaticConflictResolutionNameStyle : OCCoreDuplicateNameStyle.bracketed.rawValue + ], placeholderCompletionHandler: nil, resultHandler: { (error, _ core, _ item, _) in + if error != nil { + Log.debug("Error uploading \(Log.mask(name)) file to \(Log.mask(rootItem.path))") + completionHandler?(false) + } else { + Log.debug("Success uploading \(Log.mask(name)) file to \(Log.mask(rootItem.path))") + completionHandler?(true) + } + }) + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + if location == .moreItem || location == .moreFolder { + if #available(iOS 13.0, *) { + return UIImage(systemName: "doc.on.clipboard") + } else { + return UIImage(named: "clipboard") + } + } + + return nil + } +} diff --git a/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift new file mode 100644 index 000000000..9b4e7d429 --- /dev/null +++ b/ownCloud/Client/Actions/ClientDirectoryPickerViewController.swift @@ -0,0 +1,237 @@ +// +// ClientDirectoryPickerViewController.swift +// ownCloud +// +// Created by Pablo Carrascal on 22/08/2018. +// Copyright © 2018 ownCloud GmbH. All rights reserved. +// + +/* +* Copyright (C) 2018, ownCloud GmbH. +* +* This code is covered by the GNU Public License Version 3. +* +* For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ +* You should have received a copy of this license along with this program. If not, see . +* +*/ + +import UIKit +import ownCloudSDK + +typealias ClientDirectoryPickerPathFilter = (_ path: String) -> Bool +typealias ClientDirectoryPickerChoiceHandler = (_ chosenItem: OCItem?) -> Void + +class ClientDirectoryPickerViewController: ClientQueryViewController { + + private let SELECT_BUTTON_HEIGHT: CGFloat = 44.0 + + // MARK: - Instance Properties + var selectButton: UIBarButtonItem? + private var selectButtonTitle: String? + private var cancelBarButton: UIBarButtonItem? + + var directoryPath : String? + + var choiceHandler: ClientDirectoryPickerChoiceHandler? + var allowedPathFilter : ClientDirectoryPickerPathFilter? + var navigationPathFilter : ClientDirectoryPickerPathFilter? + + // MARK: - Init & deinit + convenience init(core inCore: OCCore, path: String, selectButtonTitle: String, avoidConflictsWith items: [OCItem], choiceHandler: @escaping ClientDirectoryPickerChoiceHandler) { + let folderItemPaths = items.filter({ (item) -> Bool in + return item.type == .collection && item.path != nil && !item.isRoot + }).map { (item) -> String in + return item.path! + } + let itemParentPaths = items.filter({ (item) -> Bool in + return item.path?.parentPath != nil + }).map { (item) -> String in + return item.path!.parentPath + } + + var navigationPathFilter : ClientDirectoryPickerPathFilter? + + if folderItemPaths.count > 0 { + navigationPathFilter = { (targetPath) in + return !folderItemPaths.contains(targetPath) + } + } + + self.init(core: inCore, path: path, selectButtonTitle: selectButtonTitle, allowedPathFilter: { (targetPath) in + // Disallow all paths as target that are parent of any of the items + return !itemParentPaths.contains(targetPath) + }, navigationPathFilter: navigationPathFilter, choiceHandler: choiceHandler) + } + + init(core inCore: OCCore, path: String, selectButtonTitle: String, allowedPathFilter: ClientDirectoryPickerPathFilter? = nil, navigationPathFilter: ClientDirectoryPickerPathFilter? = nil, choiceHandler: @escaping ClientDirectoryPickerChoiceHandler) { + let targetDirectoryQuery = OCQuery(forPath: path) + + // Sort folders first + targetDirectoryQuery.sortComparator = { (left, right) in + guard let leftItem = left as? OCItem, let rightItem = right as? OCItem else { + return .orderedSame + } + if leftItem.type == OCItemType.collection && rightItem.type != OCItemType.collection { + return .orderedAscending + } else if leftItem.type != OCItemType.collection && rightItem.type == OCItemType.collection { + return .orderedDescending + } else if leftItem.name != nil && rightItem.name != nil { + return leftItem.name!.caseInsensitiveCompare(rightItem.name!) + } + return .orderedSame + } + + super.init(core: inCore, query: targetDirectoryQuery) + + self.directoryPath = path + + self.choiceHandler = choiceHandler + + self.selectButtonTitle = selectButtonTitle + self.allowedPathFilter = allowedPathFilter + self.navigationPathFilter = navigationPathFilter + + // Force disable sorting options + self.shallShowSortBar = false + + // Disable pull to refresh + allowPullToRefresh = false + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - ViewController lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + // Adapt to disabled pull-to-refresh + self.tableView.alwaysBounceVertical = false + + // Select button creation + selectButton = UIBarButtonItem(title: selectButtonTitle, style: .plain, target: self, action: #selector(selectButtonPressed)) + selectButton?.title = selectButtonTitle + + if let allowedPathFilter = allowedPathFilter, let directoryPath = directoryPath { + selectButton?.isEnabled = allowedPathFilter(directoryPath) + } + + // Cancel button creation + cancelBarButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelBarButtonPressed)) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(true) + + if let cancelBarButton = cancelBarButton { + navigationItem.rightBarButtonItems = [cancelBarButton] + } + + if let navController = self.navigationController, let selectButton = selectButton { + navController.isToolbarHidden = false + navController.toolbar.isTranslucent = false + let flexibleSpaceBarButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + + let leftButtonImage = Theme.shared.image(for: "folder-create", size: CGSize(width: 30.0, height: 30.0))!.withRenderingMode(.alwaysTemplate) + + let createFolderBarButton = UIBarButtonItem(image: leftButtonImage, style: .plain, target: self, action: #selector(createFolderButtonPressed)) + createFolderBarButton.accessibilityIdentifier = "client.folder-create" + + self.setToolbarItems([createFolderBarButton, flexibleSpaceBarButton, selectButton, flexibleSpaceBarButton], animated: false) + } + } + + private func allowNavigationFor(item: OCItem?) -> Bool { + guard let item = item else { return false } + + var allowNavigation = item.type == .collection + + if allowNavigation, let navigationPathFilter = navigationPathFilter, let itemPath = item.path { + allowNavigation = navigationPathFilter(itemPath) + } + + return allowNavigation + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = super.tableView(tableView, cellForRowAt: indexPath) + + if let clientItemCell = cell as? ClientItemCell { + clientItemCell.isMoreButtonPermanentlyHidden = true + clientItemCell.isActive = self.allowNavigationFor(item: clientItemCell.item) + } + + return cell + } + + override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if let item : OCItem = itemAt(indexPath: indexPath), allowNavigationFor(item: item) { + return true + } + + return false + } + + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + if let item : OCItem = itemAt(indexPath: indexPath), allowNavigationFor(item: item) { + return indexPath + } + + return nil + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let item : OCItem = itemAt(indexPath: indexPath), item.type == OCItemType.collection, let core = self.core, let path = item.path, let selectButtonTitle = selectButtonTitle, let choiceHandler = choiceHandler else { + return + } + + self.navigationController?.pushViewController(ClientDirectoryPickerViewController(core: core, path: path, selectButtonTitle: selectButtonTitle, allowedPathFilter: allowedPathFilter, navigationPathFilter: navigationPathFilter, choiceHandler: choiceHandler), animated: true) + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + return nil + } + + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + return .none + } + + // MARK: - Actions + func userChose(item: OCItem?) { + self.choiceHandler?(item) + } + + @objc private func cancelBarButtonPressed() { + dismiss(animated: true, completion: { + self.userChose(item: nil) + }) + } + + @objc private func selectButtonPressed() { + dismiss(animated: true, completion: { + self.userChose(item: self.query.rootItem) + }) + } + + @objc func createFolderButtonPressed(_ sender: UIBarButtonItem) { + // Actions for Create Folder + if let core = self.core, let rootItem = query.rootItem { + let actionsLocation = OCExtensionLocation(ofType: .action, identifier: .folderAction) + let actionContext = ActionContext(viewController: self, core: core, items: [rootItem], location: actionsLocation) + + let actions = Action.sortedApplicableActions(for: actionContext).filter { (action) -> Bool in + if action.actionExtension.identifier == OCExtensionIdentifier("com.owncloud.action.createFolder") { + return true + } + + return false + } + + let createFolderAction = actions.first + createFolderAction?.progressHandler = makeActionProgressHandler() + createFolderAction?.run() + } + } +} diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 09a85c4ba..7797ef54c 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -48,8 +48,6 @@ class ClientRootViewController: UITabBarController, BookmarkContainer, ToolAndTa var notificationPresenter : NotificationMessagePresenter? var cardMessagePresenter : CardIssueMessagePresenter? - var pasteboardChangedCounter = 0 - weak var authDelegate : ClientRootViewControllerAuthenticationDelegate? var skipAuthorizationFailure : Bool = false diff --git a/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift b/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift index 518437ef3..918ec6507 100644 --- a/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift +++ b/ownCloud/Client/FileList Extensions/QueryFileListTableViewController+Multiselect.swift @@ -43,6 +43,14 @@ extension QueryFileListTableViewController : MultiSelectSupport { copyMultipleBarButtonItem?.accessibilityLabel = "Copy".localized copyMultipleBarButtonItem?.isEnabled = false + var cutImage = UIImage(named: "clipboard") + if #available(iOS 13.0, *) { + cutImage = UIImage(systemName: "scissors") + } + cutMultipleBarButtonItem = UIBarButtonItem(image: cutImage, target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: CutAction.identifier!) + cutMultipleBarButtonItem?.accessibilityLabel = "Cut".localized + cutMultipleBarButtonItem?.isEnabled = false + openMultipleBarButtonItem = UIBarButtonItem(image: UIImage(named: "open-in"), target: self as AnyObject, action: #selector(actOnMultipleItems), dropTarget: self, actionIdentifier: OpenInAction.identifier!) openMultipleBarButtonItem?.accessibilityLabel = "Open in".localized openMultipleBarButtonItem?.isEnabled = false @@ -141,6 +149,8 @@ extension QueryFileListTableViewController : MultiSelectSupport { flexibleSpaceBarButton, copyMultipleBarButtonItem!, flexibleSpaceBarButton, + cutMultipleBarButtonItem!, + flexibleSpaceBarButton, duplicateMultipleBarButtonItem!, flexibleSpaceBarButton, deleteMultipleBarButtonItem!]) diff --git a/ownCloud/Resources/ar.lproj/Localizable.strings b/ownCloud/Resources/ar.lproj/Localizable.strings index f72977500..80fb7b8b1 100644 Binary files a/ownCloud/Resources/ar.lproj/Localizable.strings and b/ownCloud/Resources/ar.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/cs.lproj/Localizable.strings b/ownCloud/Resources/cs.lproj/Localizable.strings index 00d03e983..6b8e4ffc8 100644 --- a/ownCloud/Resources/cs.lproj/Localizable.strings +++ b/ownCloud/Resources/cs.lproj/Localizable.strings @@ -595,8 +595,8 @@ "Select Last Item on Page" = "Select Last Item on Page"; "Scroll to Top" = "Scroll to Top"; "Scroll to Bottom" = "Scroll to Bottom"; -"Copy to Pasteboard" = "Copy to Pasteboard"; -"Import from Pasteboard" = "Import from Pasteboard"; +"Copy to Clipboard" = "Copy to Clipboard"; +"Import from Clipboard" = "Import from Clipboard"; "Next" = "Následující"; "Previous" = "Předchozí"; "Favorite" = "Oblíbené"; diff --git a/ownCloud/Resources/de.lproj/Localizable.strings b/ownCloud/Resources/de.lproj/Localizable.strings index 12a605e54..119f1a1aa 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 5ee820808..c4bb5c698 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -362,6 +362,8 @@ "Open in a new Window" = "Open in a new Window"; "Open in Window" = "Open in Window"; "Take photo or video" = "Take photo or video"; +"%ld Item was copied to the clipboard" = "%ld Item was copied to the clipboard"; +"%ld Items was copied to the clipboard" = "%ld Items was copied to the clipboard"; "Preparing…" = "Preparing…"; @@ -657,8 +659,8 @@ "Select Last Item on Page" = "Select Last Item on Page"; "Scroll to Top" = "Scroll to Top"; "Scroll to Bottom" = "Scroll to Bottom"; -"Copy to Pasteboard" = "Copy to Pasteboard"; -"Import from Pasteboard" = "Import from Pasteboard"; +"Copy to Clipboard" = "Copy to Clipboard"; +"Import from Clipboard" = "Import from Clipboard"; "Next" = "Next"; "Previous" = "Previous"; "Favorite" = "Favorite"; diff --git a/ownCloud/Resources/es.lproj/Localizable.strings b/ownCloud/Resources/es.lproj/Localizable.strings index 75146470c..410b5dcfa 100644 Binary files a/ownCloud/Resources/es.lproj/Localizable.strings and b/ownCloud/Resources/es.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/eu.lproj/Localizable.strings b/ownCloud/Resources/eu.lproj/Localizable.strings index 34870d17e..b492a96b1 100644 Binary files a/ownCloud/Resources/eu.lproj/Localizable.strings and b/ownCloud/Resources/eu.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/fr.lproj/Localizable.strings b/ownCloud/Resources/fr.lproj/Localizable.strings index fa5f23d7a..66d5f7799 100644 Binary files a/ownCloud/Resources/fr.lproj/Localizable.strings and b/ownCloud/Resources/fr.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/ko.lproj/Localizable.strings b/ownCloud/Resources/ko.lproj/Localizable.strings index aef90b418..713dd7729 100644 Binary files a/ownCloud/Resources/ko.lproj/Localizable.strings and b/ownCloud/Resources/ko.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/mk.lproj/Localizable.strings b/ownCloud/Resources/mk.lproj/Localizable.strings index 7de8169b0..2e669d090 100644 Binary files a/ownCloud/Resources/mk.lproj/Localizable.strings and b/ownCloud/Resources/mk.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/nb-NO.lproj/Localizable.strings b/ownCloud/Resources/nb-NO.lproj/Localizable.strings index 7ab0e75c6..6b7dccf26 100644 --- a/ownCloud/Resources/nb-NO.lproj/Localizable.strings +++ b/ownCloud/Resources/nb-NO.lproj/Localizable.strings @@ -596,8 +596,8 @@ "Select Last Item on Page" = "Select Last Item on Page"; "Scroll to Top" = "Scroll to Top"; "Scroll to Bottom" = "Scroll to Bottom"; -"Copy to Pasteboard" = "Copy to Pasteboard"; -"Import from Pasteboard" = "Import from Pasteboard"; +"Copy to Clipboard" = "Copy to Clipboard"; +"Import from Clipboard" = "Import from Clipboard"; "Next" = "Neste"; "Previous" = "Forrige"; "Favorite" = "Gjør til favoritt"; diff --git a/ownCloud/Resources/nn-NO.lproj/Localizable.strings b/ownCloud/Resources/nn-NO.lproj/Localizable.strings index 3078c5e3b..6269f4ffd 100644 --- a/ownCloud/Resources/nn-NO.lproj/Localizable.strings +++ b/ownCloud/Resources/nn-NO.lproj/Localizable.strings @@ -595,8 +595,8 @@ "Select Last Item on Page" = "Select Last Item on Page"; "Scroll to Top" = "Scroll to Top"; "Scroll to Bottom" = "Scroll to Bottom"; -"Copy to Pasteboard" = "Copy to Pasteboard"; -"Import from Pasteboard" = "Import from Pasteboard"; +"Copy to Clipboard" = "Copy to Clipboard"; +"Import from Clipboard" = "Import from Clipboard"; "Next" = "Neste"; "Previous" = "Førre"; "Favorite" = "Favoritt"; diff --git a/ownCloud/Resources/pt-BR.lproj/Localizable.strings b/ownCloud/Resources/pt-BR.lproj/Localizable.strings index ea6488582..39b0d52c6 100644 Binary files a/ownCloud/Resources/pt-BR.lproj/Localizable.strings and b/ownCloud/Resources/pt-BR.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/pt-PT.lproj/Localizable.strings b/ownCloud/Resources/pt-PT.lproj/Localizable.strings index 238a52ab1..3986c40f3 100644 --- a/ownCloud/Resources/pt-PT.lproj/Localizable.strings +++ b/ownCloud/Resources/pt-PT.lproj/Localizable.strings @@ -595,8 +595,8 @@ "Select Last Item on Page" = "Select Last Item on Page"; "Scroll to Top" = "Scroll to Top"; "Scroll to Bottom" = "Scroll to Bottom"; -"Copy to Pasteboard" = "Copy to Pasteboard"; -"Import from Pasteboard" = "Import from Pasteboard"; +"Copy to Clipboard" = "Copy to Clipboard"; +"Import from Clipboard" = "Import from Clipboard"; "Next" = "Seguinte"; "Previous" = "Anterior"; "Favorite" = "Favorito"; diff --git a/ownCloud/Resources/ru.lproj/Localizable.strings b/ownCloud/Resources/ru.lproj/Localizable.strings index cdc8c17ca..17d466740 100644 Binary files a/ownCloud/Resources/ru.lproj/Localizable.strings and b/ownCloud/Resources/ru.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/sq.lproj/Localizable.strings b/ownCloud/Resources/sq.lproj/Localizable.strings index 94c941686..41976c4e0 100644 Binary files a/ownCloud/Resources/sq.lproj/Localizable.strings and b/ownCloud/Resources/sq.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/th-TH.lproj/Localizable.strings b/ownCloud/Resources/th-TH.lproj/Localizable.strings index 83b4494aa..466d285b6 100644 Binary files a/ownCloud/Resources/th-TH.lproj/Localizable.strings and b/ownCloud/Resources/th-TH.lproj/Localizable.strings differ diff --git a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings index 3bd2fde84..0348f7640 100644 Binary files a/ownCloud/Resources/zh-Hans.lproj/Localizable.strings and b/ownCloud/Resources/zh-Hans.lproj/Localizable.strings differ diff --git a/ownCloud/Tools/VendorServices.swift b/ownCloud/Tools/VendorServices.swift new file mode 100644 index 000000000..81cd5421b --- /dev/null +++ b/ownCloud/Tools/VendorServices.swift @@ -0,0 +1,154 @@ +// +// VendorServices.swift +// ownCloud +// +// Created by Felix Schwarz on 29.10.18. +// Copyright © 2018 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2018, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import UIKit +import MessageUI +import ownCloudSDK + +class VendorServices : NSObject { + // MARK: - App version information + var appVersion: String { + if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String { + return version + } + + return "" + } + + var appBuildNumber: String { + if let buildNumber = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String { + return buildNumber + } + + return "" + } + + var lastGitCommit: String { + if let gitCommit = LastGitCommit() { + return gitCommit + } + + return "" + } + + var isBetaBuild: Bool { + if let isBetaBuild = self.classSetting(forOCClassSettingsKey: .isBetaBuild) as? Bool { + return isBetaBuild + } + + return false + } + + var showBetaWarning: Bool { + if let showBetaWarning = self.classSetting(forOCClassSettingsKey: .showBetaWarning) as? Bool { + return showBetaWarning + } + + return false + } + + static var shared : VendorServices = { + return VendorServices() + }() + + // MARK: - Vendor services + func recommendToFriend(from viewController: UIViewController) { + + guard let appStoreLink = MoreSettingsSection.classSetting(forOCClassSettingsKey: .appStoreLink) as? String, + let appName = OCAppIdentity.shared.appName else { + return + } + + let message = """ +

I want to invite you to use \(appName) on your smartphone!

+Download here +""" + self.sendMail(to: nil, subject: "Try \(appName) on your smartphone!", message: message, from: viewController) + } + + func sendFeedback(from viewController: UIViewController) { + var buildType = "release".localized + + if self.isBetaBuild { + buildType = "beta".localized + } + + guard let feedbackEmail = MoreSettingsSection.classSetting(forOCClassSettingsKey: .feedbackEmail) as? String, + let appName = OCAppIdentity.shared.appName else { + return + } + self.sendMail(to: feedbackEmail, subject: "\(self.appVersion) (\(self.appBuildNumber)) \(buildType) \(appName)", message: nil, from: viewController) + } + + func sendMail(to: String?, subject: String?, message: String?, from viewController: UIViewController) { + if MFMailComposeViewController.canSendMail() { + let mail = MFMailComposeViewController() + mail.mailComposeDelegate = self + if to != nil { + mail.setToRecipients([to!]) + } + + if subject != nil { + mail.setSubject(subject!) + } + + if message != nil { + mail.setMessageBody(message!, isHTML: true) + } + + viewController.present(mail, animated: true) + } else { + let alert = UIAlertController(title: "Please configure an email account".localized, + message: "You need to configure an email account first to be able to send emails.".localized, + preferredStyle: .alert) + + let okAction = UIAlertAction(title: "OK", style: .default, handler: nil) + alert.addAction(okAction) + viewController.present(alert, animated: true) + } + } +} + +extension VendorServices: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true) + } +} + +// MARK: - OCClassSettings support +extension OCClassSettingsIdentifier { + static let app = OCClassSettingsIdentifier("app") +} + +extension OCClassSettingsKey { + static let showBetaWarning = OCClassSettingsKey("show-beta-warning") + static let isBetaBuild = OCClassSettingsKey("is-beta-build") + static let enableUIAnimations = OCClassSettingsKey("enable-ui-animations") +} + +extension VendorServices : OCClassSettingsSupport { + static let classSettingsIdentifier : OCClassSettingsIdentifier = .app + + static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? { + if identifier == .app { + return [ .isBetaBuild : true, .showBetaWarning : true, .enableUIAnimations: true ] + } + + return nil + } +} diff --git a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift index bf4770ace..fb9c309ca 100644 --- a/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift +++ b/ownCloudAppShared/Client/File Lists/QueryFileListTableViewController.swift @@ -68,6 +68,7 @@ open class QueryFileListTableViewController: FileListTableViewController, SortBa public var moveMultipleBarButtonItem: UIBarButtonItem? public var duplicateMultipleBarButtonItem: UIBarButtonItem? public var copyMultipleBarButtonItem: UIBarButtonItem? + public var cutMultipleBarButtonItem: UIBarButtonItem? public var openMultipleBarButtonItem: UIBarButtonItem? public var isMoreButtonPermanentlyHidden: Bool = false public var didSelectCellAction: ((_ completion: @escaping () -> Void) -> Void)?