ReadKeep/URLShare/ShareBookmarkViewModel.swift
Ilyas Hallak 5b2d177f94 feat: Enhanced tag management with unified search and keyboard handling
- 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
2025-07-30 23:53:30 +02:00

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)
}
}
}
}
}
}