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:
142
Marks/Views/BookmarksView.swift
Normal file
142
Marks/Views/BookmarksView.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarksView: View {
|
||||
@Bindable var viewModel: BookmarksViewModel
|
||||
let onDisconnect: () -> Void
|
||||
|
||||
@Environment(\.openURL) private var openURL
|
||||
@State private var showSettings = false
|
||||
@State private var showCollections = false
|
||||
@State private var showAddBookmark = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(viewModel.bookmarks) { bookmark in
|
||||
BookmarkRow(bookmark: bookmark)
|
||||
.onTapGesture {
|
||||
if let url = URL(string: bookmark.url) { openURL(url) }
|
||||
}
|
||||
.onAppear { maybeLoadMore(bookmark) }
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
|
||||
.listRowSeparator(.visible)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(role: .destructive) {
|
||||
Task { await viewModel.delete(bookmark) }
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .leading) {
|
||||
Button {
|
||||
Task { await viewModel.archive(bookmark) }
|
||||
} label: {
|
||||
Label("Archive", systemImage: "archivebox")
|
||||
}
|
||||
.tint(.orange)
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.isLoadingMore {
|
||||
HStack { Spacer(); ProgressView(); Spacer() }
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Bookmarks")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
if viewModel.claude != nil {
|
||||
Menu {
|
||||
Button {
|
||||
Task { await viewModel.generateSmartCollections() }
|
||||
showCollections = true
|
||||
} label: {
|
||||
Label("Smart Collections", systemImage: "sparkles")
|
||||
}
|
||||
Button {
|
||||
Task { await viewModel.enrichAll() }
|
||||
} label: {
|
||||
Label("Enrich All with AI", systemImage: "wand.and.stars")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "sparkles")
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
HStack(spacing: 16) {
|
||||
Button { showAddBookmark = true } label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
Button { showSettings = true } label: {
|
||||
Image(systemName: "gearshape")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||
ProgressView()
|
||||
}
|
||||
if !viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||
ContentUnavailableView("No Bookmarks", systemImage: "bookmark", description: Text("Bookmarks you save will appear here."))
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if viewModel.enrichmentProgress > 0 {
|
||||
enrichmentBanner
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView(viewModel: viewModel, onDisconnect: onDisconnect)
|
||||
}
|
||||
.sheet(isPresented: $showCollections) {
|
||||
CollectionsView(viewModel: viewModel)
|
||||
}
|
||||
.sheet(isPresented: $showAddBookmark) {
|
||||
AddBookmarkView(viewModel: viewModel)
|
||||
}
|
||||
.refreshable {
|
||||
await viewModel.load()
|
||||
}
|
||||
.task {
|
||||
if viewModel.bookmarks.isEmpty {
|
||||
await viewModel.load()
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: Binding(
|
||||
get: { viewModel.error != nil },
|
||||
set: { if !$0 { viewModel.error = nil } }
|
||||
)) {
|
||||
Button("OK") { viewModel.error = nil }
|
||||
} message: {
|
||||
Text(viewModel.error ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private var enrichmentBanner: some View {
|
||||
HStack(spacing: 10) {
|
||||
ProgressView(value: viewModel.enrichmentProgress)
|
||||
.progressViewStyle(.linear)
|
||||
.tint(.primary)
|
||||
Text("Adding AI summaries…")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 12)
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
private func maybeLoadMore(_ bookmark: Bookmark) {
|
||||
guard let last = viewModel.bookmarks.last, last.id == bookmark.id,
|
||||
viewModel.nextPageUrl != nil, !viewModel.isLoadingMore else { return }
|
||||
Task { await viewModel.loadMore() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user