Files
linkding-ios/Marks/Models/Bookmark.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

125 lines
3.2 KiB
Swift

import Foundation
struct Bookmark: Codable, Identifiable, Sendable, Hashable {
let id: Int
var url: String
var title: String
var description: String
var tagNames: [String]
var dateAdded: Date
var dateModified: Date
var isArchived: Bool
var unread: Bool
var shared: Bool
var websiteTitle: String?
var websiteDescription: String?
var faviconUrl: String?
var previewImageUrl: String?
// AI-enriched fields stored locally
var aiSummary: String?
var aiTags: [String]?
enum CodingKeys: String, CodingKey {
case id, url, title, description, unread, shared
case tagNames = "tag_names"
case dateAdded = "date_added"
case dateModified = "date_modified"
case isArchived = "is_archived"
case websiteTitle = "website_title"
case websiteDescription = "website_description"
case faviconUrl = "favicon_url"
case previewImageUrl = "preview_image_url"
}
var displayTitle: String {
if !title.isEmpty { return title }
if let t = websiteTitle, !t.isEmpty { return t }
return url
}
var domain: String {
URL(string: url)?.host ?? url
}
/// A short content snippet for list previews: the page's scraped meta
/// description, falling back to the user's own note. nil when neither exists.
var contentExcerpt: String? {
for candidate in [websiteDescription, description] {
if let c = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !c.isEmpty {
return c
}
}
return nil
}
}
struct BookmarkResponse: Codable, Sendable {
let count: Int
let next: String?
let previous: String?
let results: [Bookmark]
}
/// Response of linkding's `GET /api/bookmarks/check/?url=` tells us whether the
/// URL is already saved, plus scraped metadata and suggested tags for new saves.
struct BookmarkCheck: Codable, Sendable {
let bookmark: Bookmark?
let metadata: Metadata?
let autoTags: [String]?
struct Metadata: Codable, Sendable {
let title: String?
let description: String?
}
enum CodingKeys: String, CodingKey {
case bookmark, metadata
case autoTags = "auto_tags"
}
}
struct BookmarkCreate: Codable, Sendable {
let url: String
let title: String
let tagNames: [String]
let isArchived: Bool
let unread: Bool
let shared: Bool
enum CodingKeys: String, CodingKey {
case url, title, unread, shared
case tagNames = "tag_names"
case isArchived = "is_archived"
}
}
struct BookmarkUpdate: Codable, Sendable {
var url: String
var title: String
var description: String
var tagNames: [String]
var unread: Bool
var shared: Bool
enum CodingKeys: String, CodingKey {
case url, title, description, unread, shared
case tagNames = "tag_names"
}
}
struct LinkdingTag: Codable, Sendable {
let name: String
}
struct TagResponse: Codable, Sendable {
let results: [LinkdingTag]
}
struct SmartCollection: Identifiable, Sendable {
let id = UUID()
let name: String
let description: String
let bookmarkIds: [Int]
}