- Added search functionality to BookmarkLabelsView with real-time filtering - Implemented custom tag creation with smart suggestions - Unified search and tag selection in ShareBookmarkView - Added keyboard toolbar with 'Done' button for extensions - Implemented notification-based keyboard dismissal for extensions - Added pagination logic to ShareBookmarkViewModel - Created selected tags section with remove functionality - Improved UX with consistent tag management across views - Added proper keyboard handling for iOS extensions
109 lines
4.0 KiB
Swift
109 lines
4.0 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
class ShareBookmarkViewModel: ObservableObject {
|
|
@Published var url: String?
|
|
@Published var title: String = ""
|
|
@Published var labels: [BookmarkLabelDto] = []
|
|
@Published var selectedLabels: Set<String> = []
|
|
@Published var statusMessage: (text: String, isError: Bool, emoji: String)? = nil
|
|
@Published var isSaving: Bool = false
|
|
@Published var searchText: String = ""
|
|
let extensionContext: NSExtensionContext?
|
|
|
|
// Computed properties for pagination
|
|
var availableLabels: [BookmarkLabelDto] {
|
|
return labels.filter { !selectedLabels.contains($0.name) }
|
|
}
|
|
|
|
// Computed property for filtered labels based on search text
|
|
var filteredLabels: [BookmarkLabelDto] {
|
|
if searchText.isEmpty {
|
|
return availableLabels
|
|
} else {
|
|
return availableLabels.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
|
|
}
|
|
}
|
|
|
|
var availableLabelPages: [[BookmarkLabelDto]] {
|
|
let pageSize = Constants.Labels.pageSize
|
|
let labelsToShow = searchText.isEmpty ? availableLabels : filteredLabels
|
|
|
|
if labelsToShow.count <= pageSize {
|
|
return [labelsToShow]
|
|
} else {
|
|
return stride(from: 0, to: labelsToShow.count, by: pageSize).map {
|
|
Array(labelsToShow[$0..<min($0 + pageSize, labelsToShow.count)])
|
|
}
|
|
}
|
|
}
|
|
|
|
init(extensionContext: NSExtensionContext?) {
|
|
self.extensionContext = extensionContext
|
|
extractSharedContent()
|
|
}
|
|
|
|
func onAppear() {
|
|
loadLabels()
|
|
}
|
|
|
|
private func extractSharedContent() {
|
|
guard let extensionContext = extensionContext else { return }
|
|
for item in extensionContext.inputItems {
|
|
guard let inputItem = item as? NSExtensionItem else { continue }
|
|
for attachment in inputItem.attachments ?? [] {
|
|
if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
|
attachment.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (url, error) in
|
|
DispatchQueue.main.async {
|
|
if let url = url as? URL {
|
|
self?.url = url.absoluteString
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if attachment.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
|
attachment.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (text, error) in
|
|
DispatchQueue.main.async {
|
|
if let text = text as? String, let url = URL(string: text) {
|
|
self?.url = url.absoluteString
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadLabels() {
|
|
Task {
|
|
let loaded = await SimpleAPI.getBookmarkLabels { [weak self] message, error in
|
|
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
|
} ?? []
|
|
let sorted = loaded.sorted { $0.count > $1.count }
|
|
await MainActor.run {
|
|
self.labels = Array(sorted)
|
|
}
|
|
}
|
|
}
|
|
|
|
func save() {
|
|
guard let url = url, !url.isEmpty else {
|
|
statusMessage = ("No URL found.", true, "❌")
|
|
return
|
|
}
|
|
isSaving = true
|
|
Task {
|
|
await SimpleAPI.addBookmark(title: title, url: url, labels: Array(selectedLabels)) { [weak self] message, error in
|
|
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
|
self?.isSaving = false
|
|
if !error {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|