diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift
index 6ad8cb0286fbf..3d322c830661a 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Package.swift
@@ -18,7 +18,7 @@ let package = Package(
)
],
dependencies: [
- .package(url: "https://github.com/nextcloud/NextcloudCapabilitiesKit.git", from: "2.4.0"),
+ .package(url: "https://github.com/nextcloud/NextcloudCapabilitiesKit.git", from: "2.5.0"),
.package(url: "https://github.com/nextcloud/NextcloudKit", from: "7.2.3"),
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.55.0"),
.package(url: "https://github.com/realm/realm-swift.git", from: "20.0.1"),
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift
index 0b49d34dd2984..8a30b5f11ceb6 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Create.swift
@@ -95,12 +95,15 @@ public extension Item {
directory.downloaded = true
dbManager.addItemMetadata(directory)
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: directory.contentType)
+
let fpItem = await Item(
metadata: directory,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
)
@@ -218,12 +221,15 @@ public extension Item {
dbManager.addItemMetadata(newMetadata)
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: newMetadata.contentType)
+
let fpItem = await Item(
metadata: newMetadata,
parentItemIdentifier: itemTemplate.parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
)
@@ -422,12 +428,15 @@ public extension Item {
progress.completedUnitCount += 1
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: bundleRootMetadata.contentType)
+
return await Item(
metadata: bundleRootMetadata,
parentItemIdentifier: rootItem.parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
)
@@ -597,12 +606,15 @@ public extension Item {
)
}
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: itemMetadata.contentType)
+
item = await Item(
metadata: itemMetadata,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift
index 96e5379ace208..b324a2ba3c5fe 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift
@@ -219,12 +219,15 @@ public extension Item {
return (nil, nil, NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier))
}
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: updatedMetadata.contentType)
+
let fpItem = await Item(
metadata: updatedMetadata,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: logger.log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift
index e22e275eeb918..1159a755ecef0 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Ignored.swift
@@ -63,6 +63,7 @@ extension Item {
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: false,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift
index ab8e158c980e4..4b41af0178826 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+LockFile.swift
@@ -172,6 +172,7 @@ extension Item {
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: false,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: log
),
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift
index b6d18d2e4f585..68f10bd343ad2 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Modify.swift
@@ -78,12 +78,15 @@ public extension Item {
)
}
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: newMetadata.contentType)
+
let modifiedItem = await Item(
metadata: newMetadata,
parentItemIdentifier: newParentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: logger.log
)
@@ -209,12 +212,15 @@ public extension Item {
dbManager.addItemMetadata(newMetadata)
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: newMetadata.contentType)
+
let modifiedItem = await Item(
metadata: newMetadata,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: logger.log
)
@@ -511,12 +517,15 @@ public extension Item {
progress.completedUnitCount += 1
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: bundleRootMetadata.contentType)
+
return await Item(
metadata: bundleRootMetadata,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: logger.log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift
index a14c6b03bbb57..80b6186f0cd77 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Trash.swift
@@ -30,6 +30,7 @@ extension Item {
}
let dirtyChildren = dbManager.childItems(directoryMetadata: dirtyMetadata)
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: modifiedItem.remoteInterface, candidate: dirtyMetadata.contentType)
let dirtyItem = await Item(
metadata: dirtyMetadata,
@@ -37,6 +38,7 @@ extension Item {
account: account,
remoteInterface: modifiedItem.remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: modifiedItem.remoteInterface.supportsTrash(account: account),
log: log
)
@@ -86,6 +88,7 @@ extension Item {
account: account,
remoteInterface: modifiedItem.remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: modifiedItem.remoteInterface.supportsTrash(account: account),
log: log
)
@@ -169,6 +172,7 @@ extension Item {
}
dbManager.addItemMetadata(restoredItemMetadata)
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: restoredItemMetadata.contentType)
return await (Item(
metadata: restoredItemMetadata,
@@ -176,6 +180,7 @@ extension Item {
account: account,
remoteInterface: modifiedItem.remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: modifiedItem.remoteInterface.supportsTrash(account: account),
log: log
), nil)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift
index fe015ec4bab31..d390b76151c8e 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+Unuploaded.swift
@@ -116,12 +116,15 @@ extension Item {
modifiedMetadata.date = newModificationDate
}
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: modifiedMetadata.contentType)
+
return await Item(
metadata: modifiedMetadata,
parentItemIdentifier: modifiedParentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteInterface.supportsTrash(account: account),
log: logger.log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift
new file mode 100644
index 0000000000000..75f9355ddbfd0
--- /dev/null
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+getContextMenuItemTypeFilters.swift
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+extension Item {
+ ///
+ /// Gets all MIME type filters from the server capabilities for comparison.
+ ///
+ /// - Parameters:
+ /// - account: The account identifier for the server to check.
+ /// - remoteInterface: The server proxy object to use.
+ ///
+ /// - Returns: An array of strings as provided by NextcloudCapabilitiesKit or an empty array in case of error.
+ ///
+ static func getContextMenuItemTypeFilters(account: Account, remoteInterface: RemoteInterface) async -> [String] {
+ let (_, capabilities, _, capabilitiesError) = await remoteInterface.currentCapabilities(account: account, options: .init(), taskHandler: { _ in })
+
+ if capabilitiesError == .success {
+ if let capabilities {
+ if let apps = capabilities.clientIntegration?.apps {
+ return apps.flatMap(\.contextMenuItems).flatMap(\.filters)
+ }
+ }
+ }
+
+ return []
+ }
+}
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift
new file mode 100644
index 0000000000000..ef363a39189ff
--- /dev/null
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item+typeHasApplicableContextMenuItems.swift
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+extension Item {
+ ///
+ /// Convenience wrapper for ``getContextMenuItemTypeFilters(account:remoteInterface:)`` and ``typeHasApplicableContextMenuItems(filters:candidate:)``.
+ ///
+ /// Depending on the call site, it might be more efficient to call both methods individually to avoid redundant capability checks or circumvent boundaries of synchronous and asynchronous contexts.
+ ///
+ /// - Parameters:
+ /// - candidate: The MIME type of the file provider item to check.
+ ///
+ /// - Returns: `true`, if the candidate MIME type is covered by the list of filters provided, otherwise `false`.
+ ///
+ static func typeHasApplicableContextMenuItems(account: Account, remoteInterface: RemoteInterface, candidate: String) async -> Bool {
+ let filters = await getContextMenuItemTypeFilters(account: account, remoteInterface: remoteInterface)
+ return typeHasApplicableContextMenuItems(filters: filters, candidate: candidate)
+ }
+
+ ///
+ /// Check whether the MIME type of an item matches any type filter of server-defined context menu items.
+ ///
+ /// - Parameters:
+ /// - filters: A list of MIME type filter strings and prefixes as provided by the server.
+ /// - candidate: The MIME type of the file provider item to check.
+ ///
+ /// - Returns: `true`, if the candidate MIME type is covered by the list of filters provided, otherwise `false`.
+ ///
+ static func typeHasApplicableContextMenuItems(filters: [String], candidate: String) -> Bool {
+ filters.first(where: { candidate.hasPrefix($0) }) != nil
+ }
+}
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift
index 5bf4b3d78ade5..92db85fc84658 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Item/Item.swift
@@ -20,6 +20,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
public let account: Account
public let remoteInterface: RemoteInterface
+ private let displayFileActions: Bool
private let remoteSupportsTrash: Bool
public var itemIdentifier: NSFileProviderItemIdentifier {
@@ -188,6 +189,8 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
public var userInfo: [AnyHashable: Any]? {
var userInfoDict = [AnyHashable: Any]()
+ userInfoDict["displayFileActions"] = displayFileActions
+
if metadata.lock {
// Can be used to display lock/unlock context menu entries for FPUIActions
// Note that only files, not folders, should be lockable/unlockable
@@ -199,11 +202,10 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
userInfoDict["displayEvict"] = metadata.downloaded && !metadata.keepDownloaded
// https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html
- if metadata.permissions.uppercased().contains("R"), // Shareable
- ![.rootContainer, .trashContainer].contains(itemIdentifier)
- {
+ if metadata.permissions.uppercased().contains("R") /* Shareable */, ![.rootContainer, .trashContainer].contains(itemIdentifier) {
userInfoDict["displayShare"] = true
}
+
return userInfoDict
}
@@ -265,6 +267,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: false,
remoteSupportsTrash: remoteSupportsTrash,
log: log
)
@@ -308,6 +311,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: false,
remoteSupportsTrash: remoteSupportsTrash,
log: log
)
@@ -321,6 +325,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
account: Account,
remoteInterface: RemoteInterface,
dbManager: FilesDatabaseManager,
+ displayFileActions: Bool,
remoteSupportsTrash: Bool,
log: any FileProviderLogging
) {
@@ -330,6 +335,7 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
logger = FileProviderLogger(category: "Item", log: log)
self.remoteInterface = remoteInterface
self.dbManager = dbManager
+ self.displayFileActions = displayFileActions
self.remoteSupportsTrash = remoteSupportsTrash
super.init()
}
@@ -382,12 +388,17 @@ public final class Item: NSObject, NSFileProviderItem, Sendable {
return nil
}
+ // Display File Actions
+
+ let displayFileActions = await Item.typeHasApplicableContextMenuItems(account: account, remoteInterface: remoteInterface, candidate: metadata.contentType)
+
return Item(
metadata: metadata,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteSupportsTrash,
log: log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift
index 5752d354f3e27..b39be8f6b99eb 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Sources/NextcloudFileProviderKit/Metadata/SendableItemMetadata+Array.swift
@@ -10,6 +10,7 @@ extension [SendableItemMetadata] {
func toFileProviderItems(account: Account, remoteInterface: RemoteInterface, dbManager: FilesDatabaseManager, log: any FileProviderLogging) async throws -> [Item] {
let logger = FileProviderLogger(category: "toFileProviderItems", log: log)
let remoteSupportsTrash = await remoteInterface.supportsTrash(account: account)
+ let allFilters = await Item.getContextMenuItemTypeFilters(account: account, remoteInterface: remoteInterface)
return try await concurrentChunkedCompactMap { (itemMetadata: SendableItemMetadata) -> Item? in
guard !itemMetadata.e2eEncrypted else {
@@ -28,12 +29,15 @@ extension [SendableItemMetadata] {
throw FilesDatabaseManager.parentMetadataNotFoundError(itemUrl: targetUrl)
}
+ let displayFileActions = Item.typeHasApplicableContextMenuItems(filters: allFilters, candidate: itemMetadata.contentType)
+
let item = Item(
metadata: itemMetadata,
parentItemIdentifier: parentItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: displayFileActions,
remoteSupportsTrash: remoteSupportsTrash,
log: log
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift
index eb2f15422dd3b..96a01317c2494 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/Item+Init.swift
@@ -20,6 +20,7 @@ public extension Item {
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift
index 67aadd9becc0a..a850477b1b712 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/Interface/MockEnumerator.swift
@@ -20,9 +20,7 @@ public class MockEnumerator: NSObject, NSFileProviderEnumerator {
self.remoteInterface = remoteInterface
}
- public func enumerateItems(
- for observer: any NSFileProviderEnumerationObserver, startingAt _: NSFileProviderPage
- ) {
+ public func enumerateItems(for observer: any NSFileProviderEnumerationObserver, startingAt _: NSFileProviderPage) {
let remoteSupportsTrash = remoteInterface.directMockCapabilities()?.files?.undelete ?? false
var items: [Item] = []
for item in enumeratorItems {
@@ -36,6 +34,7 @@ public class MockEnumerator: NSObject, NSFileProviderEnumerator {
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
+ displayFileActions: false,
remoteSupportsTrash: remoteSupportsTrash,
log: FileProviderLogMock()
)
diff --git a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift
index 12b30c9ef885a..5a75a8c82bb3e 100644
--- a/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift
+++ b/shell_integration/MacOSX/NextcloudFileProviderKit/Tests/NextcloudFileProviderKitTests/ItemPropertyTests.swift
@@ -365,6 +365,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: remoteInterface,
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: remoteSupportsTrash,
log: FileProviderLogMock()
)
@@ -426,6 +427,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -452,6 +454,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -475,6 +478,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -495,6 +499,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -514,6 +519,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -534,6 +540,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -549,6 +556,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: false,
log: FileProviderLogMock()
)
@@ -568,6 +576,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -586,6 +595,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -607,6 +617,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -625,6 +636,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -643,6 +655,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -666,6 +679,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -684,6 +698,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -705,6 +720,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -723,6 +739,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -741,6 +758,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -761,6 +779,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -793,6 +812,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
@@ -822,6 +842,7 @@ final class ItemPropertyTests: NextcloudFileProviderKitTestCase {
account: Self.account,
remoteInterface: MockRemoteInterface(account: Self.account),
dbManager: Self.dbManager,
+ displayFileActions: false,
remoteSupportsTrash: true,
log: FileProviderLogMock()
)
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift
index e5838d51e29c6..78e7a417564a3 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift
+++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift
@@ -12,6 +12,40 @@ extension FileProviderExtension: NSFileProviderCustomAction {
completionHandler: @escaping ((any Error)?) -> Void
) -> Progress {
switch actionIdentifier.rawValue {
+ case "com.nextcloud.desktopclient.FileProviderExt.FileActionsAction":
+ guard let itemIdentifier = itemIdentifiers.first else {
+ logger.error("Failed to get first item identifier for file actions action.")
+ completionHandler(NSFileProviderError(.noSuchItem))
+ return Progress()
+ }
+
+ guard let dbManager else {
+ logger.error("Cannot fetch metadata for item file actions due to database manager not being available.", [.item: itemIdentifier])
+ completionHandler(NSFileProviderError(.cannotSynchronize))
+ return Progress()
+ }
+
+ Task {
+ guard let userVisibleURL = try await manager?.getUserVisibleURL(for: itemIdentifier) else {
+ logger.error("Failed to get user-visible URL for item.", [.item: itemIdentifier])
+ completionHandler(NSFileProviderError(.noSuchItem))
+ return
+ }
+
+ guard let metadata = dbManager.itemMetadata(itemIdentifier) else {
+ logger.error("Failed to get metadata for item.", [.item: itemIdentifier])
+ completionHandler(NSFileProviderError(.cannotSynchronize))
+ return
+ }
+
+ let path = userVisibleURL.path
+ let domainIdentifier = domain.identifier.rawValue
+ logger.info("Telling main app to present file actions.", [.item: path, .domain: domainIdentifier])
+ app?.presentFileActions(metadata.ocId, path: path, remoteItemPath: metadata.path, withDomainIdentifier: domainIdentifier)
+ completionHandler(nil)
+ }
+
+ return Progress()
case "com.nextcloud.desktopclient.FileProviderExt.KeepDownloadedAction":
return performKeepDownloadedAction(
keepDownloaded: true,
@@ -30,7 +64,7 @@ extension FileProviderExtension: NSFileProviderCustomAction {
return Progress()
}
}
-
+
private func performKeepDownloadedAction(
keepDownloaded: Bool,
onItemsWithIdentifiers itemIdentifiers: [NSFileProviderItemIdentifier],
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift
index faf49bdec593c..35bce1526acda 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift
+++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift
@@ -11,12 +11,20 @@ import OSLog
/// The file provider replicated extension implementation.
///
@objc final class FileProviderExtension: NSObject, NSFileProviderReplicatedExtension, @unchecked Sendable {
+ ///
+ /// The file provider domain managed by this file provider extension implementation.
+ ///
let domain: NSFileProviderDomain
let keychain: Keychain
let log: any FileProviderLogging
let logger: FileProviderLogger
+ ///
+ /// The file provider manager for the domain managed by this extension implementation.
+ ///
+ let manager: NSFileProviderManager?
+
// MARK: XPC
///
@@ -81,6 +89,7 @@ import OSLog
// application extension process, call `FileProviderExtension.init(domain:)` to instantiate
// the extension for that domain, and call methods on the instance.
self.domain = domain
+ self.manager = NSFileProviderManager(for: domain)
// Set up logging.
self.log = FileProviderLog(fileProviderDomainIdentifier: domain.identifier)
@@ -514,13 +523,13 @@ import OSLog
return
}
- guard let fpManager = NSFileProviderManager(for: domain) else {
+ guard let manager = manager else {
logger.error("Could not get file provider manager for domain: \(self.domain.displayName)")
completionHandler()
return
}
- let materialisedEnumerator = fpManager.enumeratorForMaterializedItems()
+ let materialisedEnumerator = manager.enumeratorForMaterializedItems()
let materialisedObserver = MaterializedEnumerationObserver(account: ncAccount, dbManager: dbManager, log: log) { _, _ in
completionHandler()
}
@@ -532,12 +541,12 @@ import OSLog
// MARK: - Helper functions
func signalEnumerator(completionHandler: @escaping (_ error: Error?) -> Void) {
- guard let fpManager = NSFileProviderManager(for: domain) else {
+ guard let manager = manager else {
logger.error("Could not get file provider manager for domain, could not signal enumerator. This might lead to future conflicts.")
return
}
- fpManager.signalEnumerator(for: .workingSet, completionHandler: completionHandler)
+ manager.signalEnumerator(for: .workingSet, completionHandler: completionHandler)
}
@objc func sendFileProviderDomainIdentifier() {
@@ -548,14 +557,14 @@ import OSLog
}
private func signalEnumeratorAfterAccountSetup() {
- guard let fpManager = NSFileProviderManager(for: domain) else {
+ guard let manager = manager else {
logger.error("Could not get file provider manager for domain \(self.domain.displayName), cannot notify after account setup")
return
}
assert(ncAccount != nil)
- fpManager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
+ manager.signalErrorResolved(NSFileProviderError(.notAuthenticated)) { error in
if error != nil {
self.logger.error("Error resolving not authenticated, received error: \(error!.localizedDescription)")
}
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist
index 853d3bb257e44..616bcdd392ce1 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist
+++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist
@@ -41,6 +41,14 @@
NSExtensionFileProviderActionName
Allow automatic freeing up space
+
+ NSExtensionFileProviderActionActivationRule
+ SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayFileActions == true ).@count > 0
+ NSExtensionFileProviderActionIdentifier
+ com.nextcloud.desktopclient.FileProviderExt.FileActionsAction
+ NSExtensionFileProviderActionName
+ File actions
+
NSExtensionFileProviderDocumentGroup
$(DEVELOPMENT_TEAM).$(OC_APPLICATION_REV_DOMAIN)
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h
index 08569ab826f6d..9e8f599b12be8 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h
+++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/AppProtocol.h
@@ -7,12 +7,22 @@
#define AppProtocol_h
#import
+NS_ASSUME_NONNULL_BEGIN
/**
* @brief The main app APIs exposed through XPC.
*/
@protocol AppProtocol
+/**
+ * @brief The file provider extension can tell the main app to offer the user server-features for the given item.
+ * @param fileId The ocId as provided by the server for item identification independent from path.
+ * @param path The local and absolute path for the item to offer actions for.
+ * @param remoteItemPath The server-side path of the item, used as a fallback when no sync folder is configured.
+ * @param domainIdentifier The file provider domain identifier for the account that manages this file.
+ */
+- (void)presentFileActions:(NSString *)fileId path:(NSString *)path remoteItemPath:(NSString *)remoteItemPath withDomainIdentifier:(NSString *)domainIdentifier;
+
/**
* @brief The file provider extension can report its synchronization status as a string constant value to the main app through this method.
* @param status The synchronization status string.
@@ -22,4 +32,6 @@
@end
+NS_ASSUME_NONNULL_END
#endif /* AppProtocol_h */
+
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m
index 7c389f8a47263..f52d279ec4656 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m
+++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m
@@ -5,6 +5,7 @@
*/
#import "FinderSync.h"
+#import
@interface FinderSync()
{
@@ -13,9 +14,21 @@ @interface FinderSync()
NSMutableDictionary *_strings;
NSMutableArray *_menuItems;
NSCondition *_menuIsComplete;
+ os_log_t _log;
}
@end
+static os_log_t getFinderSyncLogger(void) {
+ static dispatch_once_t onceToken;
+ static os_log_t logger = NULL;
+ dispatch_once(&onceToken, ^{
+ NSBundle *bundle = [NSBundle bundleForClass:[FinderSync class]];
+ NSString *subsystem = bundle.bundleIdentifier ?: @"FinderSyncExt";
+ logger = os_log_create(subsystem.UTF8String, "FinderSync");
+ });
+ return logger;
+}
+
@implementation FinderSync
- (instancetype)init
@@ -23,10 +36,12 @@ - (instancetype)init
self = [super init];
if (self) {
+ _log = getFinderSyncLogger();
+ os_log_debug(_log, "Initializing FinderSync extension");
FIFinderSyncController *syncController = [FIFinderSyncController defaultController];
NSBundle *extBundle = [NSBundle bundleForClass:[self class]];
// This was added to the bundle's Info.plist to get it from the build system
- NSString *socketApiPrefix = [extBundle objectForInfoDictionaryKey:@"SocketApiPrefix"];
+ NSString *groupIdentifier = [extBundle objectForInfoDictionaryKey:@"NCApplicationGroupIdentifier"];
NSImage *ok = [extBundle imageForResource:@"ok.icns"];
NSImage *ok_swm = [extBundle imageForResource:@"ok_swm.icns"];
@@ -45,27 +60,29 @@ - (instancetype)init
[syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"];
[syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"];
- NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:socketApiPrefix];
+ NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupIdentifier];
NSURL *library = [container URLByAppendingPathComponent:@"Library" isDirectory:true];
NSURL *applicationSupport = [library URLByAppendingPathComponent:@"Application Support" isDirectory:true];
NSURL *socketPath = [applicationSupport URLByAppendingPathComponent:@"s" isDirectory:NO];
- NSLog(@"Socket path: %@", socketPath.path);
+ os_log_debug(_log, "Socket path: %{public}@", socketPath.path);
if (socketPath.path) {
+ os_log_debug(_log, "Socket path determined: %{public}@", socketPath.path);
self.lineProcessor = [[FinderSyncSocketLineProcessor alloc] initWithDelegate:self];
self.localSocketClient = [[LocalSocketClient alloc] initWithSocketPath:socketPath.path
lineProcessor:self.lineProcessor];
[self.localSocketClient start];
[self.localSocketClient askOnSocket:@"" query:@"GET_STRINGS"];
} else {
- NSLog(@"No socket path. Not initiating local socket client.");
+ os_log_error(_log, "No socket path available. Not initiating local socket client.");
self.localSocketClient = nil;
}
_registeredDirectories = NSMutableSet.set;
_strings = NSMutableDictionary.dictionary;
_menuIsComplete = [[NSCondition alloc] init];
+ os_log_debug(_log, "FinderSync extension initialization completed");
}
return self;
@@ -75,20 +92,23 @@ - (instancetype)init
- (void)requestBadgeIdentifierForURL:(NSURL *)url
{
+ os_log_debug(_log, "Requesting badge identifier for URL: %{public}@", url.path);
BOOL isDir;
if ([[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory: &isDir] == NO) {
- NSLog(@"ERROR: Could not determine file type of %@", [url path]);
+ os_log_error(_log, "Could not determine file type of %{public}@", [url path]);
isDir = NO;
}
NSString* normalizedPath = [[url path] decomposedStringWithCanonicalMapping];
[self.localSocketClient askForIcon:normalizedPath isDirectory:isDir];
+ os_log_debug(_log, "Badge identifier request completed for: %{public}@", normalizedPath);
}
#pragma mark - Menu and toolbar item support
- (NSString*) selectedPathsSeparatedByRecordSeparator
{
+ os_log_debug(_log, "Building selected paths string with record separators");
FIFinderSyncController *syncController = [FIFinderSyncController defaultController];
NSMutableString *string = [[NSMutableString alloc] init];
[syncController.selectedItemURLs enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
@@ -98,19 +118,24 @@ - (NSString*) selectedPathsSeparatedByRecordSeparator
NSString* normalizedPath = [[obj path] decomposedStringWithCanonicalMapping];
[string appendString:normalizedPath];
}];
+ os_log_debug(_log, "Selected paths string built: %lu paths", (unsigned long)syncController.selectedItemURLs.count);
return string;
}
- (void)waitForMenuToArrive
{
+ os_log_debug(_log, "Waiting for menu to arrive");
[self->_menuIsComplete lock];
[self->_menuIsComplete wait];
[self->_menuIsComplete unlock];
+ os_log_debug(_log, "Menu arrival wait completed");
}
- (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu
{
+ os_log_debug(_log, "Building menu for menu kind: %lu", (unsigned long)whichMenu);
if(![self.localSocketClient isConnected]) {
+ os_log_error(_log, "Local socket client not connected, cannot build menu");
return nil;
}
@@ -131,6 +156,7 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu
*stop = YES;
}
}];
+ os_log_debug(_log, "Root directories check: onlyRootsSelected = %d", onlyRootsSelected);
NSString *paths = [self selectedPathsSeparatedByRecordSeparator];
[self.localSocketClient askOnSocket:paths query:@"GET_MENU_ITEMS"];
@@ -141,6 +167,7 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu
id contextMenuTitle = [_strings objectForKey:@"CONTEXT_MENU_TITLE"];
if (contextMenuTitle && !onlyRootsSelected) {
+ os_log_debug(_log, "Creating context menu with title: %{public}@", contextMenuTitle);
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
NSMenu *subMenu = [[NSMenu alloc] initWithTitle:@""];
NSMenuItem *subMenuItem = [menu addItemWithTitle:contextMenuTitle action:nil keyEquivalent:@""];
@@ -162,70 +189,88 @@ - (NSMenu *)menuForMenuKind:(FIMenuKind)whichMenu
}
idx++;
}
+ os_log_debug(_log, "Context menu created with %d items", idx);
return menu;
}
+ os_log_debug(_log, "No context menu created: contextMenuTitle=%@, onlyRootsSelected=%d", contextMenuTitle != nil ? @"present" : @"absent", onlyRootsSelected);
return nil;
}
- (void)subMenuActionClicked:(id)sender {
long idx = [(NSMenuItem*)sender tag];
+ os_log_debug(_log, "Menu item clicked at index: %ld", idx);
NSString *command = [[_menuItems objectAtIndex:idx] valueForKey:@"command"];
NSString *paths = [self selectedPathsSeparatedByRecordSeparator];
+ os_log_debug(_log, "Executing command: %{public}@", command);
[self.localSocketClient askOnSocket:paths query:command];
+ os_log_debug(_log, "Command execution completed");
}
#pragma mark - SyncClientProxyDelegate implementation
- (void)setResult:(NSString *)result forPath:(NSString*)path
{
+ os_log_debug(_log, "Setting result: %{public}@ for path: %{public}@", result, path);
NSString *const normalizedPath = path.decomposedStringWithCanonicalMapping;
NSURL *const urlForPath = [NSURL fileURLWithPath:normalizedPath];
if (urlForPath == nil) {
+ os_log_error(_log, "Failed to create URL for path: %{public}@", normalizedPath);
return;
}
[FIFinderSyncController.defaultController setBadgeIdentifier:result forURL:urlForPath];
+ os_log_debug(_log, "Badge identifier set successfully");
}
- (void)reFetchFileNameCacheForPath:(NSString*)path
{
-
+ os_log_debug(_log, "Refetching file name cache for path: %{public}@", path);
}
- (void)registerPath:(NSString*)path
{
+ os_log_debug(_log, "Registering path: %{public}@", path);
NSAssert(_registeredDirectories, @"Registered directories should be a valid set!");
[_registeredDirectories addObject:[NSURL fileURLWithPath:path]];
[FIFinderSyncController defaultController].directoryURLs = _registeredDirectories;
+ os_log_debug(_log, "Path registration completed");
}
- (void)unregisterPath:(NSString*)path
{
+ os_log_debug(_log, "Unregistering path: %{public}@", path);
[_registeredDirectories removeObject:[NSURL fileURLWithPath:path]];
[FIFinderSyncController defaultController].directoryURLs = _registeredDirectories;
+ os_log_debug(_log, "Path unregistration completed");
}
- (void)setString:(NSString*)key value:(NSString*)value
{
+ os_log_debug(_log, "Setting string: %{public}@ = %{public}@", key, value);
[_strings setObject:value forKey:key];
}
- (void)resetMenuItems
{
+ os_log_debug(_log, "Resetting menu items");
_menuItems = [[NSMutableArray alloc] init];
+ os_log_debug(_log, "Menu items reset completed");
}
- (void)addMenuItem:(NSDictionary *)item {
- NSLog(@"Adding menu item.");
+ os_log_debug(_log, "Adding menu item with title: %{public}@", [item valueForKey:@"text"] ?: @"(no title)");
[_menuItems addObject:item];
+ os_log_debug(_log, "Menu item added, total items: %lu", (unsigned long)_menuItems.count);
}
- (void)menuHasCompleted
{
- NSLog(@"Emitting menu is complete signal now.");
+ os_log_debug(_log, "Menu completion signal received");
[self->_menuIsComplete signal];
+ os_log_debug(_log, "Menu signal emitted");
}
- (void)connectionDidDie
{
+ os_log_error(_log, "Connection to sync client died");
[_strings removeAllObjects];
[_registeredDirectories removeAllObjects];
// For some reason the FIFinderSync cache doesn't seem to be cleared for the root item when
@@ -235,7 +280,9 @@ - (void)connectionDidDie
// This will tell Finder that this extension isn't attached to any directory
// until we can reconnect to the sync client.
[FIFinderSyncController defaultController].directoryURLs = nil;
+ os_log_error(_log, "Connection cleanup completed, waiting for reconnection");
}
@end
+
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m
index 818666db51315..22f2eecfa2be7 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m
+++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSyncSocketLineProcessor.m
@@ -4,90 +4,119 @@
*/
#import
+#import
#import "FinderSyncSocketLineProcessor.h"
+static os_log_t getFinderSyncSocketLineProcessorLogger(void) {
+ static dispatch_once_t onceToken;
+ static os_log_t logger = NULL;
+ dispatch_once(&onceToken, ^{
+ NSBundle *bundle = [NSBundle bundleForClass:[FinderSyncSocketLineProcessor class]];
+ NSString *subsystem = bundle.bundleIdentifier ?: @"FinderSyncExt";
+ logger = os_log_create(subsystem.UTF8String, "FinderSyncSocketLineProcessor");
+ });
+ return logger;
+}
+
+@interface FinderSyncSocketLineProcessor()
+{
+ os_log_t _log;
+}
+@end
+
@implementation FinderSyncSocketLineProcessor
-(instancetype)initWithDelegate:(id)delegate
{
- NSLog(@"Init line processor with delegate.");
+ os_log_t logger = getFinderSyncSocketLineProcessorLogger();
+ os_log_debug(logger, "Initializing FinderSyncSocketLineProcessor with delegate");
self = [super init];
if (self) {
+ _log = logger;
self.delegate = delegate;
+ os_log_debug(logger, "FinderSyncSocketLineProcessor initialization completed");
}
return self;
}
-(void)process:(NSString*)line
{
- NSLog(@"Processing line: '%@'", line);
+ os_log_debug(_log, "Processing line: %{public}@", line);
NSArray *split = [line componentsSeparatedByString:@":"];
NSString *command = [split objectAtIndex:0];
- NSLog(@"Command: %@", command);
+ os_log_debug(_log, "Command: %{public}@", command);
if([command isEqualToString:@"STATUS"]) {
NSString *result = [split objectAtIndex:1];
NSArray *pathSplit = [split subarrayWithRange:NSMakeRange(2, [split count] - 2)]; // Get everything after location 2
NSString *path = [pathSplit componentsJoinedByString:@":"];
+ os_log_debug(_log, "STATUS command: result=%{public}@, path=%{public}@", result, path);
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Setting result %@ for path %@", result, path);
+ os_log_debug(_log, "Setting result %{public}@ for path %{public}@", result, path);
[self.delegate setResult:result forPath:path];
});
} else if([command isEqualToString:@"UPDATE_VIEW"]) {
NSString *path = [split objectAtIndex:1];
+ os_log_debug(_log, "UPDATE_VIEW command: path=%{public}@", path);
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Re-fetching filename cache for path %@", path);
+ os_log_debug(_log, "Re-fetching filename cache for path %{public}@", path);
[self.delegate reFetchFileNameCacheForPath:path];
});
} else if([command isEqualToString:@"REGISTER_PATH"]) {
NSString *path = [split objectAtIndex:1];
+ os_log_debug(_log, "REGISTER_PATH command: path=%{public}@", path);
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Registering path %@", path);
+ os_log_debug(_log, "Registering path %{public}@", path);
[self.delegate registerPath:path];
});
} else if([command isEqualToString:@"UNREGISTER_PATH"]) {
NSString *path = [split objectAtIndex:1];
+ os_log_debug(_log, "UNREGISTER_PATH command: path=%{public}@", path);
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Unregistering path %@", path);
+ os_log_debug(_log, "Unregistering path %{public}@", path);
[self.delegate unregisterPath:path];
});
} else if([command isEqualToString:@"GET_STRINGS"]) {
+ os_log_debug(_log, "GET_STRINGS command: %{public}@", [split objectAtIndex:1] ?: @"(no subcommand)");
// BEGIN and END messages, do nothing.
return;
} else if([command isEqualToString:@"STRING"]) {
NSString *key = [split objectAtIndex:1];
NSString *value = [split objectAtIndex:2];
+ os_log_debug(_log, "STRING command: key=%{public}@, value=%{public}@", key, value);
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Setting string %@ to value %@", key, value);
+ os_log_debug(_log, "Setting string %{public}@ to value %{public}@", key, value);
[self.delegate setString:key value:value];
});
} else if([command isEqualToString:@"GET_MENU_ITEMS"]) {
+ os_log_debug(_log, "GET_MENU_ITEMS command: subcommand=%{public}@", [split objectAtIndex:1] ?: @"(no subcommand)");
if([[split objectAtIndex:1] isEqualToString:@"BEGIN"]) {
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Resetting menu items.");
+ os_log_debug(_log, "Resetting menu items");
[self.delegate resetMenuItems];
});
} else {
- NSLog(@"Emitting menu has completed signal.");
+ os_log_debug(_log, "Emitting menu has completed signal");
[self.delegate menuHasCompleted];
}
} else if([command isEqualToString:@"MENU_ITEM"]) {
NSDictionary *item = @{@"command": [split objectAtIndex:1], @"flags": [split objectAtIndex:2], @"text": [split objectAtIndex:3]};
+ os_log_debug(_log, "MENU_ITEM command: command=%{public}@, flags=%{public}@, text=%{public}@", [split objectAtIndex:1], [split objectAtIndex:2], [split objectAtIndex:3]);
dispatch_async(dispatch_get_main_queue(), ^{
- NSLog(@"Adding menu item with command %@, flags %@, and text %@", [split objectAtIndex:1], [split objectAtIndex:2], [split objectAtIndex:3]);
+ os_log_debug(_log, "Adding menu item with command %{public}@, flags %{public}@, and text %{public}@", [split objectAtIndex:1], [split objectAtIndex:2], [split objectAtIndex:3]);
[self.delegate addMenuItem:item];
});
} else {
- // LOG UNKNOWN COMMAND
- NSLog(@"Unknown command: %@", command);
+ os_log_error(_log, "Unknown command: %{public}@", command);
}
+ os_log_debug(_log, "Line processing completed");
}
@end
diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist
index 927085601d11d..75f88b0ba1536 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist
+++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/Info.plist
@@ -2,6 +2,8 @@
+ NCApplicationGroupIdentifier
+ $(DEVELOPMENT_TEAM).$(OC_APPLICATION_REV_DOMAIN)
NSAppTransportSecurity
NSAllowsArbitraryLoads
diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 42f4f740055f7..27644534f27e3 100644
--- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/nextcloud/NextcloudCapabilitiesKit.git",
"state" : {
- "revision" : "296f28db1bb02c51f215e7ac34430885f5046058",
- "version" : "2.4.6"
+ "revision" : "e7cc7d2214ff565e08cffd8495465634d2c6998c",
+ "version" : "2.5.0"
}
},
{
diff --git a/src/gui/integration/FileActionsWindow.qml b/src/gui/integration/FileActionsWindow.qml
index e526fbff6dc25..19e01f242f3e4 100644
--- a/src/gui/integration/FileActionsWindow.qml
+++ b/src/gui/integration/FileActionsWindow.qml
@@ -25,6 +25,8 @@ ApplicationWindow {
property var accountState: ({})
property string localPath: ""
property string shortLocalPath: ""
+ property string fileId: ""
+ property string remoteItemPath: ""
readonly property int windowRadius: Systray.useNormalWindow ? 0.0 : Style.trayWindowRadius
@@ -34,6 +36,8 @@ ApplicationWindow {
id: fileActionModel
accountState: root.accountState
localPath: root.localPath
+ fileId: root.fileId
+ remoteItemPath: root.remoteItemPath
}
background: Rectangle {
diff --git a/src/gui/integration/fileactionsmodel.cpp b/src/gui/integration/fileactionsmodel.cpp
index 1d8d913ca0c2e..395934b73e486 100644
--- a/src/gui/integration/fileactionsmodel.cpp
+++ b/src/gui/integration/fileactionsmodel.cpp
@@ -116,24 +116,71 @@ QByteArray FileActionsModel::fileId() const
return _fileId;
}
-void FileActionsModel::setupFileProperties()
+void FileActionsModel::setFileId(const QByteArray &fileId)
{
- const auto folderForPath = FolderMan::instance()->folderForPath(_localPath);
- _filePath = _localPath.mid(folderForPath->cleanPath().length() + 1);
- SyncJournalFileRecord fileRecord;
- if (!folderForPath->journalDb()->getFileRecord(_filePath, &fileRecord)) {
- qCWarning(lcFileActions) << "Invalid file record for path:" << _localPath;
+ if (fileId == _fileId) {
return;
}
- _fileId = fileRecord._fileId;
+ _fileId = fileId;
+
+ if (_accountState && !_localPath.isEmpty()) {
+ parseEndpoints();
+ }
+
+ Q_EMIT fileChanged();
+}
+
+QString FileActionsModel::remoteItemPath() const
+{
+ return _remoteItemPath;
+}
+
+void FileActionsModel::setRemoteItemPath(const QString &remoteItemPath)
+{
+ if (remoteItemPath == _remoteItemPath) {
+ return;
+ }
+ _remoteItemPath = remoteItemPath;
+ Q_EMIT fileChanged();
+}
+
+void FileActionsModel::setupFileProperties()
+{
+ qCDebug(lcFileActions) << "Setting up file properties for:" << _localPath;
+
+ const auto folderForPath = FolderMan::instance()->folderForPath(_localPath);
+
+ // Declare once so it's available after the if-else clause.
+ QMimeDatabase::MatchMode mimeMatchMode;
+
+ if (folderForPath) { // Synchronization folders
+ qCDebug(lcFileActions) << "Found synchronization folder for" << _localPath;
+ _filePath = _localPath.mid(folderForPath->cleanPath().length() + 1);
+
+ SyncJournalFileRecord fileRecord;
+
+ if (!folderForPath->journalDb()->getFileRecord(_filePath, &fileRecord)) {
+ qCWarning(lcFileActions) << "Invalid file record for path:" << _localPath;
+ return;
+ }
+
+ _fileId = fileRecord._fileId;
+
+ // Decide match mode based on whether this is a virtual file
+ mimeMatchMode = fileRecord.isVirtualFile() ? QMimeDatabase::MatchExtension
+ : QMimeDatabase::MatchDefault;
+ } else { // Virtual file systems
+ qCDebug(lcFileActions) << "Did not find synchronization folder for" << _localPath;
+ // In this case, _fileId should already be initialized with a value from the calling code.
+ _filePath = _localPath;
+ // When we don't have a sync folder, use extension matching
+ mimeMatchMode = QMimeDatabase::MatchExtension;
+ }
- const auto mimeMatchMode = fileRecord.isVirtualFile() ? QMimeDatabase::MatchExtension
- : QMimeDatabase::MatchDefault;
QMimeDatabase mimeDb;
const auto mimeType = mimeDb.mimeTypeForFile(_localPath, mimeMatchMode);
_mimeType = mimeType;
-
_fileIcon = _accountUrl + Activity::relativeServerFileTypeIconPath(_mimeType);
}
@@ -175,11 +222,25 @@ void FileActionsModel::setResponse(const Response &response)
void FileActionsModel::parseEndpoints()
{
+ auto resetActions = [this](const ActionList &actions) {
+ beginResetModel();
+ _fileActions = actions;
+ endResetModel();
+ Q_EMIT fileActionModelChanged();
+ };
+
+ if (!_accountState) {
+ qCWarning(lcFileActions) << "No account state available for" << _localPath;
+ resetActions({});
+ return;
+ }
+
if (!_accountState->isConnected()) {
qCWarning(lcFileActions) << "The account is not connected"
<< _accountUrl;
setResponse({ tr("Your account is offline %1.", "account url").arg(_accountUrl),
_accountUrl });
+ resetActions({});
return;
}
@@ -196,6 +257,7 @@ void FileActionsModel::parseEndpoints()
<< _localPath;
setResponse({ tr("The file type for %1 is not valid.", "file name").arg(_localPath),
_accountUrl });
+ resetActions({});
return;
}
@@ -206,9 +268,11 @@ void FileActionsModel::parseEndpoints()
setResponse({ tr("No file actions were returned by the server for %1 files.", "file mymetype")
.arg(_mimeType.filterString()),
_accountUrl });
+ resetActions({});
return;
}
+ ActionList actions;
for (const auto &contextMenu : contextMenuList) {
QueryList queryList;
const auto paramsMap = contextMenu.value("params").toMap();
@@ -228,17 +292,18 @@ void FileActionsModel::parseEndpoints()
}
}
- _fileActions.append({ parseIcon(contextMenu.value("icon").toString()),
- contextMenu.value("name").toString(),
- contextMenu.value("url").toString(),
- contextMenu.value("method").toString(),
- queryList });
+ actions.append({ parseIcon(contextMenu.value("icon").toString()),
+ contextMenu.value("name").toString(),
+ contextMenu.value("url").toString(),
+ contextMenu.value("method").toString(),
+ queryList });
}
+ resetActions(actions);
+
qCDebug(lcFileActions) << "File" << _localPath << "has"
- << _fileActions.size()
+ << actions.size()
<< "actions available.";
- Q_EMIT fileActionModelChanged();
}
QString FileActionsModel::parseUrl(const QString &url) const
@@ -297,14 +362,25 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode)
const auto root = json.object().value(QStringLiteral("root")).toObject();
const auto folderForPath = FolderMan::instance()->folderForPath(_localPath);
- const auto remoteFolderPath = _accountUrl + folderForPath->remotePath();
const auto successMessage = tr("%1 done.", "file action success message").arg(fileAction);
+
+ QString remoteFolderPath;
+ if (folderForPath) {
+ remoteFolderPath = _accountUrl + folderForPath->remotePath();
+ } else if (!_remoteItemPath.isEmpty()) {
+ remoteFolderPath = _accountUrl + _remoteItemPath;
+ } else {
+ qCWarning(lcFileActions) << "Failed to find folder for path and no remote item path available:" << _localPath;
+ return;
+ }
+
if (root.empty()) {
setResponse({ successMessage, remoteFolderPath });
return;
}
const auto rows = root.value(QStringLiteral("rows")).toArray();
+
if (rows.empty()) {
setResponse({ successMessage, remoteFolderPath });
return;
@@ -313,6 +389,7 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode)
for (const auto &rowValue : rows) {
const auto row = rowValue.toObject();
const auto children = row.value("children").toArray();
+
for (const auto &childValue : children) {
const auto child = childValue.toObject();
setResponse({ child.value(QStringLiteral("element")).toString(),
@@ -322,3 +399,4 @@ void FileActionsModel::processRequest(const QJsonDocument &json, int statusCode)
}
} // namespace OCC
+
diff --git a/src/gui/integration/fileactionsmodel.h b/src/gui/integration/fileactionsmodel.h
index 222478a62479d..b0354bdd74cd3 100644
--- a/src/gui/integration/fileactionsmodel.h
+++ b/src/gui/integration/fileactionsmodel.h
@@ -19,6 +19,8 @@ class FileActionsModel : public QAbstractListModel {
Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged)
Q_PROPERTY(QString localPath READ localPath WRITE setLocalPath NOTIFY fileChanged)
+ Q_PROPERTY(QByteArray fileId READ fileId WRITE setFileId NOTIFY fileChanged)
+ Q_PROPERTY(QString remoteItemPath READ remoteItemPath WRITE setRemoteItemPath NOTIFY fileChanged)
Q_PROPERTY(QString fileIcon READ fileIcon NOTIFY fileChanged)
Q_PROPERTY(QString responseLabel READ responseLabel WRITE setResponseLabel NOTIFY responseChanged)
Q_PROPERTY(QString responseUrl READ responseUrl WRITE setResponseUrl NOTIFY responseChanged)
@@ -67,6 +69,11 @@ class FileActionsModel : public QAbstractListModel {
void setLocalPath(const QString &localPath);
[[nodiscard]] QByteArray fileId() const;
+ void setFileId(const QByteArray &fileId);
+
+ [[nodiscard]] QString remoteItemPath() const;
+ void setRemoteItemPath(const QString &remoteItemPath);
+
[[nodiscard]] QMimeType mimeType() const;
[[nodiscard]] QString fileIcon() const;
void setupFileProperties();
@@ -99,6 +106,7 @@ public slots:
AccountState *_accountState;
QString _localPath;
QByteArray _fileId;
+ QString _remoteItemPath;
QMimeType _mimeType;
QString _filePath;
QString _accountUrl;
diff --git a/src/gui/macOS/fileproviderservice.h b/src/gui/macOS/fileproviderservice.h
index 98fe8ba9334ff..dab535181b893 100644
--- a/src/gui/macOS/fileproviderservice.h
+++ b/src/gui/macOS/fileproviderservice.h
@@ -59,6 +59,15 @@ class FileProviderService : public QObject
*/
void syncStateChanged(const AccountPtr &account, SyncResult::Status state);
+ /**
+ * @brief Emitted when a file provider extension requests to show the file actions dialog.
+ * @param fileId The ocId as provided by the server for item identification independent from path.
+ * @param localFile The local file path for which to show actions.
+ * @param remoteItemPath The server-side path of the item, used as a fallback when no sync folder is configured.
+ * @param fileProviderDomainIdentifier The file provider domain identifier (optional, empty if not provided).
+ */
+ void showFileActionsDialog(const QString &fileId, const QString &localFile, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier);
+
private:
class MacImplementation;
std::unique_ptr d;
diff --git a/src/gui/macOS/fileproviderservice.mm b/src/gui/macOS/fileproviderservice.mm
index 5840f44619b09..0b2c97e9171cd 100644
--- a/src/gui/macOS/fileproviderservice.mm
+++ b/src/gui/macOS/fileproviderservice.mm
@@ -30,6 +30,31 @@ @interface FileProviderServiceDelegate : NSObject
@implementation FileProviderServiceDelegate
+- (void)presentFileActions:(NSString *)fileId path:(NSString *)path remoteItemPath:(NSString *)remoteItemPath withDomainIdentifier:(NSString *)domainIdentifier
+{
+ qCDebug(OCC::lcMacFileProviderService) << "Should present file actions for item with fileId:"
+ << fileId
+ << "and path:"
+ << path
+ << "remote item path:"
+ << remoteItemPath
+ << "domain identifier:"
+ << domainIdentifier;
+
+ const auto qFileId = QString::fromNSString(fileId);
+ const auto localPath = QString::fromNSString(path);
+ const auto qRemoteItemPath = QString::fromNSString(remoteItemPath);
+ const auto domainId = QString::fromNSString(domainIdentifier);
+
+ // Use QMetaObject::invokeMethod to emit the signal on the correct thread
+ // since this callback may be called on an XPC dispatch queue (non-main thread)
+ QMetaObject::invokeMethod(_service, "showFileActionsDialog", Qt::QueuedConnection,
+ Q_ARG(QString, qFileId),
+ Q_ARG(QString, localPath),
+ Q_ARG(QString, qRemoteItemPath),
+ Q_ARG(QString, domainId));
+}
+
- (void)reportSyncStatus:(NSString *)status forDomainIdentifier:(NSString *)domainIdentifier
{
const auto statusString = QString::fromNSString(status);
diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp
index 1fe337b4433d4..2f1b3bd0214c8 100644
--- a/src/gui/owncloudgui.cpp
+++ b/src/gui/owncloudgui.cpp
@@ -117,6 +117,7 @@ ownCloudGui::ownCloudGui(Application *parent)
#ifdef BUILD_FILE_PROVIDER_MODULE
connect(Mac::FileProvider::instance()->service(), &Mac::FileProviderService::syncStateChanged, this, &ownCloudGui::slotComputeOverallSyncStatus);
+ connect(Mac::FileProvider::instance()->service(), &Mac::FileProviderService::showFileActionsDialog, _tray.data(), &Systray::slotShowFileProviderFileActionsDialog);
#endif
connect(Logger::instance(), &Logger::guiLog, this, &ownCloudGui::slotShowTrayMessage);
diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp
index 39ba5a42b66e4..89d07bde3f5fc 100644
--- a/src/gui/systray.cpp
+++ b/src/gui/systray.cpp
@@ -441,6 +441,32 @@ void Systray::showFileActionsDialog(const QString &localPath)
createFileActionsDialog(localPath);
}
+#ifdef BUILD_FILE_PROVIDER_MODULE
+
+void Systray::slotShowFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier)
+{
+ createFileProviderFileActionsDialog(fileId, localPath, remoteItemPath, fileProviderDomainIdentifier);
+}
+
+void Systray::createFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier)
+{
+ if (!_trayEngine) {
+ qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "as no tray engine was available";
+ return;
+ }
+
+ const auto accountState = AccountManager::instance()->accountFromFileProviderDomainIdentifier(fileProviderDomainIdentifier);
+ if (!accountState) {
+ qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath
+ << "no account found for domain identifier" << fileProviderDomainIdentifier;
+ return;
+ }
+
+ createFileActionsDialogWithAccountState(localPath, accountState.data(), fileId, remoteItemPath);
+}
+
+#endif
+
void Systray::createFileActionsDialog(const QString &localPath)
{
if (!_trayEngine) {
@@ -454,7 +480,23 @@ void Systray::createFileActionsDialog(const QString &localPath)
return;
}
+ createFileActionsDialogWithAccountState(localPath, folder->accountState());
+}
+
+void Systray::createFileActionsDialogWithAccountState(const QString &localPath, AccountState *accountState, const QString &fileId, const QString &remoteItemPath)
+{
+ if (!_trayEngine) {
+ qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "as no tray engine was available";
+ return;
+ }
+
+ if (!accountState) {
+ qCWarning(lcSystray) << "Could not open file actions dialog for" << localPath << "no account state provided";
+ return;
+ }
+
QQmlComponent fileActionsQml(trayEngine(), QStringLiteral("qrc:/qml/src/gui/integration/FileActionsWindow.qml"));
+
if (fileActionsQml.isError()) {
qCWarning(lcSystray) << fileActionsQml.errorString();
qCWarning(lcSystray) << fileActionsQml.errors();
@@ -463,14 +505,18 @@ void Systray::createFileActionsDialog(const QString &localPath)
QFileInfo localFile{localPath};
const auto shortLocalPath = localFile.fileName();
+
const QVariantMap initialProperties{
- {"accountState", QVariant::fromValue(folder->accountState())},
+ {"accountState", QVariant::fromValue(accountState)},
+ {"fileId", fileId},
+ {"remoteItemPath", remoteItemPath},
{"shortLocalPath", shortLocalPath},
{"localPath", localPath},
};
const auto fileActionsDialog = fileActionsQml.createWithInitialProperties(initialProperties);
const auto dialog = qobject_cast(fileActionsDialog);
+
if (!dialog) {
qCWarning(lcSystray) << "File Actions dialog window resulted in creation of object that was not a window!";
return;
diff --git a/src/gui/systray.h b/src/gui/systray.h
index 4cc3e4b0a2b65..b48d2dbfc3ff1 100644
--- a/src/gui/systray.h
+++ b/src/gui/systray.h
@@ -148,6 +148,12 @@ public slots:
void createFileActivityDialog(const QString &localPath);
void showFileActionsDialog(const QString &localPath);
+ #ifdef BUILD_FILE_PROVIDER_MODULE
+
+ void slotShowFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier);
+
+ #endif
+
void presentShareViewInTray(const QString &localPath);
void presentFileActionsViewInSystray(const QString &localPath);
@@ -169,6 +175,18 @@ private slots:
void createFileDetailsDialog(const QString &localPath);
void createFileActionsDialog(const QString &localPath);
+ #ifdef BUILD_FILE_PROVIDER_MODULE
+
+ void createFileProviderFileActionsDialog(const QString &fileId, const QString &localPath, const QString &remoteItemPath, const QString &fileProviderDomainIdentifier);
+
+ #endif
+
+ /**
+ * @param fileId This is optional and null by default. Only when there are no synchronization folders configured (for example when only a file provider is used) is this value required.
+ * @param remoteItemPath The server-side path of the item. Used as a fallback to determine the success response URL when no sync folder is configured.
+ */
+ void createFileActionsDialogWithAccountState(const QString &localPath, AccountState *accountState, const QString &fileId = {}, const QString &remoteItemPath = {});
+
[[nodiscard]] QScreen *currentScreen() const;
[[nodiscard]] QRect currentScreenRect() const;
[[nodiscard]] QRect currentAvailableScreenRect() const;