All checks were successful
CI / build-and-deploy (push) Successful in 20s
Replace the fire-and-forget share toast with an interactive SwiftUI card. On open it checks linkding's /api/bookmarks/check/ so an already-saved URL routes to an Update (no more silent duplicates) and a new URL shows a save form with title, tags, notes, and a read-later toggle. Tag suggestions are hybrid: fetch the user's existing tag vocabulary (/api/tags/) and AI tag ideas (/v1/marks/enrich via anonymous device auth), auto-fill known-vocabulary matches into the field and surface net-new ideas as tappable chips. Best-effort and non-blocking — failures never block save. Also: - Refresh bookmarks when the app returns to the foreground (scenePhase). - Render bookmark dates as a static relative label instead of the live ticking RelativeDateTime timer. - Share LinkdingAPI/Bookmark/ServerConfig/MarksAuth into the ShareExtension target via project.yml (durable across xcodegen regen) and regenerate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.2 KiB
Swift
65 lines
2.2 KiB
Swift
import UIKit
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
@objc(ShareViewController)
|
|
class ShareViewController: UIViewController {
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
|
blur.frame = view.bounds
|
|
blur.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
view.addSubview(blur)
|
|
extractURL()
|
|
}
|
|
|
|
private func extractURL() {
|
|
guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
|
|
let attachments = item.attachments else {
|
|
present(url: nil)
|
|
return
|
|
}
|
|
|
|
func load(typeId: String) -> Bool {
|
|
guard let attachment = attachments.first(where: { $0.hasItemConformingToTypeIdentifier(typeId) }) else {
|
|
return false
|
|
}
|
|
attachment.loadItem(forTypeIdentifier: typeId, options: nil) { [weak self] item, _ in
|
|
let urlString = typeId == UTType.url.identifier
|
|
? ((item as? URL)?.absoluteString ?? (item as? String))
|
|
: (item as? String)
|
|
DispatchQueue.main.async { self?.present(url: urlString) }
|
|
}
|
|
return true
|
|
}
|
|
|
|
if !load(typeId: UTType.url.identifier) && !load(typeId: UTType.plainText.identifier) {
|
|
present(url: nil)
|
|
}
|
|
}
|
|
|
|
/// Hosts the SwiftUI save card. A nil URL surfaces an error state in the card.
|
|
private func present(url: String?) {
|
|
let root = ShareView(url: url ?? "") { [weak self] _ in
|
|
self?.close()
|
|
}
|
|
let host = UIHostingController(rootView: root)
|
|
host.view.backgroundColor = .clear
|
|
addChild(host)
|
|
host.view.frame = view.bounds
|
|
host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
view.addSubview(host.view)
|
|
host.didMove(toParent: self)
|
|
|
|
if url == nil {
|
|
// No URL to work with — let the card render its failure state briefly.
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { [weak self] in self?.close() }
|
|
}
|
|
}
|
|
|
|
private func close() {
|
|
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
}
|
|
}
|