diff --git a/Action Assistant/ActionViewController.swift b/Action Assistant/ActionViewController.swift new file mode 100644 index 0000000000..8109425769 --- /dev/null +++ b/Action Assistant/ActionViewController.swift @@ -0,0 +1,148 @@ +// +// ActionViewController.swift +// Action Assistant +// +// Created by Marino Faggiana on 14/05/2026. +// Copyright © 2026 Marino Faggiana. All rights reserved. +// + +import UIKit +import NextcloudKit +import UniformTypeIdentifiers + +final class ActionViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + + view.isHidden = true + view.alpha = 0 + view.backgroundColor = .clear + preferredContentSize = .zero + + Task { + await handleAction() + } + } + + private func handleAction() async { + guard let text = await loadText() else { + extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + return + } + + NCAssistantSharedTextStore.save(text) + openMainAppForAssistantSharedText() + } + + private func loadText() async -> String? { + guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else { + return nil + } + + for extensionItem in extensionItems { + guard let attachments = extensionItem.attachments else { + continue + } + + for provider in attachments { + if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { + return await loadText(from: provider, typeIdentifier: UTType.plainText.identifier) + } + + if provider.hasItemConformingToTypeIdentifier(UTType.utf8PlainText.identifier) { + return await loadText(from: provider, typeIdentifier: UTType.utf8PlainText.identifier) + } + + if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { + return await loadText(from: provider, typeIdentifier: UTType.text.identifier) + } + } + } + + return nil + } + + private func loadText(from provider: NSItemProvider, typeIdentifier: String) async -> String? { + await withCheckedContinuation { continuation in + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in + let text: String? + + if let string = item as? String { + text = string + } else if let attributedString = item as? NSAttributedString { + text = attributedString.string + } else if let data = item as? Data { + text = String(data: data, encoding: .utf8) + } else if let url = item as? URL { + text = try? String(contentsOf: url, encoding: .utf8) + } else { + text = nil + } + + guard let text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + continuation.resume(returning: nil) + return + } + + continuation.resume(returning: text) + } + } + } + + private func openMainAppForAssistantSharedText() { + guard let url = URL(string: "nextcloud://assistant/shared-text") else { + extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + return + } + + openAssistantSharedTextURLThroughResponderChain(url) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } + } + + /// Opens the Assistant shared-text deep link from the Share extension. + /// + /// Share extensions cannot use `UIApplication.shared` directly because it is not + /// extension-safe. This method walks the responder chain until it finds the hidden + /// `UIApplication` responder and invokes the modern `open(_:options:completionHandler:)` + /// Objective-C selector dynamically. + /// + /// This is intentionally isolated because it relies on Objective-C runtime dispatch. + /// + /// - Parameter url: Deep link URL to open in the containing application. + private func openAssistantSharedTextURLThroughResponderChain(_ url: URL) { + let selector = NSSelectorFromString("openURL:options:completionHandler:") + let applicationClass: AnyClass? = NSClassFromString("UIApplication") + var responder: UIResponder? = self + + while let currentResponder = responder { + guard let applicationClass, + currentResponder.isKind(of: applicationClass), + currentResponder.responds(to: selector), + let implementation = currentResponder.method(for: selector) else { + responder = currentResponder.next + continue + } + + typealias CompletionBlock = @convention(block) (Bool) -> Void + typealias OpenURLFunction = @convention(c) (AnyObject, Selector, NSURL, NSDictionary, CompletionBlock?) -> Void + + let openURL = unsafeBitCast(implementation, to: OpenURLFunction.self) + + let completion: CompletionBlock = { success in + if success { + nkLog(debug: "Assistant shared text deep link performed through modern responder chain") + } else { + nkLog(error: "Assistant shared text deep link modern responder chain returned false") + } + } + + openURL(currentResponder, selector, url as NSURL, NSDictionary(), completion) + return + } + + nkLog(error: "Assistant shared text deep link failed because no UIApplication responder can open URL") + } +} diff --git a/Action Assistant/Base.lproj/MainInterface.storyboard b/Action Assistant/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000000..816db3336e --- /dev/null +++ b/Action Assistant/Base.lproj/MainInterface.storyboard @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Action Assistant/Images.xcassets/AppIcon.appiconset/Contents.json b/Action Assistant/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..8f31f592a8 --- /dev/null +++ b/Action Assistant/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "Senza titolo.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Senza titolo 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "Senza titolo 2.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo 1.png b/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo 1.png new file mode 100644 index 0000000000..a1fe849708 Binary files /dev/null and b/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo 1.png differ diff --git a/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo 2.png b/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo 2.png new file mode 100644 index 0000000000..a1fe849708 Binary files /dev/null and b/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo 2.png differ diff --git a/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo.png b/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo.png new file mode 100644 index 0000000000..a1fe849708 Binary files /dev/null and b/Action Assistant/Images.xcassets/AppIcon.appiconset/Senza titolo.png differ diff --git a/Action Assistant/Images.xcassets/Contents.json b/Action Assistant/Images.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Action Assistant/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Brand/Action_Assistant.entitlements b/Brand/Action_Assistant.entitlements new file mode 100644 index 0000000000..4ecc3f0d13 --- /dev/null +++ b/Brand/Action_Assistant.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.it.twsweb.Crypto-Cloud + + keychain-access-groups + + $(AppIdentifierPrefix)it.twsweb.Crypto-Cloud + + + diff --git a/Brand/Action_Assistant.plist b/Brand/Action_Assistant.plist new file mode 100644 index 0000000000..8fe4c9c6c9 --- /dev/null +++ b/Brand/Action_Assistant.plist @@ -0,0 +1,23 @@ + + + + + CFBundleDisplayName + Nextcloud Assistant + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + + NSExtensionMainStoryboard + MainInterface + + NSExtensionPointIdentifier + com.apple.ui-services + + + diff --git a/Brand/Share.plist b/Brand/Share.plist index 567991a236..ec2daea2b1 100755 --- a/Brand/Share.plist +++ b/Brand/Share.plist @@ -30,8 +30,7 @@ NSExtensionAttributes NSExtensionActivationRule - SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments,$attachment,(ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data")).@count == $extensionItem.attachments.@count).@count > 0 - + SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" OR ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text")).@count == $extensionItem.attachments.@count).@count > 0 NSExtensionMainStoryboard MainInterface @@ -39,4 +38,4 @@ com.apple.share-services - + \ No newline at end of file diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8bd5feb863..de48cf1181 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -304,7 +304,7 @@ F72CA05C2F5051DB002E2F06 /* AlertActionBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CA05B2F5051DB002E2F06 /* AlertActionBannerView.swift */; }; F72CD63A25C19EBF00F46F9A /* NCAutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CD63925C19EBF00F46F9A /* NCAutoUpload.swift */; }; F72D1007210B6882009C96B7 /* NCPushNotificationEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F72D1005210B6882009C96B7 /* NCPushNotificationEncryption.m */; }; - F72D404923D2082500A97FD0 /* NCViewerNextcloudText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */; }; + F72D404923D2082500A97FD0 /* NCViewerDirectEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */; }; F72D7EB7263B1207000B3DFC /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = F72D7EB6263B1207000B3DFC /* MarkdownKit */; }; F72DA9B425F53E4E00B87DB1 /* SwiftRichString in Frameworks */ = {isa = PBXBuildFile; productRef = F72DA9B325F53E4E00B87DB1 /* SwiftRichString */; }; F72EA95228B7BA2A00C88F0C /* DashboardWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EA95128B7BA2A00C88F0C /* DashboardWidgetProvider.swift */; }; @@ -339,7 +339,7 @@ F7386E482DA90E0F009A00F6 /* NCAppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */; }; F73BC74F2F23811E003170C2 /* WarningBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B3E2F1A2EE400514020 /* WarningBannerView.swift */; }; F73CB3B222E072A000AD728E /* NCShareHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F73CB3B122E072A000AD728E /* NCShareHeaderView.xib */; }; - F73D11FA253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73D11F9253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard */; }; + F73D11FA253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */; }; F73EF7A72B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; }; F73EF7A82B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; }; F73EF7A92B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; }; @@ -430,6 +430,7 @@ F758B45A212C564000515F55 /* NCScan.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F758B457212C564000515F55 /* NCScan.storyboard */; }; F758B45E212C569D00515F55 /* NCScanCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F758B45D212C569C00515F55 /* NCScanCell.swift */; }; F758B460212C56A400515F55 /* NCScan.swift in Sources */ = {isa = PBXBuildFile; fileRef = F758B45F212C56A400515F55 /* NCScan.swift */; }; + F75A60552FB4493A00F8247E /* NCDirectEditorAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A60542FB4493A00F8247E /* NCDirectEditorAdapter.swift */; }; F75A9EE623796C6F0044CFCE /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F75A9EE723796C6F0044CFCE /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F75C0C4823D1FAE300163CC8 /* NCRichWorkspaceCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75C0C4723D1FAE300163CC8 /* NCRichWorkspaceCommon.swift */; }; @@ -740,6 +741,18 @@ F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */; }; F7C30E00291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; }; F7C30E01291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; }; + F7C55C512FB4A658004A974F /* NCAssistantInputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */; }; + F7C55C7C2FB5AEF7004A974F /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */; }; + F7C55C882FB5AEF7004A974F /* Action Assistant.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F7C55C8D2FB5B02C004A974F /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; + F7C55C8E2FB5B03D004A974F /* NCGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */; }; + F7C55C8F2FB5B045004A974F /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; + F7C55C9A2FB5B127004A974F /* ThreadSafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */; }; + F7C55C9B2FB5B1A7004A974F /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; + F7C55C9F2FB5B83A004A974F /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = F7C55C9E2FB5B83A004A974F /* NextcloudKit */; }; + F7C55CC92FB5CE74004A974F /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55CC32FB5CE74004A974F /* ActionViewController.swift */; }; + F7C55CCA2FB5CE74004A974F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7C55CC42FB5CE74004A974F /* Images.xcassets */; }; + F7C55CCC2FB5CE74004A974F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */; }; F7C687E92D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */; }; F7C687EA2D22BDE5004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */; }; F7C687EB2D22BDE5004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */; }; @@ -926,6 +939,9 @@ F7FDFF702E437E55000D7688 /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7FDFF512E437E55000D7688 /* NCAccountRequest.storyboard */; }; F7FDFF722E437E55000D7688 /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FDFF522E437E55000D7688 /* NCAccountRequest.swift */; }; F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */; }; + F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; + F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; + F7FFFCA42FB3088E0015441E /* NCShareExtension+Assistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFCA32FB3088D0015441E /* NCShareExtension+Assistant.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -985,6 +1001,13 @@ remoteGlobalIDString = F771E3CF20E2392D00AFB62D; remoteInfo = "File Provider Extension"; }; + F7C55C862FB5AEF7004A974F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F7F67BA01A24D27800EE80DA /* Project object */; + proxyType = 1; + remoteGlobalIDString = F7C55C792FB5AEF7004A974F; + remoteInfo = "Action Assistant"; + }; F7C9739728F17131002C43E2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F7F67BA01A24D27800EE80DA /* Project object */; @@ -1108,6 +1131,7 @@ 2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */, F7C9739928F17131002C43E2 /* WidgetDashboardIntentHandler.appex in Embed Foundation Extensions */, F7346E1C28B0EF5E006CE2D2 /* Widget.appex in Embed Foundation Extensions */, + F7C55C882FB5AEF7004A974F /* Action Assistant.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -1370,7 +1394,7 @@ F72CD63925C19EBF00F46F9A /* NCAutoUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUpload.swift; sourceTree = ""; }; F72D1005210B6882009C96B7 /* NCPushNotificationEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCPushNotificationEncryption.m; sourceTree = ""; }; F72D1006210B6882009C96B7 /* NCPushNotificationEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCPushNotificationEncryption.h; sourceTree = ""; }; - F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCViewerNextcloudText.swift; sourceTree = ""; }; + F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCViewerDirectEditing.swift; sourceTree = ""; }; F72EA95128B7BA2A00C88F0C /* DashboardWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardWidgetProvider.swift; sourceTree = ""; }; F72EA95328B7BABA00C88F0C /* FilesWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesWidgetProvider.swift; sourceTree = ""; }; F72EA95728B7BC4F00C88F0C /* FilesData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesData.swift; sourceTree = ""; }; @@ -1397,7 +1421,7 @@ F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAppVersionManager.swift; sourceTree = ""; }; F73CB3B122E072A000AD728E /* NCShareHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCShareHeaderView.xib; sourceTree = ""; }; F73CB5771ED46807005F2A5A /* NCBridgeSwift.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCBridgeSwift.h; sourceTree = ""; }; - F73D11F9253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerNextcloudText.storyboard; sourceTree = ""; }; + F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerDirectEditing.storyboard; sourceTree = ""; }; F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Comments.swift"; sourceTree = ""; }; F73EF7B62B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+ExternalSites.swift"; sourceTree = ""; }; F73EF7BE2B02250B0087E6E9 /* NCManageDatabase+GPS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+GPS.swift"; sourceTree = ""; }; @@ -1435,6 +1459,7 @@ F758B457212C564000515F55 /* NCScan.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCScan.storyboard; sourceTree = ""; }; F758B45D212C569C00515F55 /* NCScanCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCScanCell.swift; sourceTree = ""; }; F758B45F212C56A400515F55 /* NCScan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCScan.swift; sourceTree = ""; }; + F75A60542FB4493A00F8247E /* NCDirectEditorAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDirectEditorAdapter.swift; sourceTree = ""; }; F75A9EE523796C6F0044CFCE /* NCNetworking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCNetworking.swift; sourceTree = ""; }; F75B91E21ECAE17800199C96 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; F75B91F71ECAE26300199C96 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; @@ -1711,6 +1736,12 @@ F7C30DF9291BCF790017149B /* NCNetworkingE2EECreateFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EECreateFolder.swift; sourceTree = ""; }; F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEDelete.swift; sourceTree = ""; }; F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EERename.swift; sourceTree = ""; }; + F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantInputModel.swift; sourceTree = ""; }; + F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Action Assistant.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; + F7C55CC32FB5CE74004A974F /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; + F7C55CC42FB5CE74004A974F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + F7C55CC62FB5CE74004A974F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+RecommendedFiles.swift"; sourceTree = ""; }; F7C7B488245EBA4100D93E60 /* NCViewerQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewerQuickLook.swift; sourceTree = ""; }; F7C9555221F0C4CA0024296E /* NCActivity.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCActivity.storyboard; sourceTree = ""; }; @@ -1829,6 +1860,8 @@ F7FDFF572E437E55000D7688 /* NCAccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccountSettingsView.swift; sourceTree = ""; }; F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; + F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; + F7FFFCA32FB3088D0015441E /* NCShareExtension+Assistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+Assistant.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -1977,6 +2010,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7C55C772FB5AEF7004A974F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F7C55C9F2FB5B83A004A974F /* NextcloudKit in Frameworks */, + F7C55C7C2FB5AEF7004A974F /* UniformTypeIdentifiers.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F7C9738D28F17131002C43E2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2159,8 +2201,10 @@ F3A0478E2BD2668800658E7B /* Assistant */ = { isa = PBXGroup; children = ( + F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */, F3A047962BD2668800658E7B /* NCAssistant.swift */, F3A047932BD2668800658E7B /* NCAssistantModel.swift */, + F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */, F3DDFE0D2F15452F00A784C8 /* Chat */, F3DDFE1F2F1F951000A784C8 /* Chat Sessions */, F3A047902BD2668800658E7B /* Create Task */, @@ -2421,13 +2465,14 @@ path = Offline; sourceTree = ""; }; - F73D11FF253C5F5400DF9BEC /* NCViewerNextcloudText */ = { + F73D11FF253C5F5400DF9BEC /* NCViewerDirectEditing */ = { isa = PBXGroup; children = ( - F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */, - F73D11F9253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard */, + F75A60542FB4493A00F8247E /* NCDirectEditorAdapter.swift */, + F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */, + F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */, ); - path = NCViewerNextcloudText; + path = NCViewerDirectEditing; sourceTree = ""; }; F74D3DB81BAC1941000BAE4B /* Networking */ = { @@ -2782,7 +2827,7 @@ F79018B1240962C7007C9B6D /* NCViewerMedia */, F723986A253C9C0E00257F49 /* NCViewerQuickLook */, F76D3CEF2428B3DD005DFA87 /* NCViewerPDF */, - F73D11FF253C5F5400DF9BEC /* NCViewerNextcloudText */, + F73D11FF253C5F5400DF9BEC /* NCViewerDirectEditing */, F7239861253C95D500257F49 /* NCViewerRichdocument */, ); path = Viewer; @@ -2980,6 +3025,7 @@ AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */, AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */, AF22B216277D196700DAB0CC /* NCShareExtension+Files.swift */, + F7FFFCA32FB3088D0015441E /* NCShareExtension+Assistant.swift */, AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */, F7148046262EBE4B00693E51 /* Share-Bridging-Header.h */, ); @@ -3016,6 +3062,16 @@ path = E2EE; sourceTree = ""; }; + F7C55CC82FB5CE74004A974F /* Action Assistant */ = { + isa = PBXGroup; + children = ( + F7C55CC32FB5CE74004A974F /* ActionViewController.swift */, + F7C55CC42FB5CE74004A974F /* Images.xcassets */, + F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */, + ); + path = "Action Assistant"; + sourceTree = ""; + }; F7C9739328F17131002C43E2 /* WidgetDashboardIntentHandler */ = { isa = PBXGroup; children = ( @@ -3202,6 +3258,7 @@ F7F1FBA62E27D13700C79E20 /* Frameworks */ = { isa = PBXGroup; children = ( + F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */, ); name = Frameworks; sourceTree = ""; @@ -3243,6 +3300,7 @@ 2C33C48023E2C475005F963B /* Notification Service Extension */, F7346E1428B0EF5B006CE2D2 /* Widget */, F7C9739328F17131002C43E2 /* WidgetDashboardIntentHandler */, + F7C55CC82FB5CE74004A974F /* Action Assistant */, F7FC7D651DC1F98700BB2C6A /* Products */, F30A962A2A27A9C800D7BCFE /* Tests */, F771E3D020E2392D00AFB62D /* File Provider Extension.appex */, @@ -3255,6 +3313,7 @@ C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */, F7F1FBA62E27D13700C79E20 /* Frameworks */, F31165012F9674A1009A1E37 /* AppIcon.icon */, + F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */, ); sourceTree = ""; }; @@ -3614,6 +3673,7 @@ F7346E1B28B0EF5E006CE2D2 /* PBXTargetDependency */, F7C9739828F17131002C43E2 /* PBXTargetDependency */, F70716EC2987F81500E72C1D /* PBXTargetDependency */, + F7C55C872FB5AEF7004A974F /* PBXTargetDependency */, ); name = Nextcloud; packageProductDependencies = ( @@ -3650,6 +3710,26 @@ productReference = F7CE8AFA1DC1F8D8009CAE48 /* Nextcloud.app */; productType = "com.apple.product-type.application"; }; + F7C55C792FB5AEF7004A974F /* Action Assistant */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Action Assistant" */; + buildPhases = ( + F7C55C762FB5AEF7004A974F /* Sources */, + F7C55C772FB5AEF7004A974F /* Frameworks */, + F7C55C782FB5AEF7004A974F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Action Assistant"; + packageProductDependencies = ( + F7C55C9E2FB5B83A004A974F /* NextcloudKit */, + ); + productName = "Action Assistant"; + productReference = F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */; + productType = "com.apple.product-type.app-extension"; + }; F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */ = { isa = PBXNativeTarget; buildConfigurationList = F7C9739A28F17132002C43E2 /* Build configuration list for PBXNativeTarget "WidgetDashboardIntentHandler" */; @@ -3682,7 +3762,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 2640; LastUpgradeCheck = 2640; ORGANIZATIONNAME = "Marino Faggiana"; TargetAttributes = { @@ -3736,6 +3816,9 @@ }; }; }; + F7C55C792FB5AEF7004A974F = { + CreatedOnToolsVersion = 26.4; + }; F7C9738F28F17131002C43E2 = { CreatedOnToolsVersion = 14.0; }; @@ -3832,6 +3915,7 @@ F7346E0F28B0EF5B006CE2D2 /* Widget */, F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */, F71459B41D12E3B700CAFEEC /* Share */, + F7C55C792FB5AEF7004A974F /* Action Assistant */, F771E3CF20E2392D00AFB62D /* File Provider Extension */, F70716E22987F81400E72C1D /* File Provider Extension UI */, 2C33C47E23E2C475005F963B /* Notification Service Extension */, @@ -3992,7 +4076,7 @@ F704B5E32430AA6F00632F5F /* NCCreateFormUploadConflict.storyboard in Resources */, F7EDE509262DA9D600414FE6 /* NCSelectCommandViewSelect.xib in Resources */, F732D23327CF8AED000B0F1B /* NCPlayerToolBar.xib in Resources */, - F73D11FA253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard in Resources */, + F73D11FA253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard in Resources */, F7EDE51B262DD0C400414FE6 /* NCSelectCommandViewCopyMove.xib in Resources */, F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */, F7D1612023CF19E30039EBBF /* NCViewerRichWorkspace.storyboard in Resources */, @@ -4010,6 +4094,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7C55C782FB5AEF7004A974F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7C55CCA2FB5CE74004A974F /* Images.xcassets in Resources */, + F7C55CCC2FB5CE74004A974F /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F7C9738E28F17131002C43E2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4277,6 +4370,7 @@ F7F878AF1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, F7327E3B2B73B8D600A462C7 /* Array+Extension.swift in Sources */, F7D7A76C2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */, + F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */, F39170AF2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */, F7D496FC2EBFA541004F9823 /* NCRecommendationsCell.swift in Sources */, F799DF832C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */, @@ -4298,6 +4392,7 @@ F763412D2EBE255B0056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F72FD3B8297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, F7A76DC8256A71CD00119AB3 /* UIImage+Extension.swift in Sources */, + F7FFFCA42FB3088E0015441E /* NCShareExtension+Assistant.swift in Sources */, F3E173C32C9B1067006D177A /* AwakeMode.swift in Sources */, F711A4E52AF9310500095DD8 /* NCUtility+Image.swift in Sources */, F73EF7AA2B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, @@ -4544,7 +4639,7 @@ F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */, F71916142E2901FB00E13E96 /* NCNetworking+Upload.swift in Sources */, F704B5E92430C0B800632F5F /* NCCreateFormUploadConflictCell.swift in Sources */, - F72D404923D2082500A97FD0 /* NCViewerNextcloudText.swift in Sources */, + F72D404923D2082500A97FD0 /* NCViewerDirectEditing.swift in Sources */, AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */, F77BB746289984CA0090FC19 /* UIViewController+Extension.swift in Sources */, F700510522DF6A89003A3356 /* NCShare.swift in Sources */, @@ -4601,6 +4696,7 @@ F78F74362163781100C2ADAD /* NCTrash.swift in Sources */, F71638922FA0C20C00A913B7 /* NCMoreView.swift in Sources */, AF2D7C7C2742556F00ADF566 /* NCShareLinkCell.swift in Sources */, + F7C55C512FB4A658004A974F /* NCAssistantInputModel.swift in Sources */, F7E41316294A19B300839300 /* UIView+Extension.swift in Sources */, F7C30E00291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */, F74AF3A4247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, @@ -4755,6 +4851,7 @@ F3DDFE212F1F953000A784C8 /* NCAssistantChatConversations.swift in Sources */, F72EC7262F45C91E00A2135C /* NCContextMenuNavigation.swift in Sources */, F7E2B64F2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift in Sources */, + F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */, F3DDFE0F2F15453900A784C8 /* NCAssistantChat.swift in Sources */, F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */, @@ -4784,6 +4881,7 @@ F7D61EA82EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, F79FFB262A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */, F70D8D8124A4A9BF000A5756 /* NCNetworkingProcess.swift in Sources */, + F75A60552FB4493A00F8247E /* NCDirectEditorAdapter.swift in Sources */, F3A0479A2BD2668800658E7B /* NCAssistantTaskDetail.swift in Sources */, F71D2FB72E09BBD700B751CC /* NCAutoUploadModel.swift in Sources */, F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */, @@ -4803,6 +4901,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7C55C762FB5AEF7004A974F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7C55C8F2FB5B045004A974F /* NCBrand.swift in Sources */, + F7C55C8E2FB5B03D004A974F /* NCGlobal.swift in Sources */, + F7C55CC92FB5CE74004A974F /* ActionViewController.swift in Sources */, + F7C55C9B2FB5B1A7004A974F /* UIColor+Extension.swift in Sources */, + F7C55C8D2FB5B02C004A974F /* NCAssistantSharedTextStore.swift in Sources */, + F7C55C9A2FB5B127004A974F /* ThreadSafeDictionary.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F7C9738C28F17131002C43E2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4902,6 +5013,11 @@ target = F771E3CF20E2392D00AFB62D /* File Provider Extension */; targetProxy = F771E3E920E2392E00AFB62D /* PBXContainerItemProxy */; }; + F7C55C872FB5AEF7004A974F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F7C55C792FB5AEF7004A974F /* Action Assistant */; + targetProxy = F7C55C862FB5AEF7004A974F /* PBXContainerItemProxy */; + }; F7C9739828F17131002C43E2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */; @@ -5080,6 +5196,14 @@ name = Intent.intentdefinition; sourceTree = ""; }; + F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7C55CC62FB5CE74004A974F /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; F7E70DE91A24DE4100E1B66A /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -5493,7 +5617,7 @@ OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = it.twsweb.Nextcloud.Share; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; @@ -5517,7 +5641,7 @@ OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = it.twsweb.Nextcloud.Share; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; @@ -5685,6 +5809,85 @@ }; name = Release; }; + F7C55C892FB5AEF7004A974F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/Action_Assistant.entitlements"; + CODE_SIGN_STYLE = Automatic; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/Brand/Action_Assistant.plist"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.Nextcloud.Action-Assistant"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + }; + name = Debug; + }; + F7C55C8A2FB5AEF7004A974F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/Action_Assistant.entitlements"; + CODE_SIGN_STYLE = Automatic; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/Brand/Action_Assistant.plist"; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.Nextcloud.Action-Assistant"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; F7C9739B28F17132002C43E2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5790,7 +5993,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -5821,6 +6024,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) NC DEBUG"; SWIFT_OBJC_INTEROP_MODE = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -5857,7 +6061,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = NKUJUXUJ3B; @@ -5886,6 +6090,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) NC"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_INTEROP_MODE = objc; @@ -5979,6 +6184,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Action Assistant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7C55C892FB5AEF7004A974F /* Debug */, + F7C55C8A2FB5AEF7004A974F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F7C9739A28F17132002C43E2 /* Build configuration list for PBXNativeTarget "WidgetDashboardIntentHandler" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -6132,8 +6346,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - kind = exactVersion; - version = 7.3.1; + branch = main; + kind = branch; }; }; F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { @@ -6575,6 +6789,11 @@ package = F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */; productName = Parchment; }; + F7C55C9E2FB5B83A004A974F /* NextcloudKit */ = { + isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; + productName = NextcloudKit; + }; F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { isa = XCSwiftPackageProductDependency; package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; diff --git a/Share/NCShareExtension+Assistant.swift b/Share/NCShareExtension+Assistant.swift new file mode 100644 index 0000000000..250ab85c2f --- /dev/null +++ b/Share/NCShareExtension+Assistant.swift @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import UniformTypeIdentifiers +import NextcloudKit + +extension NCShareExtension { + func handleAssistantSharedTextIfNeeded(inputItems: [NSExtensionItem]) async -> Bool { + guard let text = await loadText(from: inputItems), + !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return false + } + + NCAssistantSharedTextStore.save(text) + openMainAppForAssistantSharedText() + + return true + } + + private func loadText(from inputItems: [NSExtensionItem]) async -> String? { + for item in inputItems { + guard let attachments = item.attachments else { + continue + } + + for provider in attachments { + let plainTextIdentifier = UTType.plainText.identifier + let textIdentifier = UTType.text.identifier + + if provider.hasItemConformingToTypeIdentifier(plainTextIdentifier) { + return await loadText(from: provider, typeIdentifier: plainTextIdentifier) + } + + if provider.hasItemConformingToTypeIdentifier(textIdentifier) { + return await loadText(from: provider, typeIdentifier: textIdentifier) + } + + if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { + return await loadText(from: provider, typeIdentifier: UTType.text.identifier) + } + } + } + + return nil + } + + private func loadText(from provider: NSItemProvider, typeIdentifier: String) async -> String? { + await withCheckedContinuation { continuation in + provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in + let text: String? + + if let string = item as? String { + text = string + } else if let attributedString = item as? NSAttributedString { + text = attributedString.string + } else if let data = item as? Data { + text = String(data: data, encoding: .utf8) + } else if let url = item as? URL { + text = try? String(contentsOf: url, encoding: .utf8) + } else { + text = nil + } + + guard let text, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + continuation.resume(returning: nil) + return + } + + continuation.resume(returning: text) + } + } + } + + /// Opens the main app using the Assistant shared-text deep link. + private func openMainAppForAssistantSharedText() { + guard let url = URL(string: "nextcloud://assistant/shared-text") else { + extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + return + } + + openAssistantSharedTextURLThroughResponderChain(url) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) + } + } + + /// Opens the Assistant shared-text deep link from the Share extension. + /// + /// Share extensions cannot use `UIApplication.shared` directly because it is not + /// extension-safe. This method walks the responder chain until it finds the hidden + /// `UIApplication` responder and invokes the modern `open(_:options:completionHandler:)` + /// Objective-C selector dynamically. + /// + /// This is intentionally isolated because it relies on Objective-C runtime dispatch. + /// + /// - Parameter url: Deep link URL to open in the containing application. + private func openAssistantSharedTextURLThroughResponderChain(_ url: URL) { + let selector = NSSelectorFromString("openURL:options:completionHandler:") + let applicationClass: AnyClass? = NSClassFromString("UIApplication") + var responder: UIResponder? = self + + while let currentResponder = responder { + guard let applicationClass, + currentResponder.isKind(of: applicationClass), + currentResponder.responds(to: selector), + let implementation = currentResponder.method(for: selector) else { + responder = currentResponder.next + continue + } + + typealias CompletionBlock = @convention(block) (Bool) -> Void + typealias OpenURLFunction = @convention(c) (AnyObject, Selector, NSURL, NSDictionary, CompletionBlock?) -> Void + + let openURL = unsafeBitCast(implementation, to: OpenURLFunction.self) + + let completion: CompletionBlock = { success in + if success { + nkLog(debug: "Assistant shared text deep link performed through modern responder chain") + } else { + nkLog(error: "Assistant shared text deep link modern responder chain returned false") + } + } + + openURL(currentResponder, selector, url as NSURL, NSDictionary(), completion) + return + } + + nkLog(error: "Assistant shared text deep link failed because no UIApplication responder can open URL") + } +} diff --git a/Share/NCShareExtension.swift b/Share/NCShareExtension.swift index f3e5f8b60b..1d0ba7cc68 100644 --- a/Share/NCShareExtension.swift +++ b/Share/NCShareExtension.swift @@ -4,6 +4,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import UIKit +import UniformTypeIdentifiers import NextcloudKit import LucidBanner import SwiftUI @@ -150,20 +151,33 @@ class NCShareExtension: UIViewController { return } - NCFilesExtensionHandler(items: inputItems) { fileNames in - self.filesName = fileNames - DispatchQueue.main.async { - self.setCommandView() + // Keep the Share extension visually hidden until we know whether this is + // an Assistant text handoff or a normal file upload flow. This avoids the + // visible open-and-close flash when the extension only needs to redirect text. + view.alpha = 0 + + Task { @MainActor in + if await handleAssistantSharedTextIfNeeded(inputItems: inputItems) { + return } - } - if NCPreferences().presentPasscode { - NCPasscode.shared.presentPasscode(viewController: self, delegate: self) { - NCPasscode.shared.enableTouchFaceID() + self.view.alpha = 1 + + NCFilesExtensionHandler(items: inputItems) { fileNames in + self.filesName = fileNames + DispatchQueue.main.async { + self.setCommandView() + } } - } - self.collectionView.reloadData() + if NCPreferences().presentPasscode { + NCPasscode.shared.presentPasscode(viewController: self, delegate: self) { + NCPasscode.shared.enableTouchFaceID() + } + } + + self.collectionView.reloadData() + } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { diff --git a/iOSClient/Assistant/Chat/NCAssistantChat.swift b/iOSClient/Assistant/Chat/NCAssistantChat.swift index b256478acc..40cf01fcdb 100644 --- a/iOSClient/Assistant/Chat/NCAssistantChat.swift +++ b/iOSClient/Assistant/Chat/NCAssistantChat.swift @@ -23,7 +23,7 @@ struct NCAssistantChat: View { } .safeAreaInset(edge: .bottom) { - ChatInputField(isLoading: $chatModel.isSending, isDisabled: $chatModel.isSendingDisabled) { input in + ChatInputField(text: $chatModel.text, initialText: $chatModel.inputText, isLoading: $chatModel.isSending, isDisabled: $chatModel.isSendingDisabled) { input in if chatModel.selectedConversation != nil { chatModel.sendMessage(input: input) } else { @@ -209,15 +209,15 @@ struct EmptyChatView: View { #Preview { NavigationStack { NCAssistantChat(conversationsModel: .constant(NCAssistantChatConversationsModel(controller: nil))) - .environment(NCAssistantChatModel(controller: nil)) - .environment(NCAssistantModel(controller: nil)) + .environment(NCAssistantChatModel(controller: nil, inputModel: NCAssistantInputModel())) + .environment(NCAssistantModel(controller: nil, inputModel: NCAssistantInputModel())) } } #Preview("With Messages") { NavigationStack { NCAssistantChat(conversationsModel: .constant(NCAssistantChatConversationsModel(controller: nil))) - .environment(NCAssistantChatModel.example) - .environment(NCAssistantModel(controller: nil)) + .environment(NCAssistantChatModel(controller: nil, inputModel: NCAssistantInputModel())) + .environment(NCAssistantModel(controller: nil, inputModel: NCAssistantInputModel())) } } diff --git a/iOSClient/Assistant/Chat/NCAssistantChatModel.swift b/iOSClient/Assistant/Chat/NCAssistantChatModel.swift index e29e34e8db..bbbabde172 100644 --- a/iOSClient/Assistant/Chat/NCAssistantChatModel.swift +++ b/iOSClient/Assistant/Chat/NCAssistantChatModel.swift @@ -15,6 +15,16 @@ class NCAssistantChatModel { var showRetryResponseGenerationButton = false var showMessageNotSentError: Bool = false + var text: String { + get { inputModel.text } + set { inputModel.text = newValue } + } + + var inputText: String { + get { inputModel.initialText } + set { inputModel.initialText = newValue } + } + public private(set) var selectedConversation: AssistantConversation? var currentSession: AssistantSession? @@ -23,15 +33,17 @@ class NCAssistantChatModel { private var pollingTask: Task? @ObservationIgnored var controller: NCMainTabBarController? + @ObservationIgnored let inputModel: NCAssistantInputModel @ObservationIgnored private var chatMessageTaskId: Int? @ObservationIgnored var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: controller) } - init(controller: NCMainTabBarController?, messages: [AssistantChatMessage] = []) { + init(controller: NCMainTabBarController?, messages: [AssistantChatMessage] = [], inputModel: NCAssistantInputModel) { self.controller = controller self.ncSession = NCSession.shared.getSession(controller: controller) self.messages = messages + self.inputModel = inputModel } func startPollingForResponse(interval: TimeInterval = 4.0) { @@ -186,5 +198,5 @@ extension NCAssistantChatModel { role: "assistant", content: "Based on the text you provided, here's a concise summary: The document discusses the classic Lorem Ipsum placeholder text, which has been used in the printing and typesetting industry for centuries as a standard dummy text.", timestamp: Int(Date().addingTimeInterval(-120).timeIntervalSince1970 * 1000) - )]) + )], inputModel: NCAssistantInputModel()) } diff --git a/iOSClient/Assistant/Components/ChatInputField.swift b/iOSClient/Assistant/Components/ChatInputField.swift index dd2845da55..c22c880bd0 100644 --- a/iOSClient/Assistant/Components/ChatInputField.swift +++ b/iOSClient/Assistant/Components/ChatInputField.swift @@ -6,12 +6,24 @@ import SwiftUI struct ChatInputField: View { @FocusState private var isInputFocused: Bool - @State var text: String = "" + @State private var hasAppliedInitialText = false + + @Binding var text: String + @Binding var initialText: String @Binding var isLoading: Bool @Binding var isDisabled: Bool + var onSend: ((_ input: String) -> Void)? - init(isLoading: Binding = .constant(false), isDisabled: Binding = .constant(false), onSend: ((_: String) -> Void)? = nil) { + init( + text: Binding = .constant(""), + initialText: Binding = .constant(""), + isLoading: Binding = .constant(false), + isDisabled: Binding = .constant(false), + onSend: ((_: String) -> Void)? = nil + ) { + _text = text + _initialText = initialText _isLoading = isLoading _isDisabled = isDisabled self.onSend = onSend @@ -56,10 +68,42 @@ struct ChatInputField: View { .padding(.top, 16) .padding(.bottom, 16) .background(.background) + .task { + applyInitialTextIfNeeded() + } + } + + private func applyInitialTextIfNeeded() { + guard !hasAppliedInitialText else { + return + } + + hasAppliedInitialText = true + + guard text.isEmpty, !initialText.isEmpty else { + return + } + + text = initialText + initialText = "" } } #Preview { - ChatInputField(isLoading: .constant(false)) - ChatInputField(isLoading: .constant(true)) + @Previewable @State var text = "" + @Previewable @State var initialText = "Text received from outside" + + VStack(spacing: 16) { + ChatInputField( + text: $text, + initialText: $initialText, + isLoading: .constant(false) + ) + + ChatInputField( + text: .constant("Loading state"), + initialText: .constant(""), + isLoading: .constant(true) + ) + } } diff --git a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift index 33cb36fa35..caec215dde 100644 --- a/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift +++ b/iOSClient/Assistant/Create Task/NCAssistantCreateNewTask.swift @@ -60,7 +60,7 @@ struct NCAssistantCreateNewTask: View { } #Preview { - let model = NCAssistantModel(controller: nil) + let model = NCAssistantModel(controller: nil, inputModel: NCAssistantInputModel()) NCAssistantCreateNewTask() .environment(model) diff --git a/iOSClient/Assistant/NCAssistant.swift b/iOSClient/Assistant/NCAssistant.swift index 239c59cb52..b8c5a29c36 100644 --- a/iOSClient/Assistant/NCAssistant.swift +++ b/iOSClient/Assistant/NCAssistant.swift @@ -89,14 +89,15 @@ struct NCAssistant: View { } #Preview { - @Previewable @State var chatModel = NCAssistantChatModel(controller: nil) - let model = NCAssistantModel(controller: nil) + @Previewable @State var chatModel = NCAssistantChatModel(controller: nil, inputModel: NCAssistantInputModel()) + + let model = NCAssistantModel(controller: nil, inputModel: NCAssistantInputModel()) let conversationsModel = NCAssistantChatConversationsModel(controller: nil) NCAssistant(assistantModel: model, chatModel: chatModel, conversationsModel: conversationsModel) - .onAppear { - model.loadDummyData() - } + .onAppear { + model.loadDummyData() + } } struct TaskList: View { @@ -182,7 +183,7 @@ struct TaskList: View { } } .safeAreaInset(edge: .bottom) { - ChatInputField(isLoading: $assistantModel.isLoading) { input in + ChatInputField(text: $assistantModel.text, initialText: $assistantModel.inputText, isLoading: $assistantModel.isLoading) { input in assistantModel.scheduleTask(input: input) } } diff --git a/iOSClient/Assistant/NCAssistantInputModel.swift b/iOSClient/Assistant/NCAssistantInputModel.swift new file mode 100644 index 0000000000..0ef847c09d --- /dev/null +++ b/iOSClient/Assistant/NCAssistantInputModel.swift @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit +import NextcloudKit +import SwiftUI + +@Observable +final class NCAssistantInputModel { + var text: String = "" + var initialText: String + + init(initialText: String = "") { + self.initialText = initialText + } +} diff --git a/iOSClient/Assistant/NCAssistantModel.swift b/iOSClient/Assistant/NCAssistantModel.swift index 3a6640e376..7b796643ca 100644 --- a/iOSClient/Assistant/NCAssistantModel.swift +++ b/iOSClient/Assistant/NCAssistantModel.swift @@ -14,21 +14,33 @@ class NCAssistantModel { var selectedType: TaskTypeData? var selectedTask: AssistantTask? + var text: String { + get { inputModel.text } + set { inputModel.text = newValue } + } + + var inputText: String { + get { inputModel.initialText } + set { inputModel.initialText = newValue } + } + var hasError: Bool = false var isLoading: Bool = false var isRefreshing: Bool = false var scrollTypeListToTop: Bool = false @ObservationIgnored let controller: NCMainTabBarController? + @ObservationIgnored let inputModel: NCAssistantInputModel @ObservationIgnored private var tasks: [AssistantTask] = [] @ObservationIgnored private let session: NCSession.Session @ObservationIgnored private let useV2: Bool @ObservationIgnored private let chatTypeId = "core:text2text:chat" @ObservationIgnored var isSelectedTypeChat: Bool { selectedType?.id == chatTypeId } - init(controller: NCMainTabBarController?) { + init(controller: NCMainTabBarController?, inputModel: NCAssistantInputModel) { self.controller = controller - session = NCSession.shared.getSession(controller: controller) + self.inputModel = inputModel + self.session = NCSession.shared.getSession(controller: controller) let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() useV2 = capabilities.serverVersionMajor >= NCGlobal.shared.nextcloudVersion30 diff --git a/iOSClient/Assistant/NCAssistantSharedTextStore.swift b/iOSClient/Assistant/NCAssistantSharedTextStore.swift new file mode 100644 index 0000000000..0a362491c6 --- /dev/null +++ b/iOSClient/Assistant/NCAssistantSharedTextStore.swift @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +enum NCAssistantSharedTextStore { + private static let sharedTextKey = "assistant.sharedText" + private static let sharedTextDateKey = "assistant.sharedTextDate" + private static var appGroupIdentifier: String { + NCBrandOptions.shared.capabilitiesGroup + } + + /// Saves text received from the Assistant share extension into the shared App Group container. + /// + /// - Parameter text: Text selected by the user in another app. + static func save(_ text: String) { + let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) + + guard !trimmedText.isEmpty, + let defaults = UserDefaults(suiteName: appGroupIdentifier) else { + return + } + + defaults.set(trimmedText, forKey: sharedTextKey) + defaults.set(Date(), forKey: sharedTextDateKey) + defaults.synchronize() + } + + /// Loads and removes the latest text received from the Assistant share extension. + /// + /// - Returns: Previously saved text, or `nil` when no valid text is available. + static func loadAndClear() -> String? { + guard let defaults = UserDefaults(suiteName: appGroupIdentifier), + let text = defaults.string(forKey: sharedTextKey)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !text.isEmpty else { + return nil + } + + defaults.removeObject(forKey: sharedTextKey) + defaults.removeObject(forKey: sharedTextDateKey) + defaults.synchronize() + + return text + } + + /// Removes any pending shared text from the App Group container. + static func clear() { + guard let defaults = UserDefaults(suiteName: appGroupIdentifier) else { + return + } + + defaults.removeObject(forKey: sharedTextKey) + defaults.removeObject(forKey: sharedTextDateKey) + defaults.synchronize() + } +} diff --git a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift index 52370b5a3f..f2dc5cc0de 100644 --- a/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift +++ b/iOSClient/Assistant/Task Detail/NCAssistantTaskDetail.swift @@ -35,7 +35,7 @@ struct NCAssistantTaskDetail: View { } #Preview { - let assistantModel = NCAssistantModel(controller: nil) + let assistantModel = NCAssistantModel(controller: nil, inputModel: NCAssistantInputModel()) NCAssistantTaskDetail(task: assistantModel.selectedTask!) .environment(assistantModel) diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index bf9a829c4b..2e44150c79 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -259,9 +259,8 @@ extension tableMetadata { directEditingEditors.isEmpty { // RichDocument: Collabora return true - } else if directEditingEditors.contains("nextcloud text") || directEditingEditors.contains("onlyoffice") { - // DirectEditing: Nextcloud Text - OnlyOffice - return true + } else if !directEditingEditors.isEmpty { + return true } return false } @@ -282,12 +281,8 @@ extension tableMetadata { guard (classFile == NKTypeClassFile.document.rawValue) && NextcloudKit.shared.isNetworkReachable() else { return false } - let editors = NCUtility().editorsDirectEditing(account: account, contentType: contentType).map { $0.lowercased() } - - if editors.contains("nextcloud text") || editors.contains("onlyoffice") { - return true - } - return false + let editors = NCUtility().editorsDirectEditing(account: account, contentType: contentType) + return !editors.isEmpty } var isPDF: Bool { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift index 2733be4b87..279de6f3a5 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+UIEditMenuInteractionDelegate.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import UIKit +import UniformTypeIdentifiers import NextcloudKit import RealmSwift import LucidBanner @@ -70,7 +71,14 @@ extension NCCollectionViewCommon: UIEditMenuInteractionDelegate { for (index, items) in UIPasteboard.general.items.enumerated() { for item in items { let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) - let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) + let identifier = item.key + let resolvedType = UTType(mimeType: identifier) ?? UTType(identifier) + let resolvedMimeType = resolvedType?.preferredMIMEType ?? identifier + let resolvedExtension = resolvedType?.preferredFilenameExtension ?? "" + let results = NKFilePropertyResolver().resolve(mimeType: resolvedMimeType, + fileExtension: resolvedExtension, + typeIdentifier: resolvedType?.identifier ?? identifier, + capabilities: capabilities) guard let data = UIPasteboard.general.data(forPasteboardType: item.key, inItemSet: IndexSet([index]))?.first else { diff --git a/iOSClient/Main/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index d4042e2e92..5db2c8818e 100644 --- a/iOSClient/Main/Create/NCCreate.swift +++ b/iOSClient/Main/Create/NCCreate.swift @@ -25,12 +25,8 @@ class NCCreate: NSObject { var options = NKRequestOptions() let serverUrl = controller.currentServerUrl() - if let creatorId, editorId == "text" || editorId == "onlyoffice" { - if editorId == "onlyoffice" { - options = NKRequestOptions(customUserAgent: NCUtility().getCustomUserAgentOnlyOffice()) - } else if editorId == "text" { - options = NKRequestOptions(customUserAgent: NCUtility().getCustomUserAgentNCText()) - } + if let creatorId, let adapter = NCDirectEditorAdapter.resolve(from: [editorId]) { + options = NKRequestOptions(customUserAgent: adapter.userAgent(utility)) let results = await NextcloudKit.shared.textCreateFileAsync(fileNamePath: fileNamePath, editorId: editorId, creatorId: creatorId, templateId: templateId, account: account, options: options) { task in Task { let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, @@ -94,13 +90,8 @@ class NCCreate: NSObject { var selectedTemplate = NKEditorTemplate() var ext: String = "" - if editorId == "text" || editorId == "onlyoffice" { - var options = NKRequestOptions() - if editorId == "onlyoffice" { - options = NKRequestOptions(customUserAgent: NCUtility().getCustomUserAgentOnlyOffice()) - } else if editorId == "text" { - options = NKRequestOptions(customUserAgent: NCUtility().getCustomUserAgentNCText()) - } + if let adapter = NCDirectEditorAdapter.resolve(from: [editorId]) { + let options = NKRequestOptions(customUserAgent: adapter.userAgent(NCUtility())) let results = await NextcloudKit.shared.textGetListOfTemplatesAsync(account: account, options: options) { task in Task { @@ -128,15 +119,7 @@ class NCCreate: NSObject { if templates.isEmpty { var temp = NKEditorTemplate() temp.identifier = "" - if editorId == "text" { - temp.ext = "md" - } else if editorId == "onlyoffice" && templateId == "document" { - temp.ext = "docx" - } else if editorId == "onlyoffice" && templateId == "spreadsheet" { - temp.ext = "xlsx" - } else if editorId == "onlyoffice" && templateId == "presentation" { - temp.ext = "pptx" - } + temp.ext = adapter.defaultExt(templateId) temp.name = "Empty" temp.preview = "" templates.append(temp) diff --git a/iOSClient/Main/NCMainNavigationController.swift b/iOSClient/Main/NCMainNavigationController.swift index beb91f757e..ccce146116 100644 --- a/iOSClient/Main/NCMainNavigationController.swift +++ b/iOSClient/Main/NCMainNavigationController.swift @@ -83,7 +83,8 @@ class NCMainNavigationController: UINavigationController, UINavigationController assistantButtonItem.title = NSLocalizedString("_assistant_", comment: "") assistantButtonItem.tintColor = NCBrandColor.shared.iconImageColor assistantButtonItem.primaryAction = UIAction(handler: { _ in - let assistant = NCAssistant(assistantModel: NCAssistantModel(controller: self.controller), chatModel: NCAssistantChatModel(controller: self.controller), conversationsModel: NCAssistantChatConversationsModel(controller: self.controller)) + let inputModel = NCAssistantInputModel() + let assistant = NCAssistant(assistantModel: NCAssistantModel(controller: self.controller, inputModel: inputModel), chatModel: NCAssistantChatModel(controller: self.controller, inputModel: inputModel), conversationsModel: NCAssistantChatConversationsModel(controller: self.controller)) let hostingController = UIHostingController(rootView: assistant) self.present(hostingController, animated: true, completion: nil) }) @@ -294,7 +295,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController !(topViewController is NCViewerMediaPage), !(topViewController is NCViewerPDF), !(topViewController is NCViewerRichDocument), - !(topViewController is NCViewerNextcloudText) + !(topViewController is NCViewerDirectEditing) else { return } diff --git a/iOSClient/Menu/NCContextMenuPlus.swift b/iOSClient/Menu/NCContextMenuPlus.swift index 2d1cdefdbb..0782aed87d 100644 --- a/iOSClient/Menu/NCContextMenuPlus.swift +++ b/iOSClient/Menu/NCContextMenuPlus.swift @@ -8,6 +8,13 @@ import NextcloudKit @MainActor class NCContextMenuPlus: NSObject { + struct CreatorMenuInfo { + let titleKey: String + let templateId: String + let icon: String + let sortOrder: Int + } + let menuToolbar: UIToolbar? let controller: NCMainTabBarController? @@ -20,6 +27,19 @@ class NCContextMenuPlus: NSObject { self.controller = controller } + nonisolated static func menuInfo(for ext: String) -> CreatorMenuInfo? { + switch ext.lowercased() { + case "docx": + return CreatorMenuInfo(titleKey: "_create_new_document_", templateId: "document", icon: "doc.text", sortOrder: 0) + case "xlsx": + return CreatorMenuInfo(titleKey: "_create_new_spreadsheet_", templateId: "spreadsheet", icon: "tablecells", sortOrder: 1) + case "pptx": + return CreatorMenuInfo(titleKey: "_create_new_presentation_", templateId: "presentation", icon: "play.rectangle", sortOrder: 2) + default: + return nil + } + } + func create(session: NCSession.Session) async { guard let controller, let menuToolbar else { return @@ -38,7 +58,7 @@ class NCContextMenuPlus: NSObject { var menuActionElement: [UIMenuElement] = [] var menuE2EEElement: [UIMenuElement] = [] var menuTextElement: [UIMenuElement] = [] - var menuOnlyOfficeElement: [UIMenuElement] = [] + var menuDirectEditingElement: [UIMenuElement] = [] var menuRichDocumentElement: [UIMenuElement] = [] // ------------------------------- ACTION @@ -214,59 +234,53 @@ class NCContextMenuPlus: NSObject { }) } - // ------------------------------- ONLY OFFICE + // ------------------------------- DIRECT EDITING CREATORS (onlyoffice, eurooffice, …) - if let creator = capabilities.directEditingCreators.first(where: { $0.editor == "onlyoffice" && $0.identifier == "onlyoffice_docx"}) { - menuOnlyOfficeElement.append(UIAction(title: NSLocalizedString("_create_new_document_", comment: ""), - image: utility.loadImage(named: "doc.text", colors: [NCBrandColor.shared.documentIconColor])) { _ in - Task { @MainActor in - let createDocument = NCCreate() - let templates = await createDocument.getTemplate(editorId: "onlyoffice", templateId: "document", account: session.account) - let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let creatorsByEditor = Dictionary(grouping: capabilities.directEditingCreators, by: \.editor) + for editorId in creatorsByEditor.keys.sorted() { + guard NCDirectEditorAdapter.resolve(from: [editorId]) != nil, + editorId != "text" else { continue } - await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "onlyoffice", creatorId: creator.identifier, templateId: templates.selectedTemplate.identifier, account: session.account) + let sortedCreators = creatorsByEditor[editorId]! + .compactMap { creator -> (NKEditorDetailsCreator, CreatorMenuInfo)? in + guard let info = NCContextMenuPlus.menuInfo(for: creator.ext) else { return nil } + return (creator, info) } - }) - } - - if let creator = capabilities.directEditingCreators.first(where: { $0.editor == "onlyoffice" && $0.identifier == "onlyoffice_xlsx"}) { - menuOnlyOfficeElement.append(UIAction(title: NSLocalizedString("_create_new_spreadsheet_", comment: ""), - image: utility.loadImage(named: "tablecells", colors: [NCBrandColor.shared.spreadsheetIconColor])) { _ in - Task { @MainActor in - let createDocument = NCCreate() - let templates = await createDocument.getTemplate(editorId: "onlyoffice", templateId: "spreadsheet", account: session.account) - let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) - - await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "onlyoffice", creatorId: creator.identifier, templateId: templates.selectedTemplate.identifier, account: session.account) - } - - }) - } - - if let creator = capabilities.directEditingCreators.first(where: { $0.editor == "onlyoffice" && $0.identifier == "onlyoffice_pptx"}) { - menuOnlyOfficeElement.append(UIAction(title: NSLocalizedString("_create_new_presentation_", comment: ""), - image: utility.loadImage(named: "play.rectangle", colors: [NCBrandColor.shared.presentationIconColor])) { _ in - Task { @MainActor in - let createDocument = NCCreate() - let templates = await createDocument.getTemplate(editorId: "onlyoffice", templateId: "presentation", account: session.account) - let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) - - await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "onlyoffice", creatorId: creator.identifier, templateId: templates.selectedTemplate.identifier, account: session.account) - } - }) + .sorted { $0.1.sortOrder < $1.1.sortOrder } + + for (creator, info) in sortedCreators { + menuDirectEditingElement.append(UIAction( + title: NSLocalizedString(info.titleKey, comment: ""), + image: utility.loadImage(named: info.icon, colors: [info.iconColor]) + ) { _ in + Task { @MainActor in + let createDocument = NCCreate() + let fileExt: String + let templateIdentifier: String + if creator.templates { + let result = await createDocument.getTemplate(editorId: editorId, templateId: info.templateId, account: session.account) + fileExt = result.ext + templateIdentifier = result.selectedTemplate.identifier + } else { + fileExt = creator.ext + templateIdentifier = "" + } + let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + fileExt, account: session.account, serverUrl: serverUrl) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) + await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: editorId, creatorId: creator.identifier, templateId: templateIdentifier, account: session.account) + } + }) + } } } let menuAction = UIMenu(title: "", options: .displayInline, children: menuActionElement) let menuText = UIMenu(title: "", options: .displayInline, children: menuTextElement) let menuE2EE = UIMenu(title: "", options: .displayInline, children: menuE2EEElement) - let menuOnlyOffice = UIMenu(title: "", options: .displayInline, children: menuOnlyOfficeElement) + let menuDirectEditing = UIMenu(title: "", options: .displayInline, children: menuDirectEditingElement) let menuRichDocument = UIMenu(title: "", options: .displayInline, children: menuRichDocumentElement) - let plusMenu = UIMenu(children: [menuAction, menuE2EE, menuText, menuRichDocument, menuOnlyOffice]) + let plusMenu = UIMenu(children: [menuAction, menuE2EE, menuText, menuRichDocument, menuDirectEditing]) let config = UIImage.SymbolConfiguration(pointSize: 25, weight: .thin) let plusImage = UIImage(systemName: "plus.circle.fill", withConfiguration: config) @@ -342,3 +356,14 @@ class NCContextMenuPlus: NSObject { } } } + +@MainActor +extension NCContextMenuPlus.CreatorMenuInfo { + var iconColor: UIColor { + switch templateId { + case "spreadsheet": return NCBrandColor.shared.spreadsheetIconColor + case "presentation": return NCBrandColor.shared.presentationIconColor + default: return NCBrandColor.shared.documentIconColor + } + } +} diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index 19c37ca126..d302ad6458 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -335,6 +335,28 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return nil } + /* + Example: nextcloud://assistant/shared-text + */ + + if scheme == global.appScheme, action == "assistant", url.path == "/shared-text" { + guard let text = NCAssistantSharedTextStore.loadAndClear() else { + return + } + + Task { @MainActor in + let capabilities = await NKCapabilities.shared.getCapabilities(for: controller.account) + if capabilities.assistantEnabled { + let inputModel = NCAssistantInputModel(initialText: text) + let assistant = NCAssistant(assistantModel: NCAssistantModel(controller: controller, inputModel: inputModel), chatModel: NCAssistantChatModel(controller: controller, inputModel: inputModel), conversationsModel: NCAssistantChatConversationsModel(controller: controller)) + let hostingController = UIHostingController(rootView: assistant) + controller.present(hostingController, animated: true, completion: nil) + } + } + + return + } + /* Example: nextcloud://open-action?action=create-voice-memo&&user=marinofaggiana&url=https://cloud.nextcloud.com */ diff --git a/iOSClient/Utility/NCUtility+Image.swift b/iOSClient/Utility/NCUtility+Image.swift index d427cc5c86..4c4f747681 100644 --- a/iOSClient/Utility/NCUtility+Image.swift +++ b/iOSClient/Utility/NCUtility+Image.swift @@ -33,6 +33,7 @@ extension NCUtility { case NKTypeIconFile.txt.rawValue: image = UIImage(systemName: "doc.text", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) case NKTypeIconFile.url.rawValue: image = UIImage(systemName: "network", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) case NKTypeIconFile.xls.rawValue: image = UIImage(systemName: "tablecells", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.spreadsheetIconColor])) + case NKTypeIconFile.draw.rawValue: image = UIImage(systemName: "pencil.and.scribble", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) default: image = UIImage(systemName: "doc", withConfiguration: UIImage.SymbolConfiguration(weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [NCBrandColor.shared.iconImageColor2])) } } diff --git a/iOSClient/Utility/NCUtility.swift b/iOSClient/Utility/NCUtility.swift index 0a36f11662..e64b90735c 100644 --- a/iOSClient/Utility/NCUtility.swift +++ b/iOSClient/Utility/NCUtility.swift @@ -39,32 +39,32 @@ final class NCUtility: NSObject, Sendable { } func editorsDirectEditing(account: String, contentType: String) -> [String] { - var names: [String] = [] + var identifiers: [String] = [] let capabilities = NCNetworking.shared.capabilities[account] capabilities?.directEditingEditors.forEach { editor in editor.mimetypes.forEach { mimetype in if mimetype == contentType { - names.append(editor.name) + identifiers.append(editor.identifier) } // HARDCODE // https://github.com/nextcloud/text/issues/913 if mimetype == "text/markdown" && contentType == "text/x-markdown" { - names.append(editor.name) + identifiers.append(editor.identifier) } if contentType == "text/html" { - names.append(editor.name) + identifiers.append(editor.identifier) } } editor.optionalMimetypes.forEach { mimetype in if mimetype == contentType { - names.append(editor.name) + identifiers.append(editor.identifier) } } } - return Array(Set(names)) + return Array(Set(identifiers)) } func getCustomUserAgentNCText() -> String { diff --git a/iOSClient/Viewer/NCViewer.swift b/iOSClient/Viewer/NCViewer.swift index 6689927a39..353787937d 100644 --- a/iOSClient/Viewer/NCViewer.swift +++ b/iOSClient/Viewer/NCViewer.swift @@ -58,6 +58,7 @@ class NCViewer: NSObject { // DOCUMENTS else if metadata.classFile == NKTypeClassFile.document.rawValue, !NCUtilityFileSystem().isDirectoryE2EE(serverUrl: metadata.serverUrl, urlBase: session.urlBase, userId: session.userId, account: session.account) { + // PDF if metadata.isPDF { let vc = UIStoryboard(name: "NCViewerPDF", bundle: nil).instantiateInitialViewController() as? NCViewerPDF @@ -68,16 +69,26 @@ class NCViewer: NSObject { return vc } - // RichDocument: Collabora - if metadata.isAvailableRichDocumentEditorView { + + // DirectEditing + if metadata.isAvailableDirectEditingEditorView { + let editors = utility.editorsDirectEditing(account: metadata.account, contentType: metadata.contentType).map { $0.lowercased() } + guard let editorAdapter = NCDirectEditorAdapter.resolve(from: editors) else { + self.QLPreview(metadata: metadata, delegate: delegate) + return nil + } + let editor = editorAdapter.apiKey + let editorViewController = editorAdapter.viewControllerEditor + let options = NKRequestOptions(customUserAgent: editorAdapter.userAgent(utility)) if metadata.url.isEmpty { + let fileNamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) NCActivityIndicator.shared.start(backgroundView: delegate?.view) - let results = await NextcloudKit.shared.createUrlRichdocumentsAsync(fileID: metadata.fileId, account: metadata.account) { task in + let results = await NextcloudKit.shared.textOpenFileAsync(fileNamePath: fileNamePath, editor: editor, account: metadata.account, options: options) { task in Task { let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, - path: metadata.fileId, - name: "createUrlRichdocuments") + path: fileNamePath, + name: "textOpenFile") await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } } @@ -89,19 +100,20 @@ class NCViewer: NSObject { return nil } - let vc = UIStoryboard(name: "NCViewerRichdocument", bundle: nil).instantiateInitialViewController() as? NCViewerRichDocument + let vc = UIStoryboard(name: "NCViewerDirectEditing", bundle: nil).instantiateInitialViewController() as? NCViewerDirectEditing vc?.metadata = metadata + vc?.editor = editorViewController vc?.link = url vc?.imageIcon = image vc?.navigationItem.setBidiSafeTitle(metadata.fileNameView) return vc - } else { - let vc = UIStoryboard(name: "NCViewerRichdocument", bundle: nil).instantiateInitialViewController() as? NCViewerRichDocument + let vc = UIStoryboard(name: "NCViewerDirectEditing", bundle: nil).instantiateInitialViewController() as? NCViewerDirectEditing vc?.metadata = metadata + vc?.editor = editorViewController vc?.link = metadata.url vc?.imageIcon = image vc?.navigationItem.setBidiSafeTitle(metadata.fileNameView) @@ -109,30 +121,16 @@ class NCViewer: NSObject { return vc } } - // DirectEditing: Nextcloud Text - OnlyOffice - if metadata.isAvailableDirectEditingEditorView { - var options = NKRequestOptions() - var editor = "" - var editorViewController = "" - let editors = utility.editorsDirectEditing(account: metadata.account, contentType: metadata.contentType).map { $0.lowercased() } - if editors.contains("nextcloud text") { - editor = "text" - editorViewController = "nextcloud text" - options = NKRequestOptions(customUserAgent: utility.getCustomUserAgentNCText()) - } else if editors.contains("onlyoffice") { - editor = "onlyoffice" - editorViewController = "onlyoffice" - options = NKRequestOptions(customUserAgent: utility.getCustomUserAgentOnlyOffice()) - } - if metadata.url.isEmpty { - let fileNamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + // RichDocument: Collabora + if metadata.isAvailableRichDocumentEditorView { + if metadata.url.isEmpty { NCActivityIndicator.shared.start(backgroundView: delegate?.view) - let results = await NextcloudKit.shared.textOpenFileAsync(fileNamePath: fileNamePath, editor: editor, account: metadata.account, options: options) { task in + let results = await NextcloudKit.shared.createUrlRichdocumentsAsync(fileID: metadata.fileId, account: metadata.account) { task in Task { let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, - path: fileNamePath, - name: "textOpenFile") + path: metadata.fileId, + name: "createUrlRichdocuments") await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } } @@ -144,33 +142,31 @@ class NCViewer: NSObject { return nil } - let vc = UIStoryboard(name: "NCViewerNextcloudText", bundle: nil).instantiateInitialViewController() as? NCViewerNextcloudText + let vc = UIStoryboard(name: "NCViewerRichdocument", bundle: nil).instantiateInitialViewController() as? NCViewerRichDocument vc?.metadata = metadata - vc?.editor = editorViewController vc?.link = url vc?.imageIcon = image vc?.navigationItem.setBidiSafeTitle(metadata.fileNameView) return vc + } else { - let vc = UIStoryboard(name: "NCViewerNextcloudText", bundle: nil).instantiateInitialViewController() as? NCViewerNextcloudText + let vc = UIStoryboard(name: "NCViewerRichdocument", bundle: nil).instantiateInitialViewController() as? NCViewerRichDocument vc?.metadata = metadata - vc?.editor = editorViewController vc?.link = metadata.url vc?.imageIcon = image vc?.navigationItem.setBidiSafeTitle(metadata.fileNameView) return vc } - } else { - self.QLPreview(metadata: metadata, delegate: delegate) } - } else { - self.QLPreview(metadata: metadata, delegate: delegate) } + // iOS QL-Preview + self.QLPreview(metadata: metadata, delegate: delegate) + return nil } diff --git a/iOSClient/Viewer/NCViewerDirectEditing/NCDirectEditorAdapter.swift b/iOSClient/Viewer/NCViewerDirectEditing/NCDirectEditorAdapter.swift new file mode 100644 index 0000000000..15cc9a33ac --- /dev/null +++ b/iOSClient/Viewer/NCViewerDirectEditing/NCDirectEditorAdapter.swift @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation + +struct NCDirectEditorAdapter { + /// Editor ID passed to the textOpenFile API. + let apiKey: String + /// Value set on NCViewerNextcloudText.editor — controls user agent and JS behaviour. + let viewControllerEditor: String + /// Resolves the custom user agent string via NCUtility. + let userAgent: (NCUtility) -> String + /// Returns the fallback file extension for a given templateId when the template API returns no templates. + let defaultExt: (_ templateId: String) -> String + + /// Lookup an adapter for the first matching editor ID in the provided list. + /// The list should already be lowercased. + static func resolve(from editors: [String]) -> NCDirectEditorAdapter? { + editors.lazy.compactMap { registry[$0.lowercased()] }.first + } + + // MARK: - Registry + + private static func officeDefaultExt(_ templateId: String) -> String { + switch templateId { + case "spreadsheet": return "xlsx" + case "presentation": return "pptx" + default: return "docx" + } + } + + private static let registry: [String: NCDirectEditorAdapter] = [ + "text": NCDirectEditorAdapter( + apiKey: "text", + viewControllerEditor: "nextcloud text", + userAgent: { $0.getCustomUserAgentNCText() }, + defaultExt: { _ in "md" } + ), + "onlyoffice": NCDirectEditorAdapter( + apiKey: "onlyoffice", + viewControllerEditor: "onlyoffice", + userAgent: { $0.getCustomUserAgentOnlyOffice() }, + defaultExt: officeDefaultExt + ), + "eurooffice": NCDirectEditorAdapter( + apiKey: "eurooffice", + viewControllerEditor: "onlyoffice", + userAgent: { $0.getCustomUserAgentOnlyOffice() }, + defaultExt: officeDefaultExt + ), + "whiteboard": NCDirectEditorAdapter( + apiKey: "whiteboard", + viewControllerEditor: "onlyoffice", + userAgent: { $0.getCustomUserAgentOnlyOffice() }, + defaultExt: { _ in "whiteboard" } + ) + ] +} diff --git a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.storyboard b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.storyboard similarity index 91% rename from iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.storyboard rename to iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.storyboard index 959209506b..e90c459511 100644 --- a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.storyboard +++ b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.storyboard @@ -1,17 +1,17 @@ - + - + - + - + diff --git a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift similarity index 98% rename from iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift rename to iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift index 577f55ea76..90fef2de6a 100644 --- a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift +++ b/iOSClient/Viewer/NCViewerDirectEditing/NCViewerDirectEditing.swift @@ -6,7 +6,7 @@ import UIKit import NextcloudKit @preconcurrency import WebKit -class NCViewerNextcloudText: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate { +class NCViewerDirectEditing: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate { var webView = WKWebView() var bottomConstraint: NSLayoutConstraint? var link: String = "" @@ -223,7 +223,7 @@ class NCViewerNextcloudText: UIViewController, WKNavigationDelegate, WKScriptMes } } -extension NCViewerNextcloudText: UINavigationControllerDelegate { +extension NCViewerDirectEditing: UINavigationControllerDelegate { override func didMove(toParent parent: UIViewController?) { super.didMove(toParent: parent) @@ -237,7 +237,7 @@ extension NCViewerNextcloudText: UINavigationControllerDelegate { } } -extension NCViewerNextcloudText: NCTransferDelegate { +extension NCViewerDirectEditing: NCTransferDelegate { func transferReloadData(serverUrl: String?) { } func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { }