From d4154eca41621f3fb8f2d3906898df0b4ee61cc5 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 1 Apr 2026 16:41:39 +0200 Subject: [PATCH 1/4] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 10 + iOSClient/Share/NCShareHeader.swift | 33 ++- iOSClient/Share/NCSharePaging.swift | 15 ++ iOSClient/Share/NCShareTagEditorModel.swift | 194 ++++++++++++++++++ iOSClient/Share/NCShareTagEditorView.swift | 94 +++++++++ .../en.lproj/Localizable.strings | 5 + 6 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 iOSClient/Share/NCShareTagEditorModel.swift create mode 100644 iOSClient/Share/NCShareTagEditorView.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 1ba23daec8..8d14a4b55a 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -36,6 +36,8 @@ AA8D316F2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D316A2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift */; }; AA8D31702D4123B200FE2775 /* DownloadLimitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D31692D4123B200FE2775 /* DownloadLimitViewModel.swift */; }; AA8D31712D4123B200FE2775 /* NCShareDownloadLimitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D316C2D4123B200FE2775 /* NCShareDownloadLimitViewController.swift */; }; + AB6000012F60000100FE2775 /* NCShareTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCShareTagEditorModel.swift */; }; + AB6000032F60000200FE2775 /* NCShareTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCShareTagEditorView.swift */; }; AA8E03DA2D2ED83300E7E89C /* TransientShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03D92D2ED83300E7E89C /* TransientShare.swift */; }; AA8E03DC2D2FBAC200E7E89C /* DownloadLimitUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */; }; AA8E041D2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */; }; @@ -1186,6 +1188,8 @@ AA8D316A2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewController.swift; sourceTree = ""; }; AA8D316B2D4123B200FE2775 /* NCShareDownloadLimitTableViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewControllerDelegate.swift; sourceTree = ""; }; AA8D316C2D4123B200FE2775 /* NCShareDownloadLimitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitViewController.swift; sourceTree = ""; }; + AB6000002F60000100FE2775 /* NCShareTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareTagEditorModel.swift; sourceTree = ""; }; + AB6000022F60000200FE2775 /* NCShareTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareTagEditorView.swift; sourceTree = ""; }; AA8E03D92D2ED83300E7E89C /* TransientShare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientShare.swift; sourceTree = ""; }; AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadLimitUITests.swift; sourceTree = ""; }; AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNetworkingDelegate.swift; sourceTree = ""; }; @@ -1261,6 +1265,7 @@ F34E1AD82ECC839100FA10C3 /* EmojiTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiTextField.swift; sourceTree = ""; }; F34E1ADA2ECC842200FA10C3 /* NCStatusMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCStatusMessageModel.swift; sourceTree = ""; }; F351D1A52D0AF24A00930F94 /* PHAssetCollection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PHAssetCollection+Extension.swift"; sourceTree = ""; }; + F35746602F6B0D27009F9F5A /* NextcloudKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NextcloudKit; path = ../NextcloudKit; sourceTree = SOURCE_ROOT; }; F359D8662A7D03420023F405 /* NCUtility+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCUtility+Exif.swift"; sourceTree = ""; }; F36C514D2E89393C0097E5F7 /* UIView+BlurVibrancy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+BlurVibrancy.swift"; sourceTree = ""; }; F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+SelectTabBarDelegate.swift"; sourceTree = ""; }; @@ -2369,6 +2374,8 @@ F769454722E9F20D000A798A /* NCShareNetworking.swift */, AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */, F769453F22E9F077000A798A /* NCSharePaging.swift */, + AB6000002F60000100FE2775 /* NCShareTagEditorModel.swift */, + AB6000022F60000200FE2775 /* NCShareTagEditorView.swift */, F774264822EB4D0000B23912 /* NCSearchUserDropDownCell.xib */, F769453B22E9CFFF000A798A /* NCShareUserCell.xib */, AF2D7C7D2742559100ADF566 /* NCShareUserCell.swift */, @@ -3277,6 +3284,7 @@ C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */, C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */, F7F1FBA62E27D13700C79E20 /* Frameworks */, + F35746602F6B0D27009F9F5A /* NextcloudKit */, ); sourceTree = ""; }; @@ -4699,6 +4707,8 @@ AA8D316F2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift in Sources */, AA8D31702D4123B200FE2775 /* DownloadLimitViewModel.swift in Sources */, AA8D31712D4123B200FE2775 /* NCShareDownloadLimitViewController.swift in Sources */, + AB6000012F60000100FE2775 /* NCShareTagEditorModel.swift in Sources */, + AB6000032F60000200FE2775 /* NCShareTagEditorView.swift in Sources */, AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */, F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */, F7A560422AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift in Sources */, diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index 1e2868ac50..2830455a9d 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -23,6 +23,8 @@ import UIKit import TagListView +import SwiftUI +import NextcloudKit class NCShareHeader: UIView { @IBOutlet weak var imageView: UIImageView! @@ -33,10 +35,13 @@ class NCShareHeader: UIView { @IBOutlet weak var fileNameTopConstraint: NSLayoutConstraint! @IBOutlet weak var tagListView: TagListView! + private var metadata = tableMetadata() + private var heightConstraintWithImage: NSLayoutConstraint? private var heightConstraintWithoutImage: NSLayoutConstraint? func setupUI(with metadata: tableMetadata) { + self.metadata = metadata.detachedCopy() let utilityFileSystem = NCUtilityFileSystem() if let image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) { fullWidthImageView.image = image @@ -64,7 +69,7 @@ class NCShareHeader: UIView { info.textColor = NCBrandColor.shared.textColor2 info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().getRelativeDateTitle(metadata.date as Date) - tagListView.addTags(Array(metadata.tags)) + refreshTags(Array(metadata.tags)) setNeedsLayout() layoutIfNeeded() @@ -75,4 +80,30 @@ class NCShareHeader: UIView { imageView.isHidden = traitCollection.verticalSizeClass != .compact } } + + func presentTagEditor(from sourceViewController: UIViewController, onApplied: (([String]) -> Void)? = nil) { + let editor = NCShareTagEditorView( + metadata: metadata.detachedCopy(), + initialTags: Array(metadata.tags), + windowScene: sourceViewController.view.window?.windowScene, + onApplied: { [weak self] tags in + self?.metadata.tags.removeAll() + self?.metadata.tags.append(objectsIn: tags) + self?.refreshTags(tags) + onApplied?(tags) + } + ) + let hosting = UIHostingController(rootView: editor) + hosting.title = NSLocalizedString("_tags_", comment: "") + if let sheet = hosting.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + } + sourceViewController.present(hosting, animated: true) + } + + private func refreshTags(_ tags: [String]) { + tagListView.removeAllTags() + tagListView.addTags(tags.sorted()) + } } diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 53cf2dbfea..278c20317e 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -52,6 +52,7 @@ class NCSharePaging: UIViewController { title = NSLocalizedString("_details_", comment: "") navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_close_", comment: ""), style: .plain, target: self, action: #selector(exitTapped(_:))) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_edit_tags_", comment: ""), style: .plain, target: self, action: #selector(editTagsTapped(_:))) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) @@ -168,6 +169,20 @@ class NCSharePaging: UIViewController { self.dismiss(animated: true, completion: nil) } + @objc func editTagsTapped(_ sender: Any?) { + guard let header = (pagingViewController.view as? NCSharePagingView)?.header else { + return + } + + header.presentTagEditor(from: self) { [weak self] tags in + guard let self else { return } + self.metadata.tags.removeAll() + self.metadata.tags.append(objectsIn: tags) + self.pagingViewController.metadata.tags.removeAll() + self.pagingViewController.metadata.tags.append(objectsIn: tags) + } + } + @objc func applicationDidEnterBackground(notification: Notification) { self.dismiss(animated: false, completion: nil) } diff --git a/iOSClient/Share/NCShareTagEditorModel.swift b/iOSClient/Share/NCShareTagEditorModel.swift new file mode 100644 index 0000000000..e98b75e44a --- /dev/null +++ b/iOSClient/Share/NCShareTagEditorModel.swift @@ -0,0 +1,194 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit +import NextcloudKit + +@MainActor +final class NCShareTagEditorModel: ObservableObject { + @Published var searchText: String = "" + @Published private(set) var tags: [NKTag] = [] + @Published private(set) var selectedTagIDs: Set = [] + @Published private(set) var pendingNewTagNames: Set = [] + @Published private(set) var isLoading = false + @Published private(set) var isSaving = false + @Published private(set) var hasLoaded = false + + private let metadata: tableMetadata + private let initialTagTokens: Set + private let windowScene: UIWindowScene? + private var initialAssignedTagIDs: Set = [] + + init(metadata: tableMetadata, initialTags: [String], windowScene: UIWindowScene?) { + self.metadata = metadata + self.initialTagTokens = Set(initialTags) + self.windowScene = windowScene + } + + var account: String { + metadata.account + } + + var filteredTags: [NKTag] { + let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + return tags.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + return tags + .filter { $0.name.localizedCaseInsensitiveContains(trimmed) } + .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + var createCandidateName: String? { + let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + return nil + } + + let hasExistingTag = tags.contains { + $0.name.compare(trimmed, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame + } + if hasExistingTag { + return nil + } + + let alreadyPending = pendingNewTagNames.contains { + $0.compare(trimmed, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame + } + + return alreadyPending ? nil : trimmed + } + + func isSelected(_ tag: NKTag) -> Bool { + selectedTagIDs.contains(tag.id) + } + + func loadTagsIfNeeded() async { + guard !hasLoaded else { + return + } + _ = await reloadTags(keepCurrentSelection: false) + } + + func toggleSelection(for tag: NKTag) { + if selectedTagIDs.contains(tag.id) { + selectedTagIDs.remove(tag.id) + } else { + selectedTagIDs.insert(tag.id) + } + } + + func addCreateCandidateToSelection() { + guard let candidate = createCandidateName else { + return + } + pendingNewTagNames.insert(candidate) + searchText = "" + } + + func saveChanges() async -> [String]? { + guard !metadata.fileId.isEmpty else { + await showErrorBanner( + windowScene: windowScene, + text: "_error_occurred_", + errorCode: NCGlobal.shared.errorInternalError + ) + return nil + } + + isSaving = true + defer { isSaving = false } + + if !pendingNewTagNames.isEmpty { + for name in pendingNewTagNames.sorted() { + let createResult = await NextcloudKit.shared.createTagAsync(name: name, account: metadata.account) + if createResult.error != .success { + await showErrorBanner(windowScene: windowScene, error: createResult.error) + return nil + } + } + + guard await reloadTags(keepCurrentSelection: true) else { + return nil + } + + for pendingName in pendingNewTagNames { + if let tag = tags.first(where: { + $0.name.compare(pendingName, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame + }) { + selectedTagIDs.insert(tag.id) + } + } + } + + let tagsToAdd = selectedTagIDs.subtracting(initialAssignedTagIDs) + let tagsToRemove = initialAssignedTagIDs.subtracting(selectedTagIDs) + + for tagID in tagsToAdd.sorted() { + let addResult = await NextcloudKit.shared.addTagToFileAsync(tagId: tagID, fileId: metadata.fileId, account: metadata.account) + if addResult.error != .success { + await showErrorBanner(windowScene: windowScene, error: addResult.error) + return nil + } + } + + for tagID in tagsToRemove.sorted() { + let removeResult = await NextcloudKit.shared.removeTagFromFileAsync(tagId: tagID, fileId: metadata.fileId, account: metadata.account) + if removeResult.error != .success { + await showErrorBanner(windowScene: windowScene, error: removeResult.error) + return nil + } + } + + let selectedTagNames = tags + .filter { selectedTagIDs.contains($0.id) } + .map(\.name) + .sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } + + await NCManageDatabase.shared.setMetadataTagsAsync(ocId: metadata.ocId, tags: selectedTagNames) + + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataNCShare) + + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: false, status: nil) + } + + initialAssignedTagIDs = selectedTagIDs + pendingNewTagNames.removeAll() + + return selectedTagNames + } + + private func reloadTags(keepCurrentSelection: Bool) async -> Bool { + isLoading = true + defer { + isLoading = false + hasLoaded = true + } + + let result = await NextcloudKit.shared.getTagsAsync(account: metadata.account) + guard result.error == .success, let receivedTags = result.tags else { + await showErrorBanner(windowScene: windowScene, error: result.error) + return false + } + + tags = receivedTags.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + + if keepCurrentSelection { + let validTagIDs = Set(tags.map(\.id)) + selectedTagIDs = Set(selectedTagIDs.filter { validTagIDs.contains($0) }) + return true + } + + let assignedIDs = Set(tags.filter { tag in + initialTagTokens.contains(tag.id) || initialTagTokens.contains(tag.name) + }.map(\.id)) + + initialAssignedTagIDs = assignedIDs + selectedTagIDs = assignedIDs + return true + } +} diff --git a/iOSClient/Share/NCShareTagEditorView.swift b/iOSClient/Share/NCShareTagEditorView.swift new file mode 100644 index 0000000000..909f574635 --- /dev/null +++ b/iOSClient/Share/NCShareTagEditorView.swift @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import UIKit + +struct NCShareTagEditorView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var model: NCShareTagEditorModel + + private let onApplied: ([String]) -> Void + + init(metadata: tableMetadata, initialTags: [String], windowScene: UIWindowScene?, onApplied: @escaping ([String]) -> Void) { + _model = StateObject(wrappedValue: NCShareTagEditorModel(metadata: metadata, initialTags: initialTags, windowScene: windowScene)) + self.onApplied = onApplied + } + + var body: some View { + NavigationStack { + List { + if let createCandidateName = model.createCandidateName { + Section { + Button { + model.addCreateCandidateToSelection() + } label: { + Label( + String(format: NSLocalizedString("_share_tags_create_", comment: ""), createCandidateName), + systemImage: "plus.circle.fill" + ) + } + } + } + + Section { + if model.filteredTags.isEmpty, model.createCandidateName == nil, !model.isLoading { + Text(NSLocalizedString("_share_tags_no_results_", comment: "")) + .foregroundStyle(.secondary) + } else { + ForEach(model.filteredTags, id: \.id) { tag in + Button { + model.toggleSelection(for: tag) + } label: { + HStack { + Text(tag.name) + .foregroundStyle(.primary) + + Spacer() + + if model.isSelected(tag) { + Image(systemName: "checkmark") + .foregroundStyle(Color(NCBrandColor.shared.getElement(account: model.account))) + } + } + } + } + } + } header: { + Text(NSLocalizedString("_tags_", comment: "")) + } + } + .listStyle(.plain) + .navigationTitle(NSLocalizedString("_tags_", comment: "")) + .searchable(text: $model.searchText, prompt: Text(NSLocalizedString("_search_", comment: ""))) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(NSLocalizedString("_cancel_", comment: "")) { + dismiss() + } + } + ToolbarItem(placement: .confirmationAction) { + Button(NSLocalizedString("_done_", comment: "")) { + Task { @MainActor in + guard let names = await model.saveChanges() else { + return + } + onApplied(names) + dismiss() + } + } + .disabled(model.isSaving || model.isLoading) + } + } + .overlay { + if model.isLoading || model.isSaving { + ProgressView() + } + } + } + .task { + await model.loadTagsIfNeeded() + } + } +} diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 1b9656f19a..a930cc57f1 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -86,6 +86,11 @@ "_add_folder_info_" = "Add folder info"; "_back_" = "Back"; "_search_" = "Search"; +"_search_or_create_tags" = "Search or create tag"; +"_tags_" = "Tags"; +"_share_tags_create_" = "Create \"%@\""; +"_share_tags_no_results_" = "No tags found"; +"_edit_tags_" = "Manage tags"; "_of_" = "of"; "_livephoto_save_" = "Save Live Photo to Photo Album"; "_livephoto_save_error_" = "Error during save of Live Photo."; From 8ab07007128a7f66188450b526f552500e7b91b0 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 1 Apr 2026 17:57:32 +0200 Subject: [PATCH 2/4] WIP Signed-off-by: Milen Pivchev --- iOSClient/Share/NCShareHeader.swift | 54 +++++++++++++++++++-- iOSClient/Share/NCSharePaging.swift | 4 +- iOSClient/Share/NCShareTagEditorModel.swift | 16 +++--- iOSClient/Share/NCShareTagEditorView.swift | 20 ++++++-- 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index 2830455a9d..37d05c9f72 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -39,6 +39,7 @@ class NCShareHeader: UIView { private var heightConstraintWithImage: NSLayoutConstraint? private var heightConstraintWithoutImage: NSLayoutConstraint? + private var currentTagsByToken: [String: NKTag] = [:] func setupUI(with metadata: tableMetadata) { self.metadata = metadata.detachedCopy() @@ -70,6 +71,7 @@ class NCShareHeader: UIView { info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().getRelativeDateTitle(metadata.date as Date) refreshTags(Array(metadata.tags)) + loadTagColors() setNeedsLayout() layoutIfNeeded() @@ -81,15 +83,15 @@ class NCShareHeader: UIView { } } - func presentTagEditor(from sourceViewController: UIViewController, onApplied: (([String]) -> Void)? = nil) { + func presentTagEditor(from sourceViewController: UIViewController, onApplied: (([NKTag]) -> Void)? = nil) { let editor = NCShareTagEditorView( metadata: metadata.detachedCopy(), initialTags: Array(metadata.tags), windowScene: sourceViewController.view.window?.windowScene, onApplied: { [weak self] tags in self?.metadata.tags.removeAll() - self?.metadata.tags.append(objectsIn: tags) - self?.refreshTags(tags) + self?.metadata.tags.append(objectsIn: tags.map(\.name)) + self?.refreshTags(tags.map(\.name), tagModels: tags) onApplied?(tags) } ) @@ -102,8 +104,50 @@ class NCShareHeader: UIView { sourceViewController.present(hosting, animated: true) } - private func refreshTags(_ tags: [String]) { + private func refreshTags(_ tags: [String], tagModels: [NKTag]? = nil) { + if let tagModels { + var tagsByToken: [String: NKTag] = [:] + for tag in tagModels { + tagsByToken[tag.id] = tag + tagsByToken[tag.name] = tag + } + currentTagsByToken = tagsByToken + } + tagListView.removeAllTags() - tagListView.addTags(tags.sorted()) + for tagToken in tags { + let matchedTag = currentTagsByToken[tagToken] + let displayName = matchedTag?.name ?? tagToken + + let tagView = tagListView.addTag(displayName) + if let colorHex = matchedTag?.color, let color = UIColor(hex: colorHex) { + tagView.tagBackgroundColor = color + tagView.textColor = color.isLight(threshold: 0.7) ? .black : .white + tagView.selectedTextColor = tagView.textColor + tagView.borderColor = color + } + } + } + + private func loadTagColors() { + let account = metadata.account + let selectedTokens = Set(Array(metadata.tags)) + guard !account.isEmpty, !selectedTokens.isEmpty else { + return + } + + NextcloudKit.shared.getTags(account: account) { [weak self] _, allTags, _, error in + guard let self, error == .success, let allTags else { + return + } + + let selectedTags = allTags.filter { tag in + selectedTokens.contains(tag.id) || selectedTokens.contains(tag.name) + } + + DispatchQueue.main.async { + self.refreshTags(Array(self.metadata.tags), tagModels: selectedTags) + } + } } } diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 278c20317e..5e83226306 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -177,9 +177,9 @@ class NCSharePaging: UIViewController { header.presentTagEditor(from: self) { [weak self] tags in guard let self else { return } self.metadata.tags.removeAll() - self.metadata.tags.append(objectsIn: tags) + self.metadata.tags.append(objectsIn: tags.map(\.name)) self.pagingViewController.metadata.tags.removeAll() - self.pagingViewController.metadata.tags.append(objectsIn: tags) + self.pagingViewController.metadata.tags.append(objectsIn: tags.map(\.name)) } } diff --git a/iOSClient/Share/NCShareTagEditorModel.swift b/iOSClient/Share/NCShareTagEditorModel.swift index e98b75e44a..c8b5126951 100644 --- a/iOSClient/Share/NCShareTagEditorModel.swift +++ b/iOSClient/Share/NCShareTagEditorModel.swift @@ -89,7 +89,13 @@ final class NCShareTagEditorModel: ObservableObject { searchText = "" } - func saveChanges() async -> [String]? { + var selectedTags: [NKTag] { + tags + .filter { selectedTagIDs.contains($0.id) } + .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + func saveChanges() async -> [NKTag]? { guard !metadata.fileId.isEmpty else { await showErrorBanner( windowScene: windowScene, @@ -143,10 +149,8 @@ final class NCShareTagEditorModel: ObservableObject { } } - let selectedTagNames = tags - .filter { selectedTagIDs.contains($0.id) } - .map(\.name) - .sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } + let selectedTags = self.selectedTags + let selectedTagNames = selectedTags.map(\.name) await NCManageDatabase.shared.setMetadataTagsAsync(ocId: metadata.ocId, tags: selectedTagNames) @@ -159,7 +163,7 @@ final class NCShareTagEditorModel: ObservableObject { initialAssignedTagIDs = selectedTagIDs pendingNewTagNames.removeAll() - return selectedTagNames + return selectedTags } private func reloadTags(keepCurrentSelection: Bool) async -> Bool { diff --git a/iOSClient/Share/NCShareTagEditorView.swift b/iOSClient/Share/NCShareTagEditorView.swift index 909f574635..190ee514bf 100644 --- a/iOSClient/Share/NCShareTagEditorView.swift +++ b/iOSClient/Share/NCShareTagEditorView.swift @@ -4,14 +4,15 @@ import SwiftUI import UIKit +import NextcloudKit struct NCShareTagEditorView: View { @Environment(\.dismiss) private var dismiss @StateObject private var model: NCShareTagEditorModel - private let onApplied: ([String]) -> Void + private let onApplied: ([NKTag]) -> Void - init(metadata: tableMetadata, initialTags: [String], windowScene: UIWindowScene?, onApplied: @escaping ([String]) -> Void) { + init(metadata: tableMetadata, initialTags: [String], windowScene: UIWindowScene?, onApplied: @escaping ([NKTag]) -> Void) { _model = StateObject(wrappedValue: NCShareTagEditorModel(metadata: metadata, initialTags: initialTags, windowScene: windowScene)) self.onApplied = onApplied } @@ -42,6 +43,10 @@ struct NCShareTagEditorView: View { model.toggleSelection(for: tag) } label: { HStack { + Circle() + .fill(color(for: tag)) + .frame(width: 10, height: 10) + Text(tag.name) .foregroundStyle(.primary) @@ -71,10 +76,10 @@ struct NCShareTagEditorView: View { ToolbarItem(placement: .confirmationAction) { Button(NSLocalizedString("_done_", comment: "")) { Task { @MainActor in - guard let names = await model.saveChanges() else { + guard let selectedTags = await model.saveChanges() else { return } - onApplied(names) + onApplied(selectedTags) dismiss() } } @@ -91,4 +96,11 @@ struct NCShareTagEditorView: View { await model.loadTagsIfNeeded() } } + + private func color(for tag: NKTag) -> Color { + if let colorHex = tag.color, let color = UIColor(hex: colorHex) { + return Color(color) + } + return .secondary + } } From 21406f8a1167ee74a6f1ad90e955d2e075dfb15b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 9 Apr 2026 16:43:29 +0200 Subject: [PATCH 3/4] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 16 ++-- Share/NCShareExtension+DataSource.swift | 2 +- .../Data/NCManageDatabase+Metadata.swift | 11 +++ .../Collection Common/Cell/NCListCell.swift | 83 ++++++++++++++++++- iOSClient/Select/NCSelect.swift | 2 +- iOSClient/Share/NCShareHeader.swift | 8 +- iOSClient/Share/NCSharePaging.swift | 9 +- ...itorModel.swift => NCTagEditorModel.swift} | 16 ++-- ...EditorView.swift => NCTagEditorView.swift} | 6 +- 9 files changed, 123 insertions(+), 30 deletions(-) rename iOSClient/Share/{NCShareTagEditorModel.swift => NCTagEditorModel.swift} (93%) rename iOSClient/Share/{NCShareTagEditorView.swift => NCTagEditorView.swift} (94%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8d14a4b55a..61c096118e 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -36,8 +36,6 @@ AA8D316F2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D316A2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift */; }; AA8D31702D4123B200FE2775 /* DownloadLimitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D31692D4123B200FE2775 /* DownloadLimitViewModel.swift */; }; AA8D31712D4123B200FE2775 /* NCShareDownloadLimitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8D316C2D4123B200FE2775 /* NCShareDownloadLimitViewController.swift */; }; - AB6000012F60000100FE2775 /* NCShareTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCShareTagEditorModel.swift */; }; - AB6000032F60000200FE2775 /* NCShareTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCShareTagEditorView.swift */; }; AA8E03DA2D2ED83300E7E89C /* TransientShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03D92D2ED83300E7E89C /* TransientShare.swift */; }; AA8E03DC2D2FBAC200E7E89C /* DownloadLimitUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */; }; AA8E041D2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */; }; @@ -46,6 +44,8 @@ AABD0C8A2D5F67A400F009E6 /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABD0C892D5F67A200F009E6 /* XCUIElement.swift */; }; AABD0C9B2D5F73FC00F009E6 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABD0C9A2D5F73FA00F009E6 /* Placeholder.swift */; }; AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */; }; + AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; }; + AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; }; @@ -1188,8 +1188,6 @@ AA8D316A2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewController.swift; sourceTree = ""; }; AA8D316B2D4123B200FE2775 /* NCShareDownloadLimitTableViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewControllerDelegate.swift; sourceTree = ""; }; AA8D316C2D4123B200FE2775 /* NCShareDownloadLimitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitViewController.swift; sourceTree = ""; }; - AB6000002F60000100FE2775 /* NCShareTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareTagEditorModel.swift; sourceTree = ""; }; - AB6000022F60000200FE2775 /* NCShareTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareTagEditorView.swift; sourceTree = ""; }; AA8E03D92D2ED83300E7E89C /* TransientShare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransientShare.swift; sourceTree = ""; }; AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadLimitUITests.swift; sourceTree = ""; }; AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNetworkingDelegate.swift; sourceTree = ""; }; @@ -1213,6 +1211,8 @@ AACCAB632CFE04F700DA1786 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/Localizable.strings; sourceTree = ""; }; AACCAB642CFE04F700DA1786 /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lo; path = lo.lproj/InfoPlist.strings; sourceTree = ""; }; AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNavigationTitleSetting.swift; sourceTree = ""; }; + AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; }; + AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; }; AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; }; @@ -2374,8 +2374,8 @@ F769454722E9F20D000A798A /* NCShareNetworking.swift */, AA8E041C2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift */, F769453F22E9F077000A798A /* NCSharePaging.swift */, - AB6000002F60000100FE2775 /* NCShareTagEditorModel.swift */, - AB6000022F60000200FE2775 /* NCShareTagEditorView.swift */, + AB6000002F60000100FE2775 /* NCTagEditorModel.swift */, + AB6000022F60000200FE2775 /* NCTagEditorView.swift */, F774264822EB4D0000B23912 /* NCSearchUserDropDownCell.xib */, F769453B22E9CFFF000A798A /* NCShareUserCell.xib */, AF2D7C7D2742559100ADF566 /* NCShareUserCell.swift */, @@ -4707,8 +4707,8 @@ AA8D316F2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift in Sources */, AA8D31702D4123B200FE2775 /* DownloadLimitViewModel.swift in Sources */, AA8D31712D4123B200FE2775 /* NCShareDownloadLimitViewController.swift in Sources */, - AB6000012F60000100FE2775 /* NCShareTagEditorModel.swift in Sources */, - AB6000032F60000200FE2775 /* NCShareTagEditorView.swift in Sources */, + AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */, + AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */, AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */, F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */, F7A560422AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift in Sources */, diff --git a/Share/NCShareExtension+DataSource.swift b/Share/NCShareExtension+DataSource.swift index bd2e3a1b10..2893fb505d 100644 --- a/Share/NCShareExtension+DataSource.swift +++ b/Share/NCShareExtension+DataSource.swift @@ -102,7 +102,7 @@ extension NCShareExtension: UICollectionViewDataSource { cell.imageStatus.image = utility.loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor2]) } - cell.setTags(tags: Array(metadata.tags)) + cell.setTags(tags: Array(metadata.tags), account: metadata.account) cell.separator.isHidden = collectionView.numberOfItems(inSection: indexPath.section) == indexPath.row + 1 diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 572fb2bb13..371f63247c 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -754,6 +754,17 @@ extension NCManageDatabase { } } + func setMetadataTagsAsync(ocId: String, tags: [String]) async { + await core.performRealmWriteAsync { realm in + guard let metadata = realm.object(ofType: tableMetadata.self, forPrimaryKey: ocId) else { + return + } + + metadata.tags.removeAll() + metadata.tags.append(objectsIn: tags) + } + } + func moveMetadataAsync(ocId: String, serverUrlTo: String) async { await core.performRealmWriteAsync { realm in if let result = realm.objects(tableMetadata.self) diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 771573e7c9..dbbf00f20e 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -14,6 +14,9 @@ protocol NCListCellDelegate: AnyObject { } class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainProtocol { + private static var tagColorsByAccount: [String: [String: NKTag]] = [:] + private static var loadingTagColorsForAccounts: Set = [] + @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageSelect: UIImageView! @IBOutlet weak var imageStatus: UIImageView! @@ -24,8 +27,8 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP @IBOutlet weak var labelInfo: UILabel! @IBOutlet weak var labelSubinfo: UILabel! @IBOutlet weak var labelInfoSeparator: UILabel! - @IBOutlet weak var tag0: UILabel! - @IBOutlet weak var tag1: UILabel! + @IBOutlet weak var tag0: PaddedAndBorderedLabel! + @IBOutlet weak var tag1: PaddedAndBorderedLabel! @IBOutlet weak var labelExtension: UILabel! @IBOutlet weak var buttonShared: UIButton! @@ -38,6 +41,8 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP @IBOutlet weak var separatorHeightConstraint: NSLayoutConstraint! weak var delegate: NCListCellDelegate? + private var currentTagTokens: [String] = [] + private var currentTagAccount: String = "" // Cell Protocol var metadata: tableMetadata? { @@ -113,6 +118,8 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP labelSubinfo.text = "" tag0.text = "" tag1.text = "" + currentTagTokens = [] + currentTagAccount = "" // Dynamic Type Font Configuration // @@ -258,7 +265,11 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP accessibilityValue = value } - func setTags(tags: [String]) { + func setTags(tags: [String], account: String) { + currentTagTokens = tags + currentTagAccount = account + applyDefaultTagBorderStyle() + if tags.isEmpty { tag0.isHidden = true tag1.isHidden = true @@ -280,6 +291,70 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP } } } + + applyTagBorderColorsIfAvailable() + loadTagColorsIfNeeded(account: account) + } + + private func applyDefaultTagBorderStyle() { + tag0.backgroundColor = .clear + tag1.backgroundColor = .clear + tag0.borderColor = .systemGray5 + tag1.borderColor = .systemGray5 + tag0.textColor = .systemGray + tag1.textColor = .systemGray + tag0.setNeedsDisplay() + tag1.setNeedsDisplay() + } + + private func applyTagBorderColorsIfAvailable() { + guard !currentTagTokens.isEmpty, + let lookup = NCListCell.tagColorsByAccount[currentTagAccount], + let firstToken = currentTagTokens.first, + let colorHex = lookup[firstToken]?.color, + let color = UIColor(hex: colorHex) else { + return + } + + tag0.borderColor = color + tag0.textColor = color + if !tag1.isHidden { + tag1.borderColor = .systemGray5 + tag1.textColor = .systemGray + } + tag0.setNeedsDisplay() + tag1.setNeedsDisplay() + } + + private func loadTagColorsIfNeeded(account: String) { + guard !account.isEmpty else { + return + } + if NCListCell.tagColorsByAccount[account] != nil || NCListCell.loadingTagColorsForAccounts.contains(account) { + return + } + + NCListCell.loadingTagColorsForAccounts.insert(account) + NextcloudKit.shared.getTags(account: account) { [weak self] _, tags, _, error in + DispatchQueue.main.async { + NCListCell.loadingTagColorsForAccounts.remove(account) + guard error == .success, let tags else { + return + } + + var lookup: [String: NKTag] = [:] + for tag in tags { + lookup[tag.id] = tag + lookup[tag.name] = tag + } + NCListCell.tagColorsByAccount[account] = lookup + + guard let self, self.currentTagAccount == account else { + return + } + self.applyTagBorderColorsIfAvailable() + } + } } func setIconOutlines() { @@ -509,7 +584,7 @@ extension NCCollectionViewCommon { } // TAGS - cell.setTags(tags: Array(metadata.tags)) + cell.setTags(tags: Array(metadata.tags), account: metadata.account) // SearchingMode - TAG Separator Hidden if isSearchingMode { diff --git a/iOSClient/Select/NCSelect.swift b/iOSClient/Select/NCSelect.swift index 775de939ab..64665f5624 100644 --- a/iOSClient/Select/NCSelect.swift +++ b/iOSClient/Select/NCSelect.swift @@ -455,7 +455,7 @@ extension NCSelect: UICollectionViewDataSource { } // Add TAGS - cell.setTags(tags: Array(metadata.tags)) + cell.setTags(tags: Array(metadata.tags), account: metadata.account) return cell } diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index 37d05c9f72..cc685582db 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -84,7 +84,7 @@ class NCShareHeader: UIView { } func presentTagEditor(from sourceViewController: UIViewController, onApplied: (([NKTag]) -> Void)? = nil) { - let editor = NCShareTagEditorView( + let editor = NCTagEditorView( metadata: metadata.detachedCopy(), initialTags: Array(metadata.tags), windowScene: sourceViewController.view.window?.windowScene, @@ -121,10 +121,10 @@ class NCShareHeader: UIView { let tagView = tagListView.addTag(displayName) if let colorHex = matchedTag?.color, let color = UIColor(hex: colorHex) { - tagView.tagBackgroundColor = color - tagView.textColor = color.isLight(threshold: 0.7) ? .black : .white - tagView.selectedTextColor = tagView.textColor + tagView.tagBackgroundColor = .clear tagView.borderColor = color + tagView.textColor = color + tagView.selectedTextColor = color } } } diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 5e83226306..7e6ac4fd48 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -52,7 +52,14 @@ class NCSharePaging: UIViewController { title = NSLocalizedString("_details_", comment: "") navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_close_", comment: ""), style: .plain, target: self, action: #selector(exitTapped(_:))) - navigationItem.rightBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_edit_tags_", comment: ""), style: .plain, target: self, action: #selector(editTagsTapped(_:))) + + let manageTagsAction = UIAction(title: NSLocalizedString("_edit_tags_", comment: ""), image: UIImage(systemName: "tag")) { [weak self] _ in + self?.editTagsTapped(nil) + } + + let moreButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), style: .plain, target: nil, action: nil) + moreButton.menu = UIMenu(children: [manageTagsAction]) + navigationItem.rightBarButtonItem = moreButton NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) diff --git a/iOSClient/Share/NCShareTagEditorModel.swift b/iOSClient/Share/NCTagEditorModel.swift similarity index 93% rename from iOSClient/Share/NCShareTagEditorModel.swift rename to iOSClient/Share/NCTagEditorModel.swift index c8b5126951..1fc332ead4 100644 --- a/iOSClient/Share/NCShareTagEditorModel.swift +++ b/iOSClient/Share/NCTagEditorModel.swift @@ -7,14 +7,14 @@ import UIKit import NextcloudKit @MainActor -final class NCShareTagEditorModel: ObservableObject { - @Published var searchText: String = "" - @Published private(set) var tags: [NKTag] = [] - @Published private(set) var selectedTagIDs: Set = [] - @Published private(set) var pendingNewTagNames: Set = [] - @Published private(set) var isLoading = false - @Published private(set) var isSaving = false - @Published private(set) var hasLoaded = false +@Observable final class NCTagEditorModel { + var searchText: String = "" + private(set) var tags: [NKTag] = [] + private(set) var selectedTagIDs: Set = [] + private(set) var pendingNewTagNames: Set = [] + private(set) var isLoading = false + private(set) var isSaving = false + private(set) var hasLoaded = false private let metadata: tableMetadata private let initialTagTokens: Set diff --git a/iOSClient/Share/NCShareTagEditorView.swift b/iOSClient/Share/NCTagEditorView.swift similarity index 94% rename from iOSClient/Share/NCShareTagEditorView.swift rename to iOSClient/Share/NCTagEditorView.swift index 190ee514bf..7a9db194d8 100644 --- a/iOSClient/Share/NCShareTagEditorView.swift +++ b/iOSClient/Share/NCTagEditorView.swift @@ -6,14 +6,14 @@ import SwiftUI import UIKit import NextcloudKit -struct NCShareTagEditorView: View { +struct NCTagEditorView: View { @Environment(\.dismiss) private var dismiss - @StateObject private var model: NCShareTagEditorModel + @State private var model: NCTagEditorModel private let onApplied: ([NKTag]) -> Void init(metadata: tableMetadata, initialTags: [String], windowScene: UIWindowScene?, onApplied: @escaping ([NKTag]) -> Void) { - _model = StateObject(wrappedValue: NCShareTagEditorModel(metadata: metadata, initialTags: initialTags, windowScene: windowScene)) + _model = State(initialValue: NCTagEditorModel(metadata: metadata, initialTags: initialTags, windowScene: windowScene)) self.onApplied = onApplied } From 5cbb451d13e739743ac634599fd921c2ba4b446e Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 9 Apr 2026 16:54:45 +0200 Subject: [PATCH 4/4] WIP Signed-off-by: Milen Pivchev --- iOSClient/Main/Collection Common/Cell/NCListCell.swift | 5 +++-- iOSClient/Share/NCShareHeader.swift | 6 ++++-- iOSClient/Share/NCTagEditorModel.swift | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index dbbf00f20e..06b3926ea0 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -335,10 +335,11 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP } NCListCell.loadingTagColorsForAccounts.insert(account) - NextcloudKit.shared.getTags(account: account) { [weak self] _, tags, _, error in + Task { [weak self] in + let result = await NextcloudKit.shared.getTags(account: account) DispatchQueue.main.async { NCListCell.loadingTagColorsForAccounts.remove(account) - guard error == .success, let tags else { + guard result.error == .success, let tags = result.tags else { return } diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index cc685582db..1d2f81946b 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -136,8 +136,10 @@ class NCShareHeader: UIView { return } - NextcloudKit.shared.getTags(account: account) { [weak self] _, allTags, _, error in - guard let self, error == .success, let allTags else { + Task { [weak self] in + guard let self else { return } + let result = await NextcloudKit.shared.getTags(account: account) + guard result.error == .success, let allTags = result.tags else { return } diff --git a/iOSClient/Share/NCTagEditorModel.swift b/iOSClient/Share/NCTagEditorModel.swift index 1fc332ead4..7825676431 100644 --- a/iOSClient/Share/NCTagEditorModel.swift +++ b/iOSClient/Share/NCTagEditorModel.swift @@ -110,7 +110,7 @@ import NextcloudKit if !pendingNewTagNames.isEmpty { for name in pendingNewTagNames.sorted() { - let createResult = await NextcloudKit.shared.createTagAsync(name: name, account: metadata.account) + let createResult = await NextcloudKit.shared.createTag(name: name, account: metadata.account) if createResult.error != .success { await showErrorBanner(windowScene: windowScene, error: createResult.error) return nil @@ -134,7 +134,7 @@ import NextcloudKit let tagsToRemove = initialAssignedTagIDs.subtracting(selectedTagIDs) for tagID in tagsToAdd.sorted() { - let addResult = await NextcloudKit.shared.addTagToFileAsync(tagId: tagID, fileId: metadata.fileId, account: metadata.account) + let addResult = await NextcloudKit.shared.addTagToFile(tagId: tagID, fileId: metadata.fileId, account: metadata.account) if addResult.error != .success { await showErrorBanner(windowScene: windowScene, error: addResult.error) return nil @@ -142,7 +142,7 @@ import NextcloudKit } for tagID in tagsToRemove.sorted() { - let removeResult = await NextcloudKit.shared.removeTagFromFileAsync(tagId: tagID, fileId: metadata.fileId, account: metadata.account) + let removeResult = await NextcloudKit.shared.removeTagFromFile(tagId: tagID, fileId: metadata.fileId, account: metadata.account) if removeResult.error != .success { await showErrorBanner(windowScene: windowScene, error: removeResult.error) return nil @@ -173,7 +173,7 @@ import NextcloudKit hasLoaded = true } - let result = await NextcloudKit.shared.getTagsAsync(account: metadata.account) + let result = await NextcloudKit.shared.getTags(account: metadata.account) guard result.error == .success, let receivedTags = result.tags else { await showErrorBanner(windowScene: windowScene, error: result.error) return false