From 4e47f9ba6853084029595bb676ec2c9cb7a1f5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20H=C3=BChne?= Date: Tue, 29 Oct 2019 20:45:18 +0100 Subject: [PATCH] [feature/multiple-windows] Multiple window support for iPadOS (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Naming improvements based on latest SDK: - uploads use new OCCoreOptionAutomaticConflictResolutionNameStyle option to automatically resolve naming conflicts during upload - Duplicate action uses the new OCCore name suggestion API to - determine naming conflicts and resolve them automatically - match the name style of the file to duplicate, f.ex. - duplicating "File copy.jpg" will create "File copy 2.jpg" - duplicating "File (1).jpg" will create "File (2).jpg" - duplicating "File Kopie 2.jpg" will create "File Kopie 3.jpg" - folder creation uses the new OCCore name suggestion API to - detect naming conflicts beforehand - pre-fill the new folder name name with an unused name directly * #393 added an activity indicator which will be shown, if offline copies will be deleted to give the user a UI feedback * - Change SDK branch to master * #76 user can import files using the iOS share sheet. all file types are accepted. * - using new SDK with fixes for requesting core - minor code fixes * - Log device, version and locale information at the beginning of every log file using new SDK protocol - Show SDK commit hash in Settings * fixed crash, changed bookmark name to shortname * changed UIAlertViewController for account selection to CardViewController style for testing * fixed icon badge creation for fastlane lane In-House Enterprise IPA generation * install librsvg for fastlane via sh * moved import file code to own class * added option to automatically resolve name conflicts, if file name already exists * Changed app version to 1.1.0 * enabled beta build and warning * Keep the gallery alive after doing some action over an item (#447) * Rename updates the currently visible file in the gallery * Keeping gallery alive fix - Don’t pop to the root view controller on destructive action by default - Only pop to the root if all items have been deleted - Select next item upon deletion of the current ones - Update UIPageViewController data source if number of items changes due e.g. to duplication or deletion * Small fixes * Fixed review findings * Media player implemented using AVKit (#429) * Media player implemented using AVKit * Added possibility to stream audio / video * Improved error handling * Fixed review finding * Added background audio, airplay and PiP mode * Propagating safe area to the main view of AVPlayerViewController * Another fix for safe area considering layout of AVPlayerViewController * Updated to latest develop version of SDK * Display error message in case file couldn’t be preview e.g. due to file corruption (#427) * Fixed UI test for creating folder, need to return a fixed name, because suggestion is not working in tests * added a description header and changed typo * - fixed showing the directory picker controller, after the card view was dismissed - use the current development SDK * Version Bump to 126 * - create local copy of import file, if needed - fixed create folder action - delete local copy, if needed - code review changes * - improved duplicate item deletion behaviour - add additional safeguard so duplicate files are only deleted if they were actually duplicated (previously non-duplicate files could be removed if duplication or folder creation failed) - remove temporary container folder, too - add 2 second delay before returning the core to give the core an opportunity to schedule the upload on a NSURLSession * Version Bump to 127 * changed back signing identity * Version Bump to 128 * added required CFBundleTypeName key * Version Bump to 129 * updated changelog * use formSheet presentation style for the iPad when showing the document picker * Fix for images not being displayed in the gallery * Version Bump to 130 * Version Bump to 131 * [fix/sharing-search] Fixed min length for searching sharing users (#455) * #454 used correct comparator for sharingSearchMinLength and set a new default value, if capability does not exists * added a constant for defaultSharingSearchMinLength value * - Update ios-sdk to address finding (1) in ios-app #446 * Preventing updating UI in DisplayViewController while item is being changed - Not calling present(item:) while meta data of OCItem is being updated - Elliminated an assumption in MediaDisplayViewController that render renderSpecificView() can be called only once. - Making sure that AVPlayer is not re-created upon changes in OCItem but that rather AVPlayerItem is replaced. * Fixed a small warning * Tried to improve gallery logic concerning items modification - Watch out if the modification does still includes the item with local ID of the currently visible item. - Take care of failed move but watching out for reappearing items * Added a setting allowing to decide user if media files shall be streamed * - Add debug output to Display*ViewController - Fix SwiftLint warnings * Fixed handling of deleted / moved item in the gallery * Another small fix for handling of failed item move * Version Bump to 132 * Added LSSupportsOpeningDocumentsInPlace key to Info.plist * Fixed Info.plist and added LSHandlerRank property * Fix for #455 issue: no search request triggered * show always the account selection sheet and added a note, that only one file can be imported at once * use securtiy scoped file operation for importing a file * Fix for the PR #447 (keep gallery alive) (#465) * Keeping track of individual OCItems in DisplayViewController instances But loading the list of items in the gallery only once and not reacting to any changes (moving, deleting) * Removed query stop call in DisplayHostViewController Since this query is not created there but just passed from the parent view. * Removing more button if the currently viewed file got moved or deleted * Fixed updating UI after renaming current item * Fixed a warning * Fixed issues in the gallery * - Minor fixes * first draft for supporting multiple windows * implemented opening an account in a new window * added row action * - open account, now opens file list - implemented open account from table view edit action - implemented contextual menu for account row * Starting implementing state restoration * Implemented UI restoration state for window scenes * Implemented UI state restoration for opening a OCItem * - only show close window item on iPad - fixed icons for location * - only show "Open in new Window" on iPad - better view restoration - deselect row * - added new menu item to open a new window - fixed tint color for icons * - added missing localization strings - removed no longer needed class * - fixed code review findings - code refactoring * - moved creating file list stack code into ClientRootViewController class - added iOS 13 available query * fixed merge error * fixed drag and drop between accounts (when dragging items from one window to the other window on iPad) * prevent dragging folders from one account to an other account --- ownCloud.xcodeproj/project.pbxproj | 24 +++ ownCloud/AppDelegate.swift | 16 ++ .../DiscardSceneAction.swift | 60 ++++++ .../Actions+Extensions/OpenInAction.swift | 2 +- .../Actions+Extensions/OpenSceneAction.swift | 60 ++++++ .../Client/ClientQueryViewController.swift | 93 +++++--- .../Client/ClientRootViewController.swift | 58 ++++- .../FileListTableViewController.swift | 47 +++-- .../QueryFileListTableViewController.swift | 29 +++ ownCloud/Resources/Info.plist | 26 ++- .../Resources/en.lproj/Localizable.strings | 3 + .../SDK Extensions/OCCore+Extension.swift | 16 ++ .../SDK Extensions/OCItem+Extension.swift | 4 + ownCloud/SceneDelegate.swift | 71 +++++++ .../ServerListTableViewController.swift | 199 ++++++++++++++---- ownCloud/Tools/VendorServices.swift | 2 +- ownCloud/Window/OpenItemUserActivity.swift | 42 ++++ 17 files changed, 655 insertions(+), 97 deletions(-) create mode 100644 ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift create mode 100644 ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift create mode 100644 ownCloud/SceneDelegate.swift create mode 100644 ownCloud/Window/OpenItemUserActivity.swift diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index ab3233937..aa327f411 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -37,11 +37,15 @@ 3913213822946E5E00EF88F4 /* FileListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913213722946E5E00EF88F4 /* FileListTableViewController.swift */; }; 3913214D22956D5700EF88F4 /* LibraryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3913214A22956D5700EF88F4 /* LibraryTableViewController.swift */; }; 394804DA225CBDBA00AA8183 /* BreadCrumbTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */; }; + 394E200C233E477F009D2897 /* OpenItemUserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */; }; 39607CBC2225D480007B386D /* UITableViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */; }; + 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3961281522F8730A0087BD3A /* SceneDelegate.swift */; }; 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */; }; 396BE4CA2289500E00B254A9 /* RoundedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396BE4C92289500E00B254A9 /* RoundedLabel.swift */; }; + 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */; }; 3971B48F221B23FE006FB441 /* ThemeableColoredView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */; }; 397328EF22D606AC006CFAA4 /* ImportFilesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */; }; + 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397754F22327A33500119FCB /* OpenSceneAction.swift */; }; 39878B7421FB1DE800DBF693 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */; }; 3998F5CC2240CD8300B66713 /* RoundedInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5CB2240CD8300B66713 /* RoundedInfoView.swift */; }; 3998F5D3224102FE00B66713 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3998F5D2224102FE00B66713 /* UITableView+Extension.swift */; }; @@ -615,11 +619,15 @@ 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 = ""; }; 394804D9225CBDBA00AA8183 /* BreadCrumbTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreadCrumbTableViewController.swift; sourceTree = ""; }; + 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenItemUserActivity.swift; sourceTree = ""; }; 39607CBB2225D480007B386D /* UITableViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Extension.swift"; sourceTree = ""; }; + 3961281522F8730A0087BD3A /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 396BE4C22288A84C00B254A9 /* PendingSharesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingSharesTableViewController.swift; sourceTree = ""; }; 396BE4C92289500E00B254A9 /* RoundedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedLabel.swift; sourceTree = ""; }; + 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscardSceneAction.swift; sourceTree = ""; }; 3971B48E221B23FE006FB441 /* ThemeableColoredView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeableColoredView.swift; sourceTree = ""; }; 397328EE22D606AC006CFAA4 /* ImportFilesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportFilesController.swift; sourceTree = ""; }; + 397754F22327A33500119FCB /* OpenSceneAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSceneAction.swift; sourceTree = ""; }; 39878B7321FB1DE800DBF693 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; 39880BAA233B5236006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 39880BB0233B524B006EA539 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1041,6 +1049,7 @@ isa = PBXGroup; children = ( 233BDE9F204FEFE500C06732 /* AppDelegate.swift */, + 3961281522F8730A0087BD3A /* SceneDelegate.swift */, 397328E822D6067B006CFAA4 /* Import */, DCF4F1612051925A00189B9A /* Bookmarks */, DC29F09122974F8000F77349 /* FileLists */, @@ -1049,6 +1058,7 @@ DC7DF17C205140F400189B9A /* Server List */, DCF4F1802051A91500189B9A /* Settings */, DC422448207CAED60006A2A6 /* Theming */, + 394E2005233E4765009D2897 /* Window */, DCF4F1622051927200189B9A /* UI Elements */, 239F1314205A69240029F186 /* UIKit Extensions */, DCE5E8B62080D8B8005F60CE /* SDK Extensions */, @@ -1185,6 +1195,14 @@ path = Library; sourceTree = ""; }; + 394E2005233E4765009D2897 /* Window */ = { + isa = PBXGroup; + children = ( + 394E200B233E477F009D2897 /* OpenItemUserActivity.swift */, + ); + path = Window; + sourceTree = ""; + }; 397328E822D6067B006CFAA4 /* Import */ = { isa = PBXGroup; children = ( @@ -1325,6 +1343,8 @@ 6E586CF52199A70100F680C4 /* Actions+Extensions */ = { isa = PBXGroup; children = ( + 397754F22327A33500119FCB /* OpenSceneAction.swift */, + 396D7C5F23224A53002380C1 /* DiscardSceneAction.swift */, 6E91F37D21ECA6FD009436D2 /* CopyAction.swift */, 6ED1B80A21A4004900E16C95 /* CreateFolderAction.swift */, 6E586CFF2199A78E00F680C4 /* DeleteAction.swift */, @@ -2513,6 +2533,7 @@ DC42244A207CAFAA0006A2A6 /* Theme.swift in Sources */, 4C464BF52187AF1500D30602 /* PDFThumbnailsCollectionViewController.swift in Sources */, 4C464BF02187AF1500D30602 /* PDFTocTableViewController.swift in Sources */, + 3961281622F8730A0087BD3A /* SceneDelegate.swift in Sources */, DC4FEAEA209E48E800D4476B /* DispatchQueueTools.swift in Sources */, 6ED1B80B21A4004900E16C95 /* CreateFolderAction.swift in Sources */, DC1B2708209CF0D3004715E1 /* IssuesPresentationAnimator.swift in Sources */, @@ -2550,6 +2571,7 @@ DC297965226E4D1100E01BC7 /* PushTransitionDelegate.swift in Sources */, 396BE4C32288A84C00B254A9 /* PendingSharesTableViewController.swift in Sources */, 233E0FD82099F11D00C3D8D5 /* SecuritySettingsSection.swift in Sources */, + 396D7C6523224A53002380C1 /* DiscardSceneAction.swift in Sources */, DCF4F18B2052BA4C00189B9A /* Log.swift in Sources */, DC8549382183B4CD00782BA8 /* ThemeStyle+Extensions.swift in Sources */, 4C82D07022C9387300835F0B /* MediaDisplayViewController.swift in Sources */, @@ -2604,10 +2626,12 @@ DC3BE0DE2077CC14002A0AC0 /* ClientQueryViewController.swift in Sources */, 4C1561EF22232357009C4EF3 /* PhotoSelectionViewCell.swift in Sources */, 23C56538212167BE00BD4B47 /* CardTransitionDelegate.swift in Sources */, + 397754F82327A33500119FCB /* OpenSceneAction.swift in Sources */, 4CAF783C2282FD40000C85CF /* FileManager+Extension.swift in Sources */, 6E3A104D219D6F0100F90C96 /* DuplicateAction.swift in Sources */, DC1B270A209CF0D3004715E1 /* ConnectionIssueViewController.swift in Sources */, DC0B37972051681600189B9A /* ThemeButton.swift in Sources */, + 394E200C233E477F009D2897 /* OpenItemUserActivity.swift in Sources */, DCF4F17B20519F9D00189B9A /* StaticTableViewSection.swift in Sources */, DC243BFF2317B446004FBB5C /* ThemeWindow.swift in Sources */, 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */, diff --git a/ownCloud/AppDelegate.swift b/ownCloud/AppDelegate.swift index 42186fc8b..bf3cef950 100644 --- a/ownCloud/AppDelegate.swift +++ b/ownCloud/AppDelegate.swift @@ -77,6 +77,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCExtensionManager.shared.addExtension(BackgroundFetchUpdateTaskAction.taskExtension) OCExtensionManager.shared.addExtension(InstantMediaUploadTaskExtension.taskExtension) + if #available(iOS 13.0, *), UIDevice.current.isIpad() { + OCExtensionManager.shared.addExtension(DiscardSceneAction.actionExtension) + OCExtensionManager.shared.addExtension(OpenSceneAction.actionExtension) + } Theme.shared.activeCollection = ThemeCollection(with: ThemeStyle.preferredStyle) @@ -126,4 +130,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { OCCoreManager.shared.handleEvents(forBackgroundURLSession: identifier, completionHandler: completionHandler) } + + // MARK: UISceneSession Lifecycle + @available(iOS 13.0, *) + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + @available(iOS 13.0, *) + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + } } diff --git a/ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift new file mode 100644 index 000000000..3bb90fdab --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/DiscardSceneAction.swift @@ -0,0 +1,60 @@ +// +// DiscardSceneAction.swift +// ownCloud +// +// Created by Matthias Hühne on 06.09.19. +// 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 UIKit +import ownCloudSDK +import MobileCoreServices + +@available(iOS 13.0, *) +class DiscardSceneAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.discardscene") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Close Window".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreFolder] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + + if UIDevice.current.isIpad() { + if let viewController = forContext.viewController, viewController.view.window?.windowScene?.userActivity != nil { + return .first + } + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard let viewController = context.viewController else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + if UIDevice.current.isIpad() { + if let scene = viewController.view.window?.windowScene { + UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil) { (_) in + } + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(systemName: "xmark.square")?.tinted(with: Theme.shared.activeCollection.tintColor) + } +} diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift index 9aaa2f340..c5eb7aad8 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenInAction.swift @@ -92,7 +92,7 @@ class OpenInAction: Action { } override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { - if location == .moreItem { + if location == .moreItem || location == .moreFolder { return UIImage(named: "open-in") } diff --git a/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift b/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift new file mode 100644 index 000000000..d865d8d49 --- /dev/null +++ b/ownCloud/Client/Actions/Actions+Extensions/OpenSceneAction.swift @@ -0,0 +1,60 @@ +// +// OpenSceneAction.swift +// ownCloud +// +// Created by Matthias Hühne on 10.09.19. +// 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 UIKit +import ownCloudSDK +import MobileCoreServices + +@available(iOS 13.0, *) +class OpenSceneAction: Action { + override class var identifier : OCExtensionIdentifier? { return OCExtensionIdentifier("com.owncloud.action.openscene") } + override class var category : ActionCategory? { return .normal } + override class var name : String { return "Open in a new Window".localized } + override class var locations : [OCExtensionLocationIdentifier]? { return [.moreItem] } + + // MARK: - Extension matching + override class func applicablePosition(forContext: ActionContext) -> ActionPosition { + + if UIDevice.current.isIpad() { + if forContext.items.count == 1 { + return .first + } + } + + return .none + } + + // MARK: - Action implementation + override func run() { + guard let viewController = context.viewController else { + self.completed(with: NSError(ocError: .insufficientParameters)) + return + } + + if UIDevice.current.isIpad() { + if context.items.count == 1, let item = context.items.first, let tabBarController = viewController.tabBarController as? ClientRootViewController { + let activity = OpenItemUserActivity(detailItem: item, detailBookmark: tabBarController.bookmark) + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity.openItemUserActivity, options: nil) + } + } + } + + override class func iconForLocation(_ location: OCExtensionLocationIdentifier) -> UIImage? { + return UIImage(systemName: "uiwindow.split.2x1")?.tinted(with: Theme.shared.activeCollection.tintColor) + } +} diff --git a/ownCloud/Client/ClientQueryViewController.swift b/ownCloud/Client/ClientQueryViewController.swift index f08ed0b01..7bdb2d7d4 100644 --- a/ownCloud/Client/ClientQueryViewController.swift +++ b/ownCloud/Client/ClientQueryViewController.swift @@ -35,6 +35,11 @@ extension OCQueryState { } } +struct OCItemDraggingValue { + var item : OCItem + var bookmarkUUID : String +} + class ClientQueryViewController: QueryFileListTableViewController, UIDropInteractionDelegate, UIPopoverPresentationControllerDelegate { var selectedItemIds = Set() @@ -220,6 +225,8 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac for item in session.items { if item.localObject == nil, item.itemProvider.hasItemConformingToTypeIdentifier("public.folder") { return false + } else if let itemValues = item.localObject as? OCItemDraggingValue, let core = self.core, core.bookmark.uuid.uuidString != itemValues.bookmarkUUID, itemValues.item.type == .collection { + return false } } return true @@ -259,11 +266,14 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac } } - func updateToolbarItemsForDropping(_ items: [OCItem]) { + func updateToolbarItemsForDropping(_ draggingValues: [OCItemDraggingValue]) { guard let tabBarController = self.tabBarController as? ClientRootViewController else { return } guard let toolbarItems = tabBarController.toolbar?.items else { return } if let core = self.core { + let items = draggingValues.map({(value: OCItemDraggingValue) -> OCItem in + return value.item + }) // Remove duplicates let uniqueItems = Array(Set(items)) // Get possible associated actions @@ -622,6 +632,12 @@ class ClientQueryViewController: QueryFileListTableViewController, UIDropInterac } } +extension OCBookmarkManager { + public func bookmark(for uuidString: String) -> OCBookmark? { + return OCBookmarkManager.shared.bookmarks.filter({ $0.uuid.uuidString == uuidString}).first + } +} + // MARK: - Drag & Drop delegates extension ClientQueryViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { @@ -629,11 +645,13 @@ extension ClientQueryViewController: UITableViewDropDelegate { for item in coordinator.items { if item.dragItem.localObject != nil { + var destinationItem: OCItem - guard let item = item.dragItem.localObject as? OCItem, let itemName = item.name else { + guard let itemValues = item.dragItem.localObject as? OCItemDraggingValue, let itemName = itemValues.item.name, let sourceBookmark = OCBookmarkManager.shared.bookmark(for: itemValues.bookmarkUUID) else { return } + let item = itemValues.item if coordinator.proposal.intent == .insertIntoDestinationIndexPath { @@ -663,13 +681,32 @@ extension ClientQueryViewController: UITableViewDropDelegate { } - if let progress = core.move(item, to: destinationItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in - if error != nil { - Log.log("Error \(String(describing: error)) moving \(String(describing: item.path))") + // Move Items in the same Account + if core.bookmark.uuid.uuidString == itemValues.bookmarkUUID { + if let progress = core.move(item, to: destinationItem, withName: itemName, options: nil, resultHandler: { (error, _, _, _) in + if error != nil { + Log.log("Error \(String(describing: error)) moving \(String(describing: item.path))") + } + }) { + self.progressSummarizer?.startTracking(progress: progress) + } + // Copy Items between Accounts + } else { + 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) { (error, _, _, _) in + if error == nil { + + } + } + } + }) + } } - }) { - self.progressSummarizer?.startTracking(progress: progress) } + // Import Items from outside } else { guard let UTI = item.dragItem.itemProvider.registeredTypeIdentifiers.last else { return } item.dragItem.itemProvider.loadFileRepresentation(forTypeIdentifier: UTI) { (url, _ error) in @@ -689,28 +726,31 @@ extension ClientQueryViewController: UITableViewDragDelegate { self.populateToolbar() } - var selectedItems = [OCItem]() + var selectedItems = [OCItemDraggingValue]() // Add Items from Multiselection too if let selectedIndexPaths = self.tableView.indexPathsForSelectedRows { if selectedIndexPaths.count > 0 { for indexPath in selectedIndexPaths { - if let selectedItem : OCItem = itemAt(indexPath: indexPath) { - selectedItems.append(selectedItem) + if let selectedItem : OCItem = itemAt(indexPath: indexPath), let uuid = core?.bookmark.uuid.uuidString { + let draggingValue = OCItemDraggingValue(item: selectedItem, bookmarkUUID: uuid) + selectedItems.append(draggingValue) } } } } for dragItem in session.items { - guard let item = dragItem.localObject as? OCItem else { continue } - selectedItems.append(item) + guard let item = dragItem.localObject as? OCItem, let uuid = core?.bookmark.uuid.uuidString else { continue } + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) } - if let item: OCItem = itemAt(indexPath: indexPath) { - selectedItems.append(item) + if let item: OCItem = itemAt(indexPath: indexPath), let uuid = core?.bookmark.uuid.uuidString { + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) updateToolbarItemsForDropping(selectedItems) - guard let dragItem = itemForDragging(item: item) else { return [] } + guard let dragItem = itemForDragging(draggingValue: draggingValue) else { return [] } return [dragItem] } @@ -718,32 +758,35 @@ extension ClientQueryViewController: UITableViewDragDelegate { } func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] { - var selectedItems = [OCItem]() + var selectedItems = [OCItemDraggingValue]() for dragItem in session.items { - guard let item = dragItem.localObject as? OCItem else { continue } - selectedItems.append(item) + guard let item = dragItem.localObject as? OCItem, let uuid = core?.bookmark.uuid.uuidString else { continue } + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) } - if let item: OCItem = itemAt(indexPath: indexPath) { - selectedItems.append(item) + if let item: OCItem = itemAt(indexPath: indexPath), let uuid = core?.bookmark.uuid.uuidString { + let draggingValue = OCItemDraggingValue(item: item, bookmarkUUID: uuid) + selectedItems.append(draggingValue) updateToolbarItemsForDropping(selectedItems) - guard let dragItem = itemForDragging(item: item) else { return [] } + guard let dragItem = itemForDragging(draggingValue: draggingValue) else { return [] } return [dragItem] } return [] } - func itemForDragging(item : OCItem) -> UIDragItem? { + func itemForDragging(draggingValue : OCItemDraggingValue) -> UIDragItem? { + let item = draggingValue.item if let core = self.core { switch item.type { case .collection: guard let data = item.serializedData() else { return nil } let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + dragItem.localObject = draggingValue return dragItem case .file: guard let itemMimeType = item.mimeType else { return nil } @@ -756,13 +799,13 @@ extension ClientQueryViewController: UITableViewDragDelegate { let itemProvider = NSItemProvider(item: fileData, typeIdentifier: rawUtiString) itemProvider.suggestedName = item.name let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + dragItem.localObject = draggingValue return dragItem } else { guard let data = item.serializedData() else { return nil } let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypeData as String) let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item + dragItem.localObject = draggingValue return dragItem } } diff --git a/ownCloud/Client/ClientRootViewController.swift b/ownCloud/Client/ClientRootViewController.swift index 55961cf79..16f4c2786 100644 --- a/ownCloud/Client/ClientRootViewController.swift +++ b/ownCloud/Client/ClientRootViewController.swift @@ -137,7 +137,7 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega } // MARK: - Startup - func afterCoreStart(_ completionHandler: @escaping (() -> Void)) { + func afterCoreStart(_ lastVisibleItemId: String?, completionHandler: @escaping (() -> Void)) { OCCoreManager.shared.requestCore(for: bookmark, setup: { (core, _) in self.core = core core?.delegate = self @@ -146,7 +146,7 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega core?.vault.keyValueStore?.storeObject(nil, forKey: .coreSkipAvailableOfflineKey) }, completionHandler: { (core, error) in if error == nil { - self.coreReady() + self.coreReady(lastVisibleItemId) } // Start showing connection status with a delay of 1 second, so "Offline" doesn't flash briefly @@ -246,14 +246,17 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega }) } - func coreReady() { + func coreReady(_ lastVisibleItemId: String?) { OnMainThread { if let core = self.core { - let query = OCQuery(forPath: "/") - - let queryViewController = ClientQueryViewController(core: core, query: query) - // Because we have nested UINavigationControllers (first one from ServerListTableViewController and each item UITabBarController needs it own UINavigationController), we have to fake the UINavigationController logic. Here we insert the emptyViewController, because in the UI should appear a "Back" button if the root of the queryViewController is shown. Therefore we put at first the emptyViewController inside and at the same time the queryViewController. Now, the back button is shown and if the users push the "Back" button the ServerListTableViewController is shown. This logic can be found in navigationController(_: UINavigationController, willShow: UIViewController, animated: Bool) below. - self.filesNavigationController?.setViewControllers([self.emptyViewController, queryViewController], animated: false) + if let localItemId = lastVisibleItemId { + self.createFileListStack(for: localItemId) + } else { + let query = OCQuery(forPath: "/") + let queryViewController = ClientQueryViewController(core: core, query: query) + // Because we have nested UINavigationControllers (first one from ServerListTableViewController and each item UITabBarController needs it own UINavigationController), we have to fake the UINavigationController logic. Here we insert the emptyViewController, because in the UI should appear a "Back" button if the root of the queryViewController is shown. Therefore we put at first the emptyViewController inside and at the same time the queryViewController. Now, the back button is shown and if the users push the "Back" button the ServerListTableViewController is shown. This logic can be found in navigationController(_: UINavigationController, willShow: UIViewController, animated: Bool) below. + self.filesNavigationController?.setViewControllers([self.emptyViewController, queryViewController], animated: false) + } let emptyViewController = self.emptyViewController emptyViewController.navigationItem.title = "Accounts".localized @@ -308,6 +311,45 @@ class ClientRootViewController: UITabBarController, UINavigationControllerDelega self.progressBar?.setNeedsLayout() // self.view.setNeedsLayout() } + + func createFileListStack(for itemLocalID: String) { + if let core = core { + // retrieve the item for the item id + core.retrieveItemFromDatabase(forLocalID: itemLocalID, completionHandler: { (error, _, item) in + if error == nil, let item = item { + OnMainThread { + // get all parent items for the item and rebuild all underlaying ClientQueryViewController for this items in the navigation stack + let parentItems = core.retrieveParentItems(for: item) + let query = OCQuery(forPath: "/") + let queryViewController = ClientQueryViewController(core: core, query: query) + + var subController = queryViewController + var newViewControllersStack : [UIViewController] = [] + for item in parentItems { + if let controller = self.open(item: item, in: subController) { + subController = controller + newViewControllersStack.append(controller) + } + } + + newViewControllersStack.insert(self.emptyViewController, at: 0) + self.filesNavigationController?.setViewControllers(newViewControllersStack, animated: false) + + // open the controller for the item + subController.open(item: item, animated: false) + } + } + }) + } + } + + func open(item: OCItem, in controller: ClientQueryViewController) -> ClientQueryViewController? { + if let subController = controller.open(item: item, animated: false, pushViewController: false) { + return subController + } + + return nil + } } extension ClientRootViewController : Themeable { diff --git a/ownCloud/FileLists/FileListTableViewController.swift b/ownCloud/FileLists/FileListTableViewController.swift index 1a280510a..298317f78 100644 --- a/ownCloud/FileLists/FileListTableViewController.swift +++ b/ownCloud/FileLists/FileListTableViewController.swift @@ -222,26 +222,43 @@ class FileListTableViewController: UITableViewController, ClientItemCellDelegate return } - if let core = self.core { - switch rowItem.type { - case .collection: - if let path = rowItem.path { - self.navigationController?.pushViewController(ClientQueryViewController(core: core, query: OCQuery(forPath: path)), animated: true) - } - - case .file: - guard let query = self.query(forItem: rowItem) else { - return - } + open(item: rowItem, animated: true) + } + } - let itemViewController = DisplayHostViewController(core: core, selectedItem: rowItem, query: query) - itemViewController.hidesBottomBarWhenPushed = true - itemViewController.progressSummarizer = self.progressSummarizer - self.navigationController?.pushViewController(itemViewController, animated: true) + func open(item: OCItem, animated: Bool, pushViewController: Bool = true) -> ClientQueryViewController? { + if let core = self.core { + if #available(iOS 13.0, *) { + if let tabBarController = self.tabBarController as? ClientRootViewController { + let activity = OpenItemUserActivity(detailItem: item, detailBookmark: tabBarController.bookmark) + view.window?.windowScene?.userActivity = activity.openItemUserActivity } } + switch item.type { + case .collection: + if let path = item.path { + let clientQueryViewController = ClientQueryViewController(core: core, query: OCQuery(forPath: path)) + if pushViewController { + self.navigationController?.pushViewController(clientQueryViewController, animated: animated) + } + + return clientQueryViewController + } + + case .file: + guard let query = self.query(forItem: item) else { + return nil + } + + let itemViewController = DisplayHostViewController(core: core, selectedItem: item, query: query) + itemViewController.hidesBottomBarWhenPushed = true + itemViewController.progressSummarizer = self.progressSummarizer + self.navigationController?.pushViewController(itemViewController, animated: animated) + } } + + return nil } // MARK: - Themable diff --git a/ownCloud/FileLists/QueryFileListTableViewController.swift b/ownCloud/FileLists/QueryFileListTableViewController.swift index 950f4ffd3..ee848507a 100644 --- a/ownCloud/FileLists/QueryFileListTableViewController.swift +++ b/ownCloud/FileLists/QueryFileListTableViewController.swift @@ -381,4 +381,33 @@ class QueryFileListTableViewController: FileListTableViewController, SortBarDele return 0 } + + @available(iOS 13.0, *) + override func tableView(_ tableView: UITableView, + contextMenuConfigurationForRowAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + if let item = itemAt(indexPath: indexPath), UIDevice.current.isIpad() { + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in + return self.makeContextMenu(for: indexPath, with: item) + }) + } + + return nil + } + + @available(iOS 13.0, *) + func makeContextMenu(for indexPath: IndexPath, with item: OCItem) -> UIMenu { + let openWindow = UIAction(title: "Open in a new Window".localized, image: UIImage(systemName: "uiwindow.split.2x1")) { _ in + self.openItemInWindow(at: indexPath) + } + return UIMenu(title: item.name ?? "", children: [openWindow]) + } + + @available(iOS 13.0, *) + func openItemInWindow(at indexPath: IndexPath) { + if let item = itemAt(indexPath: indexPath), let tabBarController = self.tabBarController as? ClientRootViewController { + let activity = OpenItemUserActivity(detailItem: item, detailBookmark: tabBarController.bookmark) + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity.openItemUserActivity, options: nil) + } + } } diff --git a/ownCloud/Resources/Info.plist b/ownCloud/Resources/Info.plist index 70227cdba..af8dd49ae 100644 --- a/ownCloud/Resources/Info.plist +++ b/ownCloud/Resources/Info.plist @@ -23,8 +23,6 @@ - LSSupportsOpeningDocumentsInPlace - CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -47,6 +45,8 @@ LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -60,6 +60,11 @@ This permission is needed to upload photos and videos from your photo library. NSPhotoLibraryUsageDescription This permission is needed to upload photos and videos from your photo library. + NSUserActivityTypes + + com.owncloud.ios-app.openAccount + com.owncloud.ios-app.openItem + OCAppGroupIdentifier group.$(PRODUCT_BUNDLE_IDENTIFIER) OCAppIdentifierPrefix @@ -68,6 +73,23 @@ OCKeychainAccessGroupIdentifier group.$(PRODUCT_BUNDLE_IDENTIFIER) + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_NAME).SceneDelegate + + + + UIBackgroundModes audio diff --git a/ownCloud/Resources/en.lproj/Localizable.strings b/ownCloud/Resources/en.lproj/Localizable.strings index 9ab0f5779..129d660c9 100644 --- a/ownCloud/Resources/en.lproj/Localizable.strings +++ b/ownCloud/Resources/en.lproj/Localizable.strings @@ -273,6 +273,9 @@ " couldn't download file(s)" = " couldn't download file(s)"; "Actions" = "Actions"; "copy" = "copy"; +"Close Window" = "Close Window"; +"Open in a new Window" = "Open in a new Window"; +"Open in Window" = "Open in Window"; "Preparing…" = "Preparing…"; diff --git a/ownCloud/SDK Extensions/OCCore+Extension.swift b/ownCloud/SDK Extensions/OCCore+Extension.swift index fa61ca2a2..2a13c2eab 100644 --- a/ownCloud/SDK Extensions/OCCore+Extension.swift +++ b/ownCloud/SDK Extensions/OCCore+Extension.swift @@ -126,4 +126,20 @@ extension OCCore { return nil } + + func retrieveParentItems(for item: OCItem) -> [OCItem] { + var parentItems : [OCItem] = [] + + if item.parentLocalID != nil { + if let parentItem = item.parentItem(from: self) { + if item.parentLocalID != nil { + parentItems.append(parentItem) + let items = self.retrieveParentItems(for: parentItem) + parentItems.append(contentsOf: items.reversed()) + } + } + } + + return parentItems.reversed() + } } diff --git a/ownCloud/SDK Extensions/OCItem+Extension.swift b/ownCloud/SDK Extensions/OCItem+Extension.swift index f731eefc0..96ddb099d 100644 --- a/ownCloud/SDK Extensions/OCItem+Extension.swift +++ b/ownCloud/SDK Extensions/OCItem+Extension.swift @@ -19,6 +19,10 @@ import UIKit import ownCloudSDK +let ownCloudItemDetailActivityType = "com.owncloud.ios-app.itemDetail" +let ownCloudItemDetailPath = "itemDetail" +let ownCloudItemDetailItemUuidKey = "itemUuid" + extension OCItem { static private let iconNamesByMIMEType : [String:String] = { var mimeTypeToIconMap : [String:String] = [ diff --git a/ownCloud/SceneDelegate.swift b/ownCloud/SceneDelegate.swift new file mode 100644 index 000000000..b1eee745f --- /dev/null +++ b/ownCloud/SceneDelegate.swift @@ -0,0 +1,71 @@ +// +// SceneDelegate.swift +// ownCloud +// +// Created by Matthias Hühne on 08/05/2018. +// Copyright © 2019 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 + +@available(iOS 13.0, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + // UIWindowScene delegate + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + if let windowScene = scene as? UIWindowScene { + window = UIWindow(windowScene: windowScene) + let serverListTableViewController = ServerListTableViewController(style: UITableView.Style.plain) + serverListTableViewController.restorationIdentifier = "ServerListTableViewController" + let navigationController = ThemeNavigationController(rootViewController: serverListTableViewController) + window?.rootViewController = navigationController + window?.addSubview((navigationController.view)!) + window?.makeKeyAndVisible() + } + + if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { + configure(window: window, with: userActivity) + } + } + + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + return scene.userActivity + } + + func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool { + guard let bookmarkUUIDString = activity.userInfo?[ownCloudOpenAccountAccountUuidKey] as? String, let bookmarkUUID = UUID(uuidString: bookmarkUUIDString), let bookmark = OCBookmarkManager.shared.bookmark(for: bookmarkUUID), let navigationController = window?.rootViewController as? ThemeNavigationController, let serverListController = navigationController.topViewController as? ServerListTableViewController else { + return false + } + + if activity.title == ownCloudOpenAccountPath { + serverListController.connect(to: bookmark, lastVisibleItemId: nil, animated: false) + window?.windowScene?.userActivity = bookmark.openAccountUserActivity + + return true + } else if activity.title == ownCloudOpenItemPath { + guard let itemLocalID = activity.userInfo?[ownCloudOpenItemUuidKey] as? String else { + return false + } + + // At first connect to the bookmark for the item + serverListController.connect(to: bookmark, lastVisibleItemId: itemLocalID, animated: false) + window?.windowScene?.userActivity = activity + + return true + } + + return false + } +} diff --git a/ownCloud/Server List/ServerListTableViewController.swift b/ownCloud/Server List/ServerListTableViewController.swift index 76a5b0ac8..fdc3d3650 100644 --- a/ownCloud/Server List/ServerListTableViewController.swift +++ b/ownCloud/Server List/ServerListTableViewController.swift @@ -78,6 +78,7 @@ class ServerListTableViewController: UITableViewController, Themeable { self.tableView.rowHeight = UITableView.automaticDimension self.tableView.estimatedRowHeight = 80 self.tableView.allowsSelectionDuringEditing = true + self.tableView.dragDelegate = self extendedLayoutIncludesOpaqueBars = true let addServerBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.add, target: self, action: #selector(addBookmark)) @@ -140,9 +141,11 @@ class ServerListTableViewController: UITableViewController, Themeable { if shownFirstTime, UIApplication.shared.applicationState != .background { shownFirstTime = false - if let bookmark = OCBookmarkManager.lastBookmarkSelectedForConnection { - connect(to: bookmark) - return true + if #available(iOS 13.0, *) { /* this will be handled automatically by scene restoration */ } else { + if let bookmark = OCBookmarkManager.lastBookmarkSelectedForConnection { + connect(to: bookmark, lastVisibleItemId: nil, animated: true) + return true + } } } @@ -271,7 +274,7 @@ class ServerListTableViewController: UITableViewController, Themeable { if attemptLoginOnSuccess { bookmarkViewController.userActionCompletionHandler = { [weak self] (bookmark, success) in if success, let bookmark = bookmark, let self = self { - self.connect(to: bookmark) + self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) } } } @@ -298,6 +301,62 @@ class ServerListTableViewController: UITableViewController, Themeable { self.present(navigationController, animated: true, completion: nil) } + @available(iOS 13.0, *) + func openAccountInWindow(at indexPath: IndexPath) { + if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { + let activity = bookmark.openAccountUserActivity + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: activity, options: nil) + } + } + + @available(iOS 13.0, *) + func dismissWindow() { + if let scene = view.window?.windowScene { + UIApplication.shared.requestSceneSessionDestruction(scene.session, options: nil) { (_) in + } + } + } + + func delete(bookmark: OCBookmark, at indexPath: IndexPath) { + OCBookmarkManager.lock(bookmark: bookmark) + + OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in + let vault : OCVault = OCVault(bookmark: bookmark) + + vault.erase(completionHandler: { (_, error) in + OnMainThread { + if error != nil { + // Inform user if vault couldn't be erased + let alertController = ThemedAlertController(title: NSString(format: "Deletion of '%@' failed".localized as NSString, bookmark.shortName as NSString) as String, + message: error?.localizedDescription, + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) + + self.present(alertController, animated: true, completion: nil) + } else { + // Success! We can now remove the bookmark + self.ignoreServerListChanges = true + + OCBookmarkManager.shared.removeBookmark(bookmark) + + self.tableView.performBatchUpdates({ + self.tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) + }, completion: { (_) in + self.ignoreServerListChanges = false + }) + + self.updateNoServerMessageVisibility() + } + + OCBookmarkManager.unlock(bookmark: bookmark) + + completionHandler() + } + }) + }, for: bookmark) + } + var themeCounter : Int = 0 @IBAction func help() { @@ -333,7 +392,7 @@ class ServerListTableViewController: UITableViewController, Themeable { return OCBookmarkManager.isLocked(bookmark: bookmark, presentAlertOn: presentAlert ? self : nil) } - func connect(to bookmark: OCBookmark) { + func connect(to bookmark: OCBookmark, lastVisibleItemId: String?, animated: Bool) { if isLocked(bookmark: bookmark) { return } @@ -356,6 +415,9 @@ class ServerListTableViewController: UITableViewController, Themeable { activityIndicator.startAnimating() } + if #available(iOS 13.0, *) { + view.window?.windowScene?.userActivity = bookmark.openAccountUserActivity + } self.setLastSelectedBookmark(bookmark, openedBlock: { activityIndicator.stopAnimating() bookmarkRow?.accessoryView = bookmarkRowAccessoryView @@ -364,7 +426,7 @@ class ServerListTableViewController: UITableViewController, Themeable { clientRootViewController.authDelegate = self clientRootViewController.modalPresentationStyle = .fullScreen - clientRootViewController.afterCoreStart { + clientRootViewController.afterCoreStart(lastVisibleItemId) { // Make sure only the UI for the last selected bookmark is actually presented (in case of other bookmarks facing a huge delay and users selecting another bookmark in the meantime) if self.lastSelectedBookmark?.uuid == bookmark.uuid { OCBookmarkManager.lastBookmarkSelectedForConnection = bookmark @@ -377,7 +439,7 @@ class ServerListTableViewController: UITableViewController, Themeable { clientRootViewController.transitioningDelegate = transitionDelegate clientRootViewController.modalPresentationStyle = .custom - navigationController.present(clientRootViewController, animated: true, completion: { + navigationController.present(clientRootViewController, animated: animated, completion: { self.resetPreviousBookmarkSelection(bookmark) }) } @@ -416,13 +478,53 @@ class ServerListTableViewController: UITableViewController, Themeable { if tableView.isEditing { self.showBookmarkUI(edit: bookmark) } else { - self.connect(to: bookmark) + self.connect(to: bookmark, lastVisibleItemId: nil, animated: true) + self.tableView.deselectRow(at: indexPath, animated: true) } self.tableView.deselectRow(at: indexPath, animated: true) } } + @available(iOS 13.0, *) + override func tableView(_ tableView: UITableView, + contextMenuConfigurationForRowAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in + return self.makeContextMenu(for: indexPath, with: bookmark) + }) + } + + return nil + } + + @available(iOS 13.0, *) + func makeContextMenu(for indexPath: IndexPath, with bookmark: OCBookmark) -> UIMenu { + var menuItems : [UIAction] = [] + + if UIDevice.current.isIpad() { + let openWindow = UIAction(title: "Open in a new Window".localized, image: UIImage(systemName: "uiwindow.split.2x1")) { _ in + self.openAccountInWindow(at: indexPath) + } + menuItems.append(openWindow) + } + let edit = UIAction(title: "Edit", image: UIImage(systemName: "gear")) { _ in + self.showBookmarkUI(edit: bookmark) + } + menuItems.append(edit) + let manage = UIAction(title: "Manage", image: UIImage(systemName: "arrow.3.trianglepath")) { _ in + self.showBookmarkInfoUI(bookmark) + } + menuItems.append(manage) + let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in + self.delete(bookmark: bookmark, at: indexPath) + } + menuItems.append(delete) + + return UIMenu(title: bookmark.shortName, children: menuItems) + } + // MARK: - Table view data source func indexPath(for bookmark: OCBookmark) -> IndexPath? { var index = 0 @@ -476,43 +578,7 @@ class ServerListTableViewController: UITableViewController, Themeable { alertController.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) alertController.addAction(UIAlertAction(title: "Delete".localized, style: .destructive, handler: { (_) in - OCBookmarkManager.lock(bookmark: bookmark) - - OCCoreManager.shared.scheduleOfflineOperation({ (bookmark, completionHandler) in - let vault : OCVault = OCVault(bookmark: bookmark) - - vault.erase(completionHandler: { (_, error) in - OnMainThread { - if error != nil { - // Inform user if vault couldn't be erased - let alertController = ThemedAlertController(title: NSString(format: "Deletion of '%@' failed".localized as NSString, bookmark.shortName as NSString) as String, - message: error?.localizedDescription, - preferredStyle: .alert) - - alertController.addAction(UIAlertAction(title: "OK".localized, style: .default, handler: nil)) - - self.present(alertController, animated: true, completion: nil) - } else { - // Success! We can now remove the bookmark - self.ignoreServerListChanges = true - - OCBookmarkManager.shared.removeBookmark(bookmark) - - tableView.performBatchUpdates({ - tableView.deleteRows(at: [indexPath], with: UITableView.RowAnimation.fade) - }, completion: { (_) in - self.ignoreServerListChanges = false - }) - - self.updateNoServerMessageVisibility() - } - - OCBookmarkManager.unlock(bookmark: bookmark) - - completionHandler() - } - }) - }, for: bookmark) + self.delete(bookmark: bookmark, at: indexPath) })) self.present(alertController, animated: true, completion: nil) @@ -534,6 +600,17 @@ class ServerListTableViewController: UITableViewController, Themeable { } }) + if #available(iOS 13.0, *), UIDevice.current.isIpad() { + let openAccountAction = UITableViewRowAction(style: .normal, + title: "Open in Window".localized, + handler: { (_, indexPath) in + self.openAccountInWindow(at: indexPath) + }) + openAccountAction.backgroundColor = .orange + + return [deleteRowAction, editRowAction, manageRowAction, openAccountAction] + } + return [deleteRowAction, editRowAction, manageRowAction] } @@ -612,3 +689,35 @@ extension ServerListTableViewController : ClientRootViewControllerAuthentication }) } } + +let ownCloudOpenAccountActivityType = "com.owncloud.ios-app.openAccount" +let ownCloudOpenAccountPath = "openAccount" +let ownCloudOpenAccountAccountUuidKey = "accountUuid" + +extension OCBookmark { + var openAccountUserActivity: NSUserActivity { + let userActivity = NSUserActivity(activityType: ownCloudOpenAccountActivityType) + userActivity.title = ownCloudOpenAccountPath + userActivity.userInfo = [ownCloudOpenAccountAccountUuidKey: uuid.uuidString] + return userActivity + } +} + +extension ServerListTableViewController: UITableViewDragDelegate { + + func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + if let bookmark = OCBookmarkManager.shared.bookmark(at: UInt(indexPath.row)) { + let userActivity = bookmark.openAccountUserActivity + let itemProvider = NSItemProvider(item: bookmark, typeIdentifier: "com.owncloud.ios-app.ocbookmark") + itemProvider.registerObject(userActivity, visibility: .all) + + let dragItem = UIDragItem(itemProvider: itemProvider) + dragItem.localObject = bookmark + + return [dragItem] + } + + return [] + } + +} diff --git a/ownCloud/Tools/VendorServices.swift b/ownCloud/Tools/VendorServices.swift index b34e451b4..e64263afc 100644 --- a/ownCloud/Tools/VendorServices.swift +++ b/ownCloud/Tools/VendorServices.swift @@ -146,7 +146,7 @@ extension VendorServices : OCClassSettingsSupport { static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? { if identifier == .app { - return [ .isBetaBuild : false, .showBetaWarning : false, .enableUIAnimations: true ] + return [ .isBetaBuild : true, .showBetaWarning : true, .enableUIAnimations: true ] } return nil diff --git a/ownCloud/Window/OpenItemUserActivity.swift b/ownCloud/Window/OpenItemUserActivity.swift new file mode 100644 index 000000000..cf162db47 --- /dev/null +++ b/ownCloud/Window/OpenItemUserActivity.swift @@ -0,0 +1,42 @@ +// +// OpenItemUserActivity.swift +// ownCloud +// +// Created by Matthias Hühne on 27.09.19. +// 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 UIKit +import ownCloudSDK + +let ownCloudOpenItemActivityType = "com.owncloud.ios-app.openItem" +let ownCloudOpenItemPath = "openItem" +let ownCloudOpenItemUuidKey = "itemUuid" + +class OpenItemUserActivity : NSObject { + + var item : OCItem + var bookmark : OCBookmark + + var openItemUserActivity: NSUserActivity { + let userActivity = NSUserActivity(activityType: ownCloudOpenItemActivityType) + userActivity.title = ownCloudOpenItemPath + userActivity.userInfo = [ownCloudOpenItemUuidKey: item.localID, ownCloudOpenAccountAccountUuidKey : bookmark.uuid.uuidString] + return userActivity + } + + init(detailItem: OCItem, detailBookmark: OCBookmark) { + item = detailItem + bookmark = detailBookmark + } +}