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:
93
Marks/Views/BookmarkRow.swift
Normal file
93
Marks/Views/BookmarkRow.swift
Normal file
@@ -0,0 +1,93 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarkRow: View {
|
||||
let bookmark: Bookmark
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .top, spacing: 11) {
|
||||
FaviconView(url: bookmark.faviconUrl)
|
||||
.padding(.top, 1)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(bookmark.displayTitle)
|
||||
.font(.system(size: 22, weight: .medium))
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Text(bookmark.domain)
|
||||
.font(.system(size: 16))
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
}
|
||||
|
||||
if let summary = bookmark.aiSummary {
|
||||
Text(summary)
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
.padding(.leading, 40)
|
||||
}
|
||||
|
||||
if !effectiveTags.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(effectiveTags, id: \.self) { tag in
|
||||
Text(tag)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundStyle(.blue)
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 40)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 14)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private var effectiveTags: [String] {
|
||||
let base = bookmark.tagNames
|
||||
let ai = bookmark.aiTags ?? []
|
||||
let extra = ai.filter { !base.contains($0) }.prefix(3)
|
||||
return (base + extra).prefix(6).map { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
struct FaviconView: View {
|
||||
let url: String?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let urlString = url, let faviconUrl = URL(string: urlString) {
|
||||
AsyncImage(url: faviconUrl) { phase in
|
||||
switch phase {
|
||||
case .success(let image):
|
||||
image.resizable().scaledToFit()
|
||||
default:
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
} else {
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
.frame(width: 26, height: 26)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
|
||||
private var placeholder: some View {
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color(.systemGray5))
|
||||
.overlay {
|
||||
Image(systemName: "bookmark")
|
||||
.font(.system(size: 9, weight: .medium))
|
||||
.foregroundStyle(Color(.systemGray2))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user