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:
73
Marks/Models/Bookmark.swift
Normal file
73
Marks/Models/Bookmark.swift
Normal 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]
|
||||
}
|
||||
38
Marks/Models/ServerConfig.swift
Normal file
38
Marks/Models/ServerConfig.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user