diff --git a/shell_integration/MacOSX/CMakeLists.txt b/shell_integration/MacOSX/CMakeLists.txt index 327392a84..a3f561484 100644 --- a/shell_integration/MacOSX/CMakeLists.txt +++ b/shell_integration/MacOSX/CMakeLists.txt @@ -35,7 +35,20 @@ if(APPLE) COMMENT building macOS File Provider extension VERBATIM) - add_dependencies(mac_overlayplugin mac_fileproviderplugin nextcloud) # for the ownCloud.icns to be generated + add_custom_target( mac_fileprovideruiplugin ALL + xcodebuild ARCHS=${CMAKE_OSX_ARCHITECTURES} ONLY_ACTIVE_ARCH=NO + -project ${CMAKE_SOURCE_DIR}/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj + -target FileProviderUIExt -configuration ${XCODE_TARGET_CONFIGURATION} "SYMROOT=${CMAKE_CURRENT_BINARY_DIR}" + "OC_APPLICATION_EXECUTABLE_NAME=${APPLICATION_EXECUTABLE}" + "OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}" + "OC_APPLICATION_NAME=${APPLICATION_NAME}" + "OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}" + "OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX=${SOCKETAPI_TEAM_IDENTIFIER_PREFIX}" + DEPENDS mac_fileproviderplugin + COMMENT building macOS File Provider UI extension + VERBATIM) + + add_dependencies(mac_overlayplugin mac_fileproviderplugin mac_fileprovideruiplugin nextcloud) # for the ownCloud.icns to be generated else() add_dependencies(mac_overlayplugin nextcloud) # for the ownCloud.icns to be generated endif() @@ -55,6 +68,10 @@ if(APPLE) install(DIRECTORY ${OSX_PLUGINS_BINARY_DIR}/FileProviderExt.appex DESTINATION ${OSX_PLUGINS_INSTALL_DIR} USE_SOURCE_PERMISSIONS) + + install(DIRECTORY ${OSX_PLUGINS_BINARY_DIR}/FileProviderUIExt.appex + DESTINATION ${OSX_PLUGINS_INSTALL_DIR} + USE_SOURCE_PERMISSIONS) endif() endif() endif() diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift index 6edb6ab4b..e9e5d78fd 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Extensions/Logger+Extensions.swift @@ -19,12 +19,15 @@ extension Logger { static let desktopClientConnection = Logger( subsystem: subsystem, category: "desktopclientconnection") + static let fpUiExtensionService = Logger(subsystem: subsystem, category: "fpUiExtensionService") static let enumeration = Logger(subsystem: subsystem, category: "enumeration") static let fileProviderExtension = Logger( subsystem: subsystem, category: "fileproviderextension") static let fileTransfer = Logger(subsystem: subsystem, category: "filetransfer") static let localFileOps = Logger(subsystem: subsystem, category: "localfileoperations") static let ncFilesDatabase = Logger(subsystem: subsystem, category: "nextcloudfilesdatabase") + static let shares = Logger(subsystem: subsystem, category: "shares") + static let ncAccount = Logger(subsystem: subsystem, category: "ncAccount") static let materialisedFileHandling = Logger( subsystem: subsystem, category: "materialisedfilehandling" ) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift index a357b79ea..5a5fcecfc 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+ClientInterface.swift @@ -39,7 +39,8 @@ extension FileProviderExtension: NSFileProviderServicing { ) -> Progress { Logger.desktopClientConnection.debug("Serving supported service sources") let clientCommService = ClientCommunicationService(fpExtension: self) - let services = [clientCommService] + let fpuiExtService = FPUIExtensionServiceSource(fpExtension: self) + let services: [NSFileProviderServiceSource] = [clientCommService, fpuiExtService] completionHandler(services, nil) let progress = Progress() progress.cancellationHandler = { diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift index cb3b7ea62..4af76fa4f 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/NextcloudAccount.swift @@ -15,6 +15,12 @@ import FileProvider import Foundation +let ncAccountDictUsernameKey = "usernameKey" +let ncAccountDictPasswordKey = "passwordKey" +let ncAccountDictNcKitAccountKey = "ncKitAccountKey" +let ncAccountDictServerUrlKey = "serverUrlKey" +let ncAccountDictDavFilesUrlKey = "davFilesUrlKey" + struct NextcloudAccount: Equatable { static let webDavFilesUrlSuffix: String = "/remote.php/dav/files/" let username, password, ncKitAccount, serverUrl, davFilesUrl: String @@ -26,4 +32,31 @@ struct NextcloudAccount: Equatable { self.serverUrl = serverUrl davFilesUrl = serverUrl + NextcloudAccount.webDavFilesUrlSuffix + user } + + init?(dictionary: Dictionary) { + guard let username = dictionary[ncAccountDictUsernameKey], + let password = dictionary[ncAccountDictPasswordKey], + let ncKitAccount = dictionary[ncAccountDictNcKitAccountKey], + let serverUrl = dictionary[ncAccountDictServerUrlKey], + let davFilesUrl = dictionary[ncAccountDictDavFilesUrlKey] + else { + return nil + } + + self.username = username + self.password = password + self.ncKitAccount = ncKitAccount + self.serverUrl = serverUrl + self.davFilesUrl = davFilesUrl + } + + func dictionary() -> Dictionary { + return [ + ncAccountDictUsernameKey: username, + ncAccountDictPasswordKey: password, + ncAccountDictNcKitAccountKey: ncKitAccount, + ncAccountDictServerUrlKey: serverUrl, + ncAccountDictDavFilesUrlKey: davFilesUrl + ] + } } diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift new file mode 100644 index 000000000..0c3e444a9 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift @@ -0,0 +1,18 @@ +// +// FPUIExtensionCommunicationProtocol.swift +// FileProviderExt +// +// Created by Claudio Cambra on 21/2/24. +// + +import FileProvider +import NextcloudKit + +let fpUiExtensionServiceName = NSFileProviderServiceName( + "com.nextcloud.desktopclient.FPUIExtensionService" +) + +@objc protocol FPUIExtensionService { + func credentials() async -> NSDictionary + func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString? +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift new file mode 100644 index 000000000..76f7f10f4 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionServiceSource.swift @@ -0,0 +1,64 @@ +// +// FPUIExtensionCommunicationService.swift +// FileProviderExt +// +// Created by Claudio Cambra on 21/2/24. +// + +import FileProvider +import Foundation +import NextcloudKit +import OSLog + +class FPUIExtensionServiceSource: NSObject, NSFileProviderServiceSource, NSXPCListenerDelegate, FPUIExtensionService { + let listener = NSXPCListener.anonymous() + let serviceName = fpUiExtensionServiceName + let fpExtension: FileProviderExtension + + init(fpExtension: FileProviderExtension) { + Logger.fpUiExtensionService.debug("Instantiating FPUIExtensionService service") + self.fpExtension = fpExtension + super.init() + } + + func makeListenerEndpoint() throws -> NSXPCListenerEndpoint { + listener.delegate = self + listener.resume() + return listener.endpoint + } + + func listener( + _ listener: NSXPCListener, + shouldAcceptNewConnection newConnection: NSXPCConnection + ) -> Bool { + newConnection.exportedInterface = NSXPCInterface(with: FPUIExtensionService.self) + newConnection.exportedObject = self + newConnection.resume() + return true + } + + //MARK: - FPUIExtensionService protocol methods + + func credentials() async -> NSDictionary { + return (fpExtension.ncAccount?.dictionary() ?? [:]) as NSDictionary + } + + func itemServerPath(identifier: NSFileProviderItemIdentifier) async -> NSString? { + let rawIdentifier = identifier.rawValue + Logger.shares.info("Fetching shares for item \(rawIdentifier, privacy: .public)") + + guard let baseUrl = fpExtension.ncAccount?.davFilesUrl else { + Logger.shares.error("Could not fetch shares as ncAccount on parent extension is nil") + return nil + } + + let dbManager = NextcloudFilesDatabaseManager.shared + guard let item = dbManager.itemMetadataFromFileProviderItemIdentifier(identifier) else { + Logger.shares.error("No item \(rawIdentifier, privacy: .public) in db, no shares.") + return nil + } + + let completePath = item.serverUrl + "/" + item.fileName + return completePath.replacingOccurrences(of: baseUrl, with: "") as NSString + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift new file mode 100644 index 000000000..dd068f0ac --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift @@ -0,0 +1,53 @@ +// +// DocumentActionViewController.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 20/2/24. +// + +import FileProviderUI +import OSLog + +class DocumentActionViewController: FPUIActionExtensionViewController { + var domain: NSFileProviderDomain { + guard let identifier = extensionContext.domainIdentifier else { + fatalError("not expected to be called with default domain") + } + return NSFileProviderDomain( + identifier: NSFileProviderDomainIdentifier(rawValue: identifier.rawValue), + displayName: "" + ) + } + + func prepare(childViewController: NSViewController) { + addChild(childViewController) + view.addSubview(childViewController.view) + + NSLayoutConstraint.activate([ + view.leadingAnchor.constraint(equalTo: childViewController.view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: childViewController.view.trailingAnchor), + view.topAnchor.constraint(equalTo: childViewController.view.topAnchor), + view.bottomAnchor.constraint(equalTo: childViewController.view.bottomAnchor) + ]) + } + + override func prepare( + forAction actionIdentifier: String, itemIdentifiers: [NSFileProviderItemIdentifier] + ) { + Logger.actionViewController.info("Preparing for action: \(actionIdentifier)") + + if actionIdentifier == "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction" { + prepare(childViewController: ShareViewController(itemIdentifiers)) + } + + } + + override func prepare(forError error: Error) { + Logger.actionViewController.info("Preparing for error: \(error.localizedDescription)") + } + + override public func loadView() { + self.view = NSView() + } +} + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Extensions/Logger+Extensions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Extensions/Logger+Extensions.swift new file mode 100644 index 000000000..960171034 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Extensions/Logger+Extensions.swift @@ -0,0 +1,21 @@ +// +// Logger+Extensions.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 21/2/24. +// + +import OSLog + +extension Logger { + private static var subsystem = Bundle.main.bundleIdentifier! + + static let actionViewController = Logger(subsystem: subsystem, category: "actionViewController") + static let shareCapabilities = Logger(subsystem: subsystem, category: "shareCapabilities") + static let shareController = Logger(subsystem: subsystem, category: "shareController") + static let shareeDataSource = Logger(subsystem: subsystem, category: "shareeDataSource") + static let sharesDataSource = Logger(subsystem: subsystem, category: "sharesDataSource") + static let shareOptionsView = Logger(subsystem: subsystem, category: "shareOptionsView") + static let shareViewController = Logger(subsystem: subsystem, category: "shareViewController") +} + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Extensions/NKShare+Extensions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Extensions/NKShare+Extensions.swift new file mode 100644 index 000000000..2527fb83c --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Extensions/NKShare+Extensions.swift @@ -0,0 +1,130 @@ +// +// NKShare+Extensions.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 28/2/24. +// + +import AppKit +import NextcloudKit + +extension NKShare { + enum ShareType: Int { + case user = 0 + case group = 1 + case publicLink = 3 + case email = 4 + case federatedCloud = 6 + case circle = 7 + case talkConversation = 10 + } + + enum PermissionValues: Int { + case readShare = 1 + case updateShare = 2 + case createShare = 4 + case deleteShare = 8 + case shareShare = 16 + case all = 31 + } + + var typeImage: NSImage? { + var image: NSImage? + switch shareType { + case ShareType.user.rawValue: + image = NSImage( + systemSymbolName: "person.circle.fill", + accessibilityDescription: "User share icon" + ) + case ShareType.group.rawValue: + image = NSImage( + systemSymbolName: "person.2.circle.fill", + accessibilityDescription: "Group share icon" + ) + case ShareType.publicLink.rawValue: + image = NSImage( + systemSymbolName: "link.circle.fill", + accessibilityDescription: "Public link share icon" + ) + case ShareType.email.rawValue: + image = NSImage( + systemSymbolName: "envelope.circle.fill", + accessibilityDescription: "Email share icon" + ) + case ShareType.federatedCloud.rawValue: + image = NSImage( + systemSymbolName: "cloud.circle.fill", + accessibilityDescription: "Federated cloud share icon" + ) + case ShareType.circle.rawValue: + image = NSImage( + systemSymbolName: "circle.circle.fill", + accessibilityDescription: "Circle share icon" + ) + case ShareType.talkConversation.rawValue: + image = NSImage( + systemSymbolName: "message.circle.fill", + accessibilityDescription: "Talk conversation share icon" + ) + default: + return nil + } + + var config = NSImage.SymbolConfiguration(textStyle: .body, scale: .large) + if #available(macOS 12.0, *) { + config = config.applying( + .init(paletteColors: [.controlBackgroundColor, .controlAccentColor]) + ) + } + return image?.withSymbolConfiguration(config) + } + + var displayString: String { + if label != "" { + return label + } + + switch shareType { + case ShareType.user.rawValue: + return "User share (\(shareWith))" + case ShareType.group.rawValue: + return "Group share (\(shareWith))" + case ShareType.publicLink.rawValue: + return "Public link share" + case ShareType.email.rawValue: + return "Email share (\(shareWith))" + case ShareType.federatedCloud.rawValue: + return "Federated cloud share (\(shareWith))" + case ShareType.circle.rawValue: + return "Circle share (\(shareWith))" + case ShareType.talkConversation.rawValue: + return "Talk conversation share (\(shareWith))" + default: + return "Unknown share" + } + } + + var expirationDateString: String? { + guard let date = expirationDate else { return nil } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" + return dateFormatter.string(from: date as Date) + } + + var shareesCanEdit: Bool { + get { (permissions & PermissionValues.updateShare.rawValue) != 0 } + set { + if newValue { + permissions |= NKShare.PermissionValues.updateShare.rawValue + } else { + permissions &= ~NKShare.PermissionValues.updateShare.rawValue + } + } + } + + static func formattedDateString(date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" + return dateFormatter.string(from: date) + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/FileProviderUIExt.entitlements b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/FileProviderUIExt.entitlements new file mode 100644 index 000000000..5d2a36d31 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/FileProviderUIExt.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN) + + + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/FileProviderUIExtRelease.entitlements b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/FileProviderUIExtRelease.entitlements new file mode 100644 index 000000000..eab912dc4 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/FileProviderUIExtRelease.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + $(OC_SOCKETAPI_TEAM_IDENTIFIER_PREFIX)$(OC_APPLICATION_REV_DOMAIN) + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist new file mode 100644 index 000000000..f559d75c4 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist @@ -0,0 +1,24 @@ + + + + + NSExtension + + NSExtensionFileProviderActions + + + NSExtensionFileProviderActionActivationRule + TRUEPREDICATE + NSExtensionFileProviderActionIdentifier + com.nextcloud.desktopclient.FileProviderUIExt.ShareAction + NSExtensionFileProviderActionName + Share options + + + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).DocumentActionViewController + NSExtensionPointIdentifier + com.apple.fileprovider-actionsui + + + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareController.swift new file mode 100644 index 000000000..ba2666f8b --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareController.swift @@ -0,0 +1,131 @@ +// +// ShareController.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 4/3/24. +// + +import Combine +import Foundation +import NextcloudKit +import OSLog + +class ShareController: ObservableObject { + @Published private(set) var share: NKShare + private let kit: NextcloudKit + + static func create( + kit: NextcloudKit, + shareType: NKShare.ShareType, + itemServerRelativePath: String, + shareWith: String?, + password: String? = nil, + expireDate: String? = nil, + permissions: Int = 1, + publicUpload: Bool = false, + note: String? = nil, + label: String? = nil, + hideDownload: Bool, + attributes: String? = nil, + options: NKRequestOptions = NKRequestOptions() + ) async -> NKError? { + Logger.shareController.info("Creating share: \(itemServerRelativePath)") + return await withCheckedContinuation { continuation in + if shareType == .publicLink { + kit.createShareLink( + path: itemServerRelativePath, + hideDownload: hideDownload, + publicUpload: publicUpload, + password: password, + permissions: permissions, + options: options + ) { account, share, data, error in + defer { continuation.resume(returning: error) } + guard error == .success else { + Logger.shareController.error("Error creating link share: \(error)") + return + } + } + } else { + guard let shareWith = shareWith else { + let errorString = "No recipient for share!" + Logger.shareController.error("\(errorString)") + let error = NKError(statusCode: 0, fallbackDescription: errorString) + continuation.resume(returning: error) + return + } + + kit.createShare( + path: itemServerRelativePath, + shareType: shareType.rawValue, + shareWith: shareWith, + password: password, + permissions: permissions, + options: options, + attributes: attributes + ) { account, share, data, error in + defer { continuation.resume(returning: error) } + guard error == .success else { + Logger.shareController.error("Error creating share: \(error)") + return + } + } + } + } + } + + init(share: NKShare, kit: NextcloudKit) { + self.share = share + self.kit = kit + } + + func save( + password: String? = nil, + expireDate: String? = nil, + permissions: Int = 1, + publicUpload: Bool = false, + note: String? = nil, + label: String? = nil, + hideDownload: Bool, + attributes: String? = nil, + options: NKRequestOptions = NKRequestOptions() + ) async -> NKError? { + Logger.shareController.info("Saving share: \(self.share.url)") + return await withCheckedContinuation { continuation in + kit.updateShare( + idShare: share.idShare, + password: password, + expireDate: expireDate, + permissions: permissions, + publicUpload: publicUpload, + note: note, + label: label, + hideDownload: hideDownload, + attributes: attributes, + options: options + ) { account, share, data, error in + Logger.shareController.info("Received update response: \(share?.url ?? "")") + defer { continuation.resume(returning: error) } + guard error == .success, let share = share else { + Logger.shareController.error("Error updating save: \(error.errorDescription)") + return + } + self.share = share + } + } + } + + func delete() async -> NKError? { + Logger.shareController.info("Deleting share: \(self.share.url)") + return await withCheckedContinuation { continuation in + kit.deleteShare(idShare: share.idShare) { account, error in + Logger.shareController.info("Received delete response: \(self.share.url)") + defer { continuation.resume(returning: error) } + guard error == .success else { + Logger.shareController.error("Error deleting save: \(error.errorDescription)") + return + } + } + } + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareOptionsView.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareOptionsView.swift new file mode 100644 index 000000000..516909055 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareOptionsView.swift @@ -0,0 +1,353 @@ +// +// ShareOptionsView.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 28/2/24. +// + +import AppKit +import Combine +import NextcloudKit +import OSLog +import SuggestionsTextFieldKit + +class ShareOptionsView: NSView { + @IBOutlet private weak var optionsTitleTextField: NSTextField! + @IBOutlet private weak var shareRecipientTextField: NSTextField! // Hide if public link share + @IBOutlet private weak var labelTextField: NSTextField! + @IBOutlet private weak var uploadEditPermissionCheckbox: NSButton! + @IBOutlet private weak var hideDownloadCheckbox: NSButton! + @IBOutlet private weak var passwordProtectCheckbox: NSButton! + @IBOutlet private weak var passwordSecureField: NSSecureTextField! + @IBOutlet private weak var expirationDateCheckbox: NSButton! + @IBOutlet private weak var expirationDatePicker: NSDatePicker! + @IBOutlet private weak var noteForRecipientCheckbox: NSButton! + @IBOutlet private weak var noteTextField: NSTextField! + @IBOutlet private weak var saveButton: NSButton! + @IBOutlet private weak var deleteButton: NSButton! + @IBOutlet private weak var shareTypePicker: NSPopUpButton! + @IBOutlet private weak var publicLinkShareMenuItem: NSMenuItem! + @IBOutlet private weak var userShareMenuItem: NSMenuItem! + @IBOutlet private weak var groupShareMenuItem: NSMenuItem! + @IBOutlet private weak var emailShareMenuItem: NSMenuItem! + @IBOutlet private weak var federatedCloudShareMenuItem: NSMenuItem! + @IBOutlet private weak var circleShare: NSMenuItem! + @IBOutlet private weak var talkConversationShare: NSMenuItem! + + var kit: NextcloudKit? { + didSet { + Logger.shareOptionsView.info("Setting up the kit.") + guard let kit = kit else { + Logger.shareOptionsView.error("Could not configure suggestions data source.") + return + } + + suggestionsTextFieldDelegate.suggestionsDataSource = ShareeSuggestionsDataSource( + kit: kit + ) + suggestionsTextFieldDelegate.confirmationHandler = { suggestion in + guard let sharee = suggestion?.data as? NKSharee else { return } + self.shareRecipientTextField.stringValue = sharee.shareWith + Logger.shareOptionsView.debug("Chose sharee \(sharee.shareWith, privacy: .public)") + } + suggestionsTextFieldDelegate.targetTextField = shareRecipientTextField + } + } + var dataSource: ShareTableViewDataSource? + var controller: ShareController? { + didSet { + guard controller != nil else { return } + optionsTitleTextField.stringValue = "Share options" + deleteButton.title = "Delete" + deleteButton.image = NSImage( + systemSymbolName: "trash", accessibilityDescription: "Delete trash icon" + ) + deleteButton.bezelColor = NSColor.systemRed + cancellable?.cancel() + createMode = false + update() + cancellable = controller.publisher.sink { _ in self.update() } + } + } + var createMode = false { + didSet { + Logger.shareOptionsView.info("Create mode set: \(self.createMode)") + shareTypePicker.isHidden = !createMode + shareRecipientTextField.isHidden = !createMode + labelTextField.isHidden = createMode // Cannot set label on create API call + guard createMode else { return } + optionsTitleTextField.stringValue = "Create new share" + deleteButton.title = "Cancel" + deleteButton.image = NSImage( + systemSymbolName: "xmark.bin", accessibilityDescription: "Cancel create icon" + ) + deleteButton.bezelColor = NSColor.controlColor + cancellable?.cancel() + cancellable = nil + controller = nil + reset() + setupCreateForm() + } + } + private var cancellable: AnyCancellable? + private var suggestionsWindowController = SuggestionsWindowController() + private var suggestionsTextFieldDelegate = SuggestionsTextFieldDelegate() + + private func update() { + guard let share = controller?.share else { + reset() + setAllFields(enabled: false) + saveButton.isEnabled = false + deleteButton.isEnabled = false + return + } + + deleteButton.isEnabled = share.canDelete + saveButton.isEnabled = share.canEdit + + if share.canEdit { + setAllFields(enabled: true) + labelTextField.stringValue = share.label + uploadEditPermissionCheckbox.state = share.shareesCanEdit ? .on : .off + hideDownloadCheckbox.state = share.hideDownload ? .on : .off + passwordProtectCheckbox.state = share.password.isEmpty ? .off : .on + passwordSecureField.isHidden = passwordProtectCheckbox.state == .off + expirationDateCheckbox.state = share.expirationDate == nil ? .off : .on + expirationDatePicker.isHidden = expirationDateCheckbox.state == .off + noteForRecipientCheckbox.state = share.note.isEmpty ? .off : .on + noteTextField.isHidden = noteForRecipientCheckbox.state == .off + } else { + setAllFields(enabled: false) + reset() + } + } + + private func reset() { + shareRecipientTextField.stringValue = "" + labelTextField.stringValue = "" + uploadEditPermissionCheckbox.state = .off + hideDownloadCheckbox.state = .off + passwordProtectCheckbox.state = .off + passwordSecureField.isHidden = true + passwordSecureField.stringValue = "" + expirationDateCheckbox.state = .off + expirationDatePicker.isHidden = true + expirationDatePicker.dateValue = NSDate.now + expirationDatePicker.minDate = NSDate.now + expirationDatePicker.maxDate = nil + noteForRecipientCheckbox.state = .off + noteTextField.isHidden = true + noteTextField.stringValue = "" + } + + private func setupCreateForm() { + guard createMode else { return } + + setAllFields(enabled: true) + + let type = pickedShareType() + shareRecipientTextField.isHidden = type == .publicLink + + if let caps = dataSource?.capabilities?.filesSharing { + uploadEditPermissionCheckbox.state = + caps.defaultPermissions & NKShare.PermissionValues.updateShare.rawValue != 0 + ? .on : .off + + switch type { + case .publicLink: + passwordProtectCheckbox.isHidden = false + passwordProtectCheckbox.state = caps.publicLink?.passwordEnforced == true ? .on : .off + passwordProtectCheckbox.isEnabled = caps.publicLink?.passwordEnforced == false + expirationDateCheckbox.state = caps.publicLink?.expireDateEnforced == true ? .on : .off + expirationDateCheckbox.isEnabled = caps.publicLink?.expireDateEnforced == false + expirationDatePicker.dateValue = Date( + timeIntervalSinceNow: + TimeInterval((caps.publicLink?.expireDateDays ?? 1) * 24 * 60 * 60) + ) + if caps.publicLink?.expireDateEnforced == true { + expirationDatePicker.maxDate = expirationDatePicker.dateValue + } + case .email: + passwordProtectCheckbox.isHidden = caps.email?.passwordEnabled == false + passwordProtectCheckbox.state = caps.email?.passwordEnforced == true ? .on : .off + default: + passwordProtectCheckbox.isHidden = true + passwordProtectCheckbox.state = .off + break + } + } + + passwordSecureField.isHidden = passwordProtectCheckbox.state == .off + expirationDatePicker.isHidden = expirationDateCheckbox.state == .off + } + + private func setAllFields(enabled: Bool) { + shareTypePicker.isEnabled = enabled + shareRecipientTextField.isEnabled = enabled + labelTextField.isEnabled = enabled + uploadEditPermissionCheckbox.isEnabled = enabled + hideDownloadCheckbox.isEnabled = enabled + passwordProtectCheckbox.isEnabled = enabled + passwordSecureField.isEnabled = enabled + expirationDateCheckbox.isEnabled = enabled + expirationDatePicker.isEnabled = enabled + noteForRecipientCheckbox.isEnabled = enabled + noteTextField.isEnabled = enabled + saveButton.isEnabled = enabled + deleteButton.isEnabled = enabled + } + + private func pickedShareType() -> NKShare.ShareType { + let selectedShareTypeItem = shareTypePicker.selectedItem + var selectedShareType = NKShare.ShareType.publicLink + if selectedShareTypeItem == publicLinkShareMenuItem { + selectedShareType = .publicLink + } else if selectedShareTypeItem == userShareMenuItem { + selectedShareType = .user + } else if selectedShareTypeItem == groupShareMenuItem { + selectedShareType = .group + } else if selectedShareTypeItem == emailShareMenuItem { + selectedShareType = .email + } else if selectedShareTypeItem == federatedCloudShareMenuItem { + selectedShareType = .federatedCloud + } else if selectedShareTypeItem == circleShare { + selectedShareType = .circle + } else if selectedShareTypeItem == talkConversationShare { + selectedShareType = .talkConversation + } + return selectedShareType + } + + @IBAction func shareTypePickerAction(_ sender: Any) { + if createMode { + setupCreateForm() + } + } + + @IBAction func passwordCheckboxAction(_ sender: Any) { + passwordSecureField.isHidden = passwordProtectCheckbox.state == .off + } + + @IBAction func expirationDateCheckboxAction(_ sender: Any) { + expirationDatePicker.isHidden = expirationDateCheckbox.state == .off + } + + @IBAction func noteForRecipientCheckboxAction(_ sender: Any) { + noteTextField.isHidden = noteForRecipientCheckbox.state == .off + } + + @IBAction func save(_ sender: Any) { + Task { @MainActor in + let password = passwordProtectCheckbox.state == .on + ? passwordSecureField.stringValue + : "" + let expireDate = expirationDateCheckbox.state == .on + ? NKShare.formattedDateString(date: expirationDatePicker.dateValue) + : "" + let note = noteForRecipientCheckbox.state == .on + ? noteTextField.stringValue + : "" + let label = labelTextField.stringValue + let hideDownload = hideDownloadCheckbox.state == .on + let uploadAndEdit = uploadEditPermissionCheckbox.state == .on + + guard !createMode else { + Logger.shareOptionsView.info("Creating new share!") + + guard let dataSource = dataSource, + let kit = kit, + let itemServerRelativePath = dataSource.itemServerRelativePath + else { + Logger.shareOptionsView.error("Cannot create new share due to missing data.") + Logger.shareOptionsView.error("dataSource: \(self.dataSource)") + Logger.shareOptionsView.error("kit: \(self.kit)") + Logger.shareOptionsView.error( + "path: \(self.dataSource?.itemServerRelativePath ?? "")" + ) + return + } + + let selectedShareType = pickedShareType() + let shareWith = shareRecipientTextField.stringValue + + var permissions = NKShare.PermissionValues.all.rawValue + permissions = uploadAndEdit + ? permissions | NKShare.PermissionValues.updateShare.rawValue + : permissions & ~NKShare.PermissionValues.updateShare.rawValue + + setAllFields(enabled: false) + deleteButton.isEnabled = false + saveButton.isEnabled = false + let error = await ShareController.create( + kit: kit, + shareType: selectedShareType, + itemServerRelativePath: itemServerRelativePath, + shareWith: shareWith, + password: password, + expireDate: expireDate, + permissions: permissions, + note: note, + label: label, + hideDownload: hideDownload + ) + if let error = error, error != .success { + dataSource.uiDelegate?.showError("Error creating: \(error.errorDescription)") + setAllFields(enabled: true) + } else { + dataSource.uiDelegate?.hideOptions(self) + await dataSource.reload() + } + return + } + + Logger.shareOptionsView.info("Editing existing share!") + + guard let controller = controller else { + Logger.shareOptionsView.error("No valid share controller, cannot edit share.") + return + } + let share = controller.share + let permissions = uploadAndEdit + ? share.permissions | NKShare.PermissionValues.updateShare.rawValue + : share.permissions & ~NKShare.PermissionValues.updateShare.rawValue + + setAllFields(enabled: false) + deleteButton.isEnabled = false + saveButton.isEnabled = false + let error = await controller.save( + password: password, + expireDate: expireDate, + permissions: permissions, + note: note, + label: label, + hideDownload: hideDownload + ) + if let error = error, error != .success { + dataSource?.uiDelegate?.showError("Error updating share: \(error.errorDescription)") + setAllFields(enabled: true) + } else { + dataSource?.uiDelegate?.hideOptions(self) + await dataSource?.reload() + } + } + } + + @IBAction func delete(_ sender: Any) { + Task { @MainActor in + guard !createMode else { + dataSource?.uiDelegate?.hideOptions(self) + reset() + return + } + + setAllFields(enabled: false) + deleteButton.isEnabled = false + saveButton.isEnabled = false + let error = await controller?.delete() + if let error = error, error != .success { + dataSource?.uiDelegate?.showError("Error deleting share: \(error.errorDescription)") + } + await dataSource?.reload() + } + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableItemView.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableItemView.swift new file mode 100644 index 000000000..665106293 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableItemView.swift @@ -0,0 +1,64 @@ +// +// ShareTableItemView.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 28/2/24. +// + +import AppKit +import NextcloudKit + +class ShareTableItemView: NSTableCellView { + @IBOutlet private weak var typeImageView: NSImageView! + @IBOutlet private weak var label: NSTextField! + @IBOutlet private weak var copyLinkButton: NSButton! + private var originalCopyImage: NSImage? + private var copiedButtonImage: NSImage? + private var tempButtonTimer: Timer? + + var share: NKShare? { + didSet { + guard let share = share else { + prepareForReuse() + return + } + typeImageView.image = share.typeImage + label.stringValue = share.displayString + copyLinkButton.isHidden = share.shareType != NKShare.ShareType.publicLink.rawValue + } + } + + override func prepareForReuse() { + typeImageView.image = nil + label.stringValue = "" + copyLinkButton.isHidden = false + super.prepareForReuse() + } + + @IBAction func copyShareLink(sender: Any) { + guard let share = share else { return } + let pasteboard = NSPasteboard.general + pasteboard.declareTypes([.string], owner: nil) + pasteboard.setString(share.url, forType: .string) + + guard tempButtonTimer == nil else { return } + + originalCopyImage = copyLinkButton.image + copiedButtonImage = NSImage( + systemSymbolName: "checkmark.circle.fill", + accessibilityDescription: "Public link has been copied icon" + ) + var config = NSImage.SymbolConfiguration(scale: .medium) + if #available(macOS 12.0, *) { + config = config.applying(.init(hierarchicalColor: .systemGreen)) + } + copiedButtonImage = copiedButtonImage?.withSymbolConfiguration(config) + copyLinkButton.image = copiedButtonImage + tempButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { timer in + self.copyLinkButton.image = self.originalCopyImage + self.copiedButtonImage = nil + self.tempButtonTimer?.invalidate() + self.tempButtonTimer = nil + } + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableItemView.xib b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableItemView.xib new file mode 100644 index 000000000..026f2acf2 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableItemView.xib @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableViewDataSource.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableViewDataSource.swift new file mode 100644 index 000000000..1200499ab --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareTableViewDataSource.swift @@ -0,0 +1,254 @@ +// +// ShareTableViewDataSource.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 27/2/24. +// + +import AppKit +import FileProvider +import NextcloudKit +import NextcloudCapabilitiesKit +import OSLog + +class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate { + private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView") + private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil) + private let reattemptInterval: TimeInterval = 3.0 + + var uiDelegate: ShareViewDataSourceUIDelegate? + var sharesTableView: NSTableView? { + didSet { + sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier) + sharesTableView?.rowHeight = 42.0 // Height of view in ShareTableItemView XIB + sharesTableView?.dataSource = self + sharesTableView?.delegate = self + sharesTableView?.reloadData() + } + } + var capabilities: Capabilities? + var itemMetadata: NKFile? + + private(set) var kit: NextcloudKit? + private(set) var itemURL: URL? + private(set) var itemServerRelativePath: String? + private(set) var shares: [NKShare] = [] { + didSet { Task { @MainActor in sharesTableView?.reloadData() } } + } + private var account: NextcloudAccount? { + didSet { + guard let account = account else { return } + kit = NextcloudKit() + kit?.setup( + user: account.username, + userId: account.username, + password: account.password, + urlBase: account.serverUrl + ) + } + } + + func loadItem(url: URL) { + itemServerRelativePath = nil + itemURL = url + Task { + await reload() + } + } + + func reattempt() { + DispatchQueue.main.async { + Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in + Task { await self.reload() } + } + } + } + + func reload() async { + guard let itemURL = itemURL else { return } + guard let itemIdentifier = await withCheckedContinuation({ + (continuation: CheckedContinuation) -> Void in + NSFileProviderManager.getIdentifierForUserVisibleFile( + at: itemURL + ) { identifier, domainIdentifier, error in + defer { continuation.resume(returning: identifier) } + guard error == nil else { + self.presentError("No item with identifier: \(error.debugDescription)") + return + } + } + }) else { + presentError("Could not get identifier for item, no shares can be acquired.") + return + } + + do { + let connection = try await serviceConnection(url: itemURL) + guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier), + let credentials = await connection.credentials() as? Dictionary, + let convertedAccount = NextcloudAccount(dictionary: credentials), + !convertedAccount.password.isEmpty + else { + presentError("Failed to get details from File Provider Extension. Retrying.") + reattempt() + return + } + let serverPathString = serverPath as String + itemServerRelativePath = serverPathString + account = convertedAccount + await sharesTableView?.deselectAll(self) + capabilities = await fetchCapabilities() + guard capabilities?.filesSharing?.apiEnabled == true else { + presentError("Server does not support shares.") + return + } + itemMetadata = await fetchItemMetadata(itemRelativePath: serverPathString) + guard itemMetadata?.permissions.contains("R") == true else { + presentError("This file cannot be shared.") + return + } + shares = await fetch( + itemIdentifier: itemIdentifier, itemRelativePath: serverPathString + ) + } catch let error { + presentError("Could not reload data: \(error), will try again.") + reattempt() + } + } + + private func serviceConnection(url: URL) async throws -> FPUIExtensionService { + let services = try await FileManager().fileProviderServicesForItem(at: url) + guard let service = services[fpUiExtensionServiceName] else { + Logger.sharesDataSource.error("Couldn't get service, required service not present") + throw NSFileProviderError(.providerNotFound) + } + let connection: NSXPCConnection + connection = try await service.fileProviderConnection() + connection.remoteObjectInterface = NSXPCInterface(with: FPUIExtensionService.self) + connection.interruptionHandler = { + Logger.sharesDataSource.error("Service connection interrupted") + } + connection.resume() + guard let proxy = connection.remoteObjectProxy as? FPUIExtensionService else { + throw NSFileProviderError(.serverUnreachable) + } + return proxy + } + + private func fetch( + itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String + ) async -> [NKShare] { + Task { @MainActor in uiDelegate?.fetchStarted() } + defer { Task { @MainActor in uiDelegate?.fetchFinished() } } + + let rawIdentifier = itemIdentifier.rawValue + Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)") + + guard let kit = kit else { + self.presentError("NextcloudKit instance is unavailable, cannot fetch shares!") + return [] + } + + let parameter = NKShareParameter(path: itemRelativePath) + + return await withCheckedContinuation { continuation in + kit.readShares(parameters: parameter) { account, shares, data, error in + let shareCount = shares?.count ?? 0 + Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares") + defer { continuation.resume(returning: shares ?? []) } + guard error == .success else { + self.presentError("Error fetching shares: \(error.errorDescription)") + return + } + } + } + } + + private func fetchCapabilities() async -> Capabilities? { + return await withCheckedContinuation { continuation in + kit?.getCapabilities { account, capabilitiesJson, error in + guard error == .success, let capabilitiesJson = capabilitiesJson else { + self.presentError("Error getting server caps: \(error.errorDescription)") + continuation.resume(returning: nil) + return + } + Logger.sharesDataSource.info("Successfully retrieved server share capabilities") + continuation.resume(returning: Capabilities(data: capabilitiesJson)) + } + } + } + + private func fetchItemMetadata(itemRelativePath: String) async -> NKFile? { + guard let kit = kit else { + presentError("Could not fetch item metadata as NextcloudKit instance is unavailable") + return nil + } + + func slashlessPath(_ string: String) -> String { + var strCopy = string + if strCopy.hasPrefix("/") { + strCopy.removeFirst() + } + if strCopy.hasSuffix("/") { + strCopy.removeLast() + } + return strCopy + } + + let nkCommon = kit.nkCommonInstance + let urlBase = slashlessPath(nkCommon.urlBase) + let davSuffix = slashlessPath(nkCommon.dav) + let userId = nkCommon.userId + let itemRelPath = slashlessPath(itemRelativePath) + + let itemFullServerPath = "\(urlBase)/\(davSuffix)/files/\(userId)/\(itemRelPath)" + return await withCheckedContinuation { continuation in + kit.readFileOrFolder(serverUrlFileName: itemFullServerPath, depth: "0") { + account, files, data, error in + guard error == .success else { + self.presentError("Error getting item metadata: \(error.errorDescription)") + continuation.resume(returning: nil) + return + } + Logger.sharesDataSource.info("Successfully retrieved item metadata") + continuation.resume(returning: files.first) + } + } + } + + private func presentError(_ errorString: String) { + Logger.sharesDataSource.error("\(errorString, privacy: .public)") + Task { @MainActor in self.uiDelegate?.showError(errorString) } + } + + // MARK: - NSTableViewDataSource protocol methods + + @objc func numberOfRows(in tableView: NSTableView) -> Int { + shares.count + } + + // MARK: - NSTableViewDelegate protocol methods + + @objc func tableView( + _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int + ) -> NSView? { + let share = shares[row] + guard let view = tableView.makeView( + withIdentifier: shareItemViewIdentifier, owner: self + ) as? ShareTableItemView else { + Logger.sharesDataSource.error("Acquired item view from table is not a share item view!") + return nil + } + view.share = share + return view + } + + @objc func tableViewSelectionDidChange(_ notification: Notification) { + guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else { + Task { @MainActor in uiDelegate?.hideOptions(self) } + return + } + let share = shares[selectedRow] + Task { @MainActor in uiDelegate?.showOptions(share: share) } + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewController.swift new file mode 100644 index 000000000..55955f143 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewController.swift @@ -0,0 +1,176 @@ +// +// ShareViewController.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 21/2/24. +// + +import AppKit +import FileProvider +import NextcloudKit +import OSLog +import QuickLookThumbnailing + +class ShareViewController: NSViewController, ShareViewDataSourceUIDelegate { + let shareDataSource = ShareTableViewDataSource() + let itemIdentifiers: [NSFileProviderItemIdentifier] + + @IBOutlet weak var fileNameIcon: NSImageView! + @IBOutlet weak var fileNameLabel: NSTextField! + @IBOutlet weak var descriptionLabel: NSTextField! + @IBOutlet weak var createButton: NSButton! + @IBOutlet weak var closeButton: NSButton! + @IBOutlet weak var tableView: NSTableView! + @IBOutlet weak var optionsView: ShareOptionsView! + @IBOutlet weak var splitView: NSSplitView! + @IBOutlet weak var loadingEffectView: NSVisualEffectView! + @IBOutlet weak var loadingIndicator: NSProgressIndicator! + @IBOutlet weak var errorMessageStackView: NSStackView! + @IBOutlet weak var errorTextLabel: NSTextField! + @IBOutlet weak var noSharesLabel: NSTextField! + + public override var nibName: NSNib.Name? { + return NSNib.Name(self.className) + } + + var actionViewController: DocumentActionViewController! { + return parent as? DocumentActionViewController + } + + init(_ itemIdentifiers: [NSFileProviderItemIdentifier]) { + self.itemIdentifiers = itemIdentifiers + super.init(nibName: nil, bundle: nil) + + guard let firstItem = itemIdentifiers.first else { + Logger.shareViewController.error("called without items") + closeAction(self) + return + } + + Task { + await processItemIdentifier(firstItem) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + dismissError(self) + hideOptions(self) + } + + @IBAction func closeAction(_ sender: Any) { + actionViewController.extensionContext.completeRequest() + } + + private func processItemIdentifier(_ itemIdentifier: NSFileProviderItemIdentifier) async { + guard let manager = NSFileProviderManager(for: actionViewController.domain) else { + fatalError("NSFileProviderManager isn't expected to fail") + } + + do { + let itemUrl = try await manager.getUserVisibleURL(for: itemIdentifier) + await updateDisplay(itemUrl: itemUrl) + shareDataSource.uiDelegate = self + shareDataSource.sharesTableView = tableView + shareDataSource.loadItem(url: itemUrl) + optionsView.dataSource = shareDataSource + } catch let error { + let errorString = "Error processing item: \(error)" + Logger.shareViewController.error("\(errorString)") + fileNameLabel.stringValue = "Unknown item" + descriptionLabel.stringValue = errorString + } + } + + private func updateDisplay(itemUrl: URL) async { + fileNameLabel.stringValue = itemUrl.lastPathComponent + + let request = QLThumbnailGenerator.Request( + fileAt: itemUrl, + size: CGSize(width: 128, height: 128), + scale: 1.0, + representationTypes: .icon + ) + + let generator = QLThumbnailGenerator.shared + let fileThumbnail = await withCheckedContinuation { continuation in + generator.generateRepresentations(for: request) { thumbnail, type, error in + if thumbnail == nil || error != nil { + Logger.shareViewController.error("Could not get thumbnail: \(error)") + } + continuation.resume(returning: thumbnail) + } + } + fileNameIcon.image = fileThumbnail?.nsImage + + let resourceValues = try? itemUrl.resourceValues( + forKeys: [.fileSizeKey, .contentModificationDateKey] + ) + var sizeDesc = "Unknown size" + var modDesc = "Unknown modification date" + if let fileSize = resourceValues?.fileSize { + sizeDesc = ByteCountFormatter().string(fromByteCount: Int64(fileSize)) + } + if let modificationDate = resourceValues?.contentModificationDate { + let modDateString = DateFormatter.localizedString( + from: modificationDate, dateStyle: .short, timeStyle: .short + ) + modDesc = "Last modified: \(modDateString)" + } + descriptionLabel.stringValue = "\(sizeDesc) · \(modDesc)" + } + + @IBAction func dismissError(_ sender: Any) { + errorMessageStackView.isHidden = true + } + + @IBAction func createShare(_ sender: Any) { + guard let kit = shareDataSource.kit else { return } + optionsView.kit = kit + optionsView.createMode = true + tableView.deselectAll(self) + if !splitView.arrangedSubviews.contains(optionsView) { + splitView.addArrangedSubview(optionsView) + optionsView.isHidden = false + } + } + + func fetchStarted() { + loadingEffectView.isHidden = false + loadingIndicator.startAnimation(self) + } + + func fetchFinished() { + noSharesLabel.isHidden = !shareDataSource.shares.isEmpty + loadingEffectView.isHidden = true + loadingIndicator.stopAnimation(self) + } + + func hideOptions(_ sender: Any) { + if sender as? ShareTableViewDataSource == shareDataSource, optionsView.createMode { + // Do not hide options if the table view has had everything deselected when we set the + // options view to be in create mode + return + } + splitView.removeArrangedSubview(optionsView) + optionsView.isHidden = true + } + + func showOptions(share: NKShare) { + guard let kit = shareDataSource.kit else { return } + optionsView.kit = kit + optionsView.controller = ShareController(share: share, kit: kit) + if !splitView.arrangedSubviews.contains(optionsView) { + splitView.addArrangedSubview(optionsView) + optionsView.isHidden = false + } + } + + func showError(_ errorString: String) { + errorMessageStackView.isHidden = false + errorTextLabel.stringValue = errorString + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewController.xib b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewController.xib new file mode 100644 index 000000000..7a61de703 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewController.xib @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewDataSourceUIDelegate.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewDataSourceUIDelegate.swift new file mode 100644 index 000000000..09bd3f7e4 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareViewDataSourceUIDelegate.swift @@ -0,0 +1,17 @@ +// +// ShareViewDataSourceUIDelegate.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 28/2/24. +// + +import Foundation +import NextcloudKit + +protocol ShareViewDataSourceUIDelegate { + func fetchStarted() + func fetchFinished() + func hideOptions(_ sender: Any) + func showOptions(share: NKShare) + func showError(_ errorString: String) +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareeSuggestionsDataSource.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareeSuggestionsDataSource.swift new file mode 100644 index 000000000..cc1b91fe3 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/ShareeSuggestionsDataSource.swift @@ -0,0 +1,60 @@ +// +// ShareeSuggestionsDataSource.swift +// FileProviderUIExt +// +// Created by Claudio Cambra on 2/4/24. +// + +import Foundation +import NextcloudKit +import OSLog +import SuggestionsTextFieldKit + +class ShareeSuggestionsDataSource: SuggestionsDataSource { + let kit: NextcloudKit + var suggestions: [Suggestion] = [] + var inputString: String = "" { + didSet { Task { await updateSuggestions() } } + } + + init(kit: NextcloudKit) { + self.kit = kit + } + + private func updateSuggestions() async { + let sharees = await fetchSharees(search: inputString) + Logger.shareeDataSource.info("Fetched \(sharees.count, privacy: .public) sharees.") + suggestions = suggestionsFromSharees(sharees) + NotificationCenter.default.post(name: SuggestionsChangedNotificationName, object: self) + } + + private func fetchSharees(search: String) async -> [NKSharee] { + Logger.shareeDataSource.debug("Searching sharees with: \(search, privacy: .public)") + return await withCheckedContinuation { continuation in + kit.searchSharees( + search: inputString, + page: 1, + perPage: 20, + completion: { account, sharees, data, error in + defer { continuation.resume(returning: sharees ?? []) } + guard error == .success else { + Logger.shareeDataSource.error( + "Error fetching sharees: \(error.description, privacy: .public)" + ) + return + } + } + ) + } + } + + private func suggestionsFromSharees(_ sharees: [NKSharee]) -> [Suggestion] { + return sharees.map { + Suggestion( + imageName: "person.fill", + displayText: $0.label.isEmpty ? $0.name : $0.label, + data: $0 + ) + } + } +} diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj index 6149378d5..e830d9ac8 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 5307A6E82965DAD8001E0C6A /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6E72965DAD8001E0C6A /* NextcloudKit */; }; 5307A6EB2965DB8D001E0C6A /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5307A6EA2965DB8D001E0C6A /* RealmSwift */; }; 5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */; }; + 531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 531522812B8E01C6002E31BE /* ShareTableItemView.xib */; }; 5318AD9129BF42FB00CBB71C /* NextcloudItemMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */; }; 5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */; }; 5318AD9729BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */; }; @@ -20,9 +21,19 @@ 5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36729DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift */; }; 5352B36C29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */; }; 5352E85B29B7BFE6002CE85C /* Progress+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */; }; + 5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */; }; 535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */; }; + 53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */; }; + 53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */; }; 536EFBF7295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */; }; 536EFC36295E3C1100F4CB13 /* NextcloudAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */; }; + 5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5374FD432B95EE1400C78D54 /* ShareController.swift */; }; + 5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */; }; + 537630912B85F4980026BFAB /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 537630902B85F4980026BFAB /* ShareViewController.xib */; }; + 537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630922B85F4B00026BFAB /* ShareViewController.swift */; }; + 537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */; }; + 537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; }; + 537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537630962B860D920026BFAB /* FPUIExtensionService.swift */; }; 538E396A27F4765000FA63D5 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */; }; 538E396D27F4765000FA63D5 /* FileProviderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */; }; 538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538E396E27F4765000FA63D5 /* FileProviderItem.swift */; }; @@ -39,11 +50,19 @@ 53903D352956184400D0B308 /* LocalSocketClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 539158B127BE891500816F56 /* LocalSocketClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 53903D37295618A400D0B308 /* LineProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 53903D36295618A400D0B308 /* LineProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 539158AC27BE71A900816F56 /* FinderSyncSocketLineProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */; }; + 53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */; }; 53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D056302970594F00988392 /* LocalFilesUtils.swift */; }; 53D666612B70C9A70042C03D /* FileProviderConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D666602B70C9A70042C03D /* FileProviderConfig.swift */; }; 53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */; }; 53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */; }; 53ED473029C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */; }; + 53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */; }; + 53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 53FE14532B8E1219006C4193 /* NextcloudKit */; }; + 53FE14552B8E28E9006C4193 /* NextcloudAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */; }; + 53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */; }; + 53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */; }; + 53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */; }; + 53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */; }; C2B573BA1B1CD91E00303B36 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; }; C2B573D21B1CD94B00303B36 /* main.m in Resources */ = {isa = PBXBuildFile; fileRef = C2B573B91B1CD91E00303B36 /* main.m */; }; C2B573DE1B1CD9CE00303B36 /* FinderSync.m in Sources */ = {isa = PBXBuildFile; fileRef = C2B573DD1B1CD9CE00303B36 /* FinderSync.m */; }; @@ -143,6 +162,7 @@ /* Begin PBXFileReference section */ 5307A6F129675346001E0C6A /* NextcloudFilesDatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudFilesDatabaseManager.swift; sourceTree = ""; }; + 531522812B8E01C6002E31BE /* ShareTableItemView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareTableItemView.xib; sourceTree = ""; }; 5318AD9029BF42FB00CBB71C /* NextcloudItemMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudItemMetadataTable.swift; sourceTree = ""; }; 5318AD9429BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudLocalFileMetadataTable.swift; sourceTree = ""; }; 5318AD9629BF493600CBB71C /* FileProviderMaterialisedEnumerationObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderMaterialisedEnumerationObserver.swift; sourceTree = ""; }; @@ -155,8 +175,16 @@ 5352B36B29DC44B50011CE03 /* FileProviderExtension+Thumbnailing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+Thumbnailing.swift"; sourceTree = ""; }; 5352E85A29B7BFE6002CE85C /* Progress+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+Extensions.swift"; sourceTree = ""; }; 535AE30D29C0A2CC0042A9BA /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = ""; }; + 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareeSuggestionsDataSource.swift; sourceTree = ""; }; 536EFBF6295CF58100F4CB13 /* FileProviderSocketLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderSocketLineProcessor.swift; sourceTree = ""; }; 536EFC35295E3C1100F4CB13 /* NextcloudAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudAccount.swift; sourceTree = ""; }; + 5374FD432B95EE1400C78D54 /* ShareController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareController.swift; sourceTree = ""; }; + 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+Extensions.swift"; sourceTree = ""; }; + 5376307E2B85E5650026BFAB /* FileProviderUIExt.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = FileProviderUIExt.entitlements; sourceTree = ""; }; + 537630902B85F4980026BFAB /* ShareViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareViewController.xib; sourceTree = ""; }; + 537630922B85F4B00026BFAB /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionServiceSource.swift; sourceTree = ""; }; + 537630962B860D920026BFAB /* FPUIExtensionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUIExtensionService.swift; sourceTree = ""; }; 538E396727F4765000FA63D5 /* FileProviderExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 538E396927F4765000FA63D5 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; 538E396C27F4765000FA63D5 /* FileProviderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderExtension.swift; sourceTree = ""; }; @@ -172,11 +200,20 @@ 539158AB27BE71A900816F56 /* FinderSyncSocketLineProcessor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FinderSyncSocketLineProcessor.m; sourceTree = ""; }; 539158B127BE891500816F56 /* LocalSocketClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalSocketClient.h; sourceTree = ""; }; 539158B227BEC98A00816F56 /* LocalSocketClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalSocketClient.m; sourceTree = ""; }; + 53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderUIExt.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentActionViewController.swift; sourceTree = ""; }; + 53B979852B84C81F002DA742 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53D056302970594F00988392 /* LocalFilesUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFilesUtils.swift; sourceTree = ""; }; 53D666602B70C9A70042C03D /* FileProviderConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderConfig.swift; sourceTree = ""; }; 53ED471F29C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderEnumerator+SyncEngine.swift"; sourceTree = ""; }; 53ED472729C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NextcloudItemMetadataTable+NKFile.swift"; sourceTree = ""; }; 53ED472F29C9CE0B00795DB1 /* FileProviderExtension+ClientInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProviderExtension+ClientInterface.swift"; sourceTree = ""; }; + 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableViewDataSource.swift; sourceTree = ""; }; + 53FE14572B8E3A7C006C4193 /* FileProviderUIExtRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FileProviderUIExtRelease.entitlements; sourceTree = ""; }; + 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareTableItemView.swift; sourceTree = ""; }; + 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NKShare+Extensions.swift"; sourceTree = ""; }; + 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewDataSourceUIDelegate.swift; sourceTree = ""; }; + 53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareOptionsView.swift; sourceTree = ""; }; C2B573B11B1CD91E00303B36 /* desktopclient.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = desktopclient.app; sourceTree = BUILT_PRODUCTS_DIR; }; C2B573B51B1CD91E00303B36 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C2B573B91B1CD91E00303B36 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -211,6 +248,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 53B9797B2B84C81F002DA742 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5358F2B92BAA0F5300E3C729 /* NextcloudCapabilitiesKit in Frameworks */, + 53651E442BBC0CA300ECAC29 /* SuggestionsTextFieldKit in Frameworks */, + 53FE14542B8E1219006C4193 /* NextcloudKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C2B573AE1B1CD91E00303B36 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -249,6 +296,8 @@ children = ( 5350E4E72B0C514400F276CB /* ClientCommunicationProtocol.h */, 5350E4E82B0C534A00F276CB /* ClientCommunicationService.swift */, + 537630962B860D920026BFAB /* FPUIExtensionService.swift */, + 537630942B860D560026BFAB /* FPUIExtensionServiceSource.swift */, ); path = Services; sourceTree = ""; @@ -263,6 +312,15 @@ path = Extensions; sourceTree = ""; }; + 5376307B2B85E2E00026BFAB /* Extensions */ = { + isa = PBXGroup; + children = ( + 5376307C2B85E2ED0026BFAB /* Logger+Extensions.swift */, + 53FE145A2B8F1305006C4193 /* NKShare+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; 538E396827F4765000FA63D5 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -306,12 +364,34 @@ path = NCDesktopClientSocketKit; sourceTree = ""; }; + 53B9797F2B84C81F002DA742 /* FileProviderUIExt */ = { + isa = PBXGroup; + children = ( + 5376307B2B85E2E00026BFAB /* Extensions */, + 53B979802B84C81F002DA742 /* DocumentActionViewController.swift */, + 5374FD432B95EE1400C78D54 /* ShareController.swift */, + 53651E452BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift */, + 53FE14662B8F78B6006C4193 /* ShareOptionsView.swift */, + 53FE14582B8E3F6C006C4193 /* ShareTableItemView.swift */, + 531522812B8E01C6002E31BE /* ShareTableItemView.xib */, + 53FE144F2B8E0658006C4193 /* ShareTableViewDataSource.swift */, + 537630922B85F4B00026BFAB /* ShareViewController.swift */, + 537630902B85F4980026BFAB /* ShareViewController.xib */, + 53FE14642B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift */, + 5376307E2B85E5650026BFAB /* FileProviderUIExt.entitlements */, + 53FE14572B8E3A7C006C4193 /* FileProviderUIExtRelease.entitlements */, + 53B979852B84C81F002DA742 /* Info.plist */, + ); + path = FileProviderUIExt; + sourceTree = ""; + }; C2B573941B1CD88000303B36 = { isa = PBXGroup; children = ( C2B573B31B1CD91E00303B36 /* desktopclient */, C2B573D81B1CD9CE00303B36 /* FinderSyncExt */, 538E396B27F4765000FA63D5 /* FileProviderExt */, + 53B9797F2B84C81F002DA742 /* FileProviderUIExt */, 53903D0D2956164F00D0B308 /* NCDesktopClientSocketKit */, 538E396827F4765000FA63D5 /* Frameworks */, C2B573B21B1CD91E00303B36 /* Products */, @@ -325,6 +405,7 @@ C2B573D71B1CD9CE00303B36 /* FinderSyncExt.appex */, 538E396727F4765000FA63D5 /* FileProviderExt.appex */, 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */, + 53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */, ); name = Products; sourceTree = ""; @@ -430,6 +511,29 @@ productReference = 53903D0C2956164F00D0B308 /* NCDesktopClientSocketKit.framework */; productType = "com.apple.product-type.framework"; }; + 53B9797D2B84C81F002DA742 /* FileProviderUIExt */ = { + isa = PBXNativeTarget; + buildConfigurationList = 53B979882B84C820002DA742 /* Build configuration list for PBXNativeTarget "FileProviderUIExt" */; + buildPhases = ( + 53B9797A2B84C81F002DA742 /* Sources */, + 53B9797B2B84C81F002DA742 /* Frameworks */, + 53B9797C2B84C81F002DA742 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 53FE14522B8E1213006C4193 /* PBXTargetDependency */, + ); + name = FileProviderUIExt; + packageProductDependencies = ( + 53FE14532B8E1219006C4193 /* NextcloudKit */, + 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */, + 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */, + ); + productName = FileProviderUIExt; + productReference = 53B9797E2B84C81F002DA742 /* FileProviderUIExt.appex */; + productType = "com.apple.product-type.app-extension"; + }; C2B573B01B1CD91E00303B36 /* desktopclient */ = { isa = PBXNativeTarget; buildConfigurationList = C2B573CC1B1CD91E00303B36 /* Build configuration list for PBXNativeTarget "desktopclient" */; @@ -482,7 +586,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1240; TargetAttributes = { 538E396627F4765000FA63D5 = { @@ -492,6 +596,9 @@ CreatedOnToolsVersion = 14.2; ProvisioningStyle = Manual; }; + 53B9797D2B84C81F002DA742 = { + CreatedOnToolsVersion = 15.2; + }; C2B573B01B1CD91E00303B36 = { CreatedOnToolsVersion = 6.3.1; DevelopmentTeam = 9B5WD74GWJ; @@ -522,6 +629,8 @@ packageReferences = ( 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */, 5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */, + 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */, + 53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */, ); productRefGroup = C2B573B21B1CD91E00303B36 /* Products */; projectDirPath = ""; @@ -530,6 +639,7 @@ C2B573B01B1CD91E00303B36 /* desktopclient */, C2B573D61B1CD9CE00303B36 /* FinderSyncExt */, 538E396627F4765000FA63D5 /* FileProviderExt */, + 53B9797D2B84C81F002DA742 /* FileProviderUIExt */, 53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */, ); }; @@ -550,6 +660,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 53B9797C2B84C81F002DA742 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 531522822B8E01C6002E31BE /* ShareTableItemView.xib in Resources */, + 537630912B85F4980026BFAB /* ShareViewController.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C2B573AF1B1CD91E00303B36 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -603,9 +722,11 @@ 53ED472029C5E64200795DB1 /* FileProviderEnumerator+SyncEngine.swift in Sources */, 5318AD9929BF58D000CBB71C /* NKError+Extensions.swift in Sources */, 53ED472829C88E7000795DB1 /* NextcloudItemMetadataTable+NKFile.swift in Sources */, + 537630972B860D920026BFAB /* FPUIExtensionService.swift in Sources */, 5318AD9529BF438F00CBB71C /* NextcloudLocalFileMetadataTable.swift in Sources */, 535AE30E29C0A2CC0042A9BA /* Logger+Extensions.swift in Sources */, 5307A6F229675346001E0C6A /* NextcloudFilesDatabaseManager.swift in Sources */, + 537630952B860D560026BFAB /* FPUIExtensionServiceSource.swift in Sources */, 53D056312970594F00988392 /* LocalFilesUtils.swift in Sources */, 538E396F27F4765000FA63D5 /* FileProviderItem.swift in Sources */, 5352B36829DC17D60011CE03 /* NextcloudFilesDatabaseManager+LocalFiles.swift in Sources */, @@ -626,6 +747,25 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 53B9797A2B84C81F002DA742 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 537630932B85F4B00026BFAB /* ShareViewController.swift in Sources */, + 53FE14672B8F78B6006C4193 /* ShareOptionsView.swift in Sources */, + 53651E462BBC0D9500ECAC29 /* ShareeSuggestionsDataSource.swift in Sources */, + 53FE14652B8F6700006C4193 /* ShareViewDataSourceUIDelegate.swift in Sources */, + 53B979812B84C81F002DA742 /* DocumentActionViewController.swift in Sources */, + 5374FD442B95EE1400C78D54 /* ShareController.swift in Sources */, + 53FE145B2B8F1305006C4193 /* NKShare+Extensions.swift in Sources */, + 53FE14592B8E3F6C006C4193 /* ShareTableItemView.swift in Sources */, + 53FE14552B8E28E9006C4193 /* NextcloudAccount.swift in Sources */, + 5376307D2B85E2ED0026BFAB /* Logger+Extensions.swift in Sources */, + 53FE14502B8E0658006C4193 /* ShareTableViewDataSource.swift in Sources */, + 537630982B8612F00026BFAB /* FPUIExtensionService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C2B573AD1B1CD91E00303B36 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -666,6 +806,10 @@ target = 53903D0B2956164F00D0B308 /* NCDesktopClientSocketKit */; targetProxy = 53903D322956173F00D0B308 /* PBXContainerItemProxy */; }; + 53FE14522B8E1213006C4193 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 53FE14512B8E1213006C4193 /* NextcloudKit */; + }; C2B573E01B1CD9CE00303B36 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C2B573D61B1CD9CE00303B36 /* FinderSyncExt */; @@ -910,6 +1054,130 @@ }; name = Release; }; + 53B979862B84C81F002DA742 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = FileProviderUIExt/FileProviderUIExt.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + 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 = FileProviderUIExt/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = FileProviderUIExt; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_OUTPUT_FORMAT = "same-as-input"; + INFOPLIST_PREPROCESS = NO; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = macOS; + }; + name = Debug; + }; + 53B979872B84C81F002DA742 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = FileProviderUIExt/FileProviderUIExtRelease.entitlements; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + 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 = FileProviderUIExt/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = FileProviderUIExt; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INFOPLIST_OUTPUT_FORMAT = "same-as-input"; + INFOPLIST_PREPROCESS = NO; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "$(OC_APPLICATION_REV_DOMAIN).$(PRODUCT_NAME)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = macOS; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; C2B573991B1CD88000303B36 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1216,6 +1484,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 53B979882B84C820002DA742 /* Build configuration list for PBXNativeTarget "FileProviderUIExt" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 53B979862B84C81F002DA742 /* Debug */, + 53B979872B84C81F002DA742 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C2B573981B1CD88000303B36 /* Build configuration list for PBXProject "NextcloudIntegration" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1250,8 +1527,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit.git"; requirement = { - branch = develop; - kind = branch; + kind = upToNextMajorVersion; + minimumVersion = 2.5.9; }; }; 5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */ = { @@ -1262,6 +1539,22 @@ version = 10.33.0; }; }; + 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/claucambra/NextcloudCapabilitiesKit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; + 53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/claucambra/SuggestionsTextFieldKit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1280,6 +1573,26 @@ package = 5307A6E92965DB57001E0C6A /* XCRemoteSwiftPackageReference "realm-swift" */; productName = RealmSwift; }; + 5358F2B82BAA0F5300E3C729 /* NextcloudCapabilitiesKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5358F2B72BAA045E00E3C729 /* XCRemoteSwiftPackageReference "NextcloudCapabilitiesKit" */; + productName = NextcloudCapabilitiesKit; + }; + 53651E432BBC0CA300ECAC29 /* SuggestionsTextFieldKit */ = { + isa = XCSwiftPackageProductDependency; + package = 53651E422BBC0C7F00ECAC29 /* XCRemoteSwiftPackageReference "SuggestionsTextFieldKit" */; + productName = SuggestionsTextFieldKit; + }; + 53FE14512B8E1213006C4193 /* NextcloudKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */; + productName = NextcloudKit; + }; + 53FE14532B8E1219006C4193 /* NextcloudKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5307A6E42965C6FA001E0C6A /* XCRemoteSwiftPackageReference "NextcloudKit" */; + productName = NextcloudKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C2B573951B1CD88000303B36 /* Project object */;