Files
linkding-ios/ShareExtension/ShareViewController.swift
Krishna Kumar e82e69c5f3
All checks were successful
CI / build-and-deploy (push) Successful in 20s
feat(share): dup-aware interactive save card with AI tag suggestions
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>
2026-06-23 20:36:46 -05:00

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