Replace Flutter app with native SwiftUI rewrite (iOS 26)

Complete ground-up rewrite in SwiftUI targeting iOS 26. Drops the Flutter/Dart
codebase entirely in favour of a lean native app with no third-party dependencies.

Features shipped:
- Bookmark list with pagination, pull-to-refresh, swipe delete/archive
- Add bookmark form + iOS share extension (zero-tap save from any app)
- Tags tab — all tags sorted by count, tap to browse filtered bookmarks
- Native search tab (Tab role: .search) with instant client-side filtering
- AI enrichment via OpenRouter (google/gemini-2.0-flash-lite-001): auto-summary
  and tag generation, semantic search, smart collections
- Settings: linkding server config, OpenRouter API key
- App Groups for credential sharing between main app and share extension
- Swift 6 strict concurrency throughout (@Observable, @MainActor)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krishna Kumar
2026-05-21 08:33:01 -05:00
parent cd85f9c67f
commit b7ef5c2215
306 changed files with 2337 additions and 17949 deletions

View File

@@ -0,0 +1,73 @@
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
}
}
struct BookmarkResponse: Codable, Sendable {
let count: Int
let next: String?
let previous: String?
let results: [Bookmark]
}
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 SmartCollection: Identifiable, Sendable {
let id = UUID()
let name: String
let description: String
let bookmarkIds: [Int]
}

View File

@@ -0,0 +1,38 @@
import Foundation
struct ServerConfig: Codable, Sendable {
var host: String
var port: Int?
var path: String
var token: String
var useHttps: Bool
var baseUrl: String {
var url = useHttps ? "https://" : "http://"
url += host
if let port { url += ":\(port)" }
if !path.isEmpty { url += path }
return url
}
}
extension ServerConfig {
private static let key = "serverConfig"
private static let appGroupId = "group.com.magicive.marks"
static func load() -> ServerConfig? {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(ServerConfig.self, from: data)
}
func save() {
guard let data = try? JSONEncoder().encode(self) else { return }
UserDefaults.standard.set(data, forKey: ServerConfig.key)
UserDefaults(suiteName: ServerConfig.appGroupId)?.set(data, forKey: ServerConfig.key)
}
func delete() {
UserDefaults.standard.removeObject(forKey: ServerConfig.key)
UserDefaults(suiteName: ServerConfig.appGroupId)?.removeObject(forKey: ServerConfig.key)
}
}