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,144 @@
import SwiftUI
struct OnboardingView: View {
let onConnect: (ServerConfig) -> Void
@State private var urlText = ""
@State private var token = ""
@State private var isConnecting = false
@State private var errorMessage: String?
@FocusState private var focused: Field?
enum Field { case url, token }
var body: some View {
VStack(spacing: 0) {
Spacer()
VStack(alignment: .leading, spacing: 8) {
Text("Marks")
.font(.system(size: 42, weight: .semibold, design: .rounded))
Text("Your bookmarks, beautifully.")
.font(.system(size: 17))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 28)
.padding(.bottom, 48)
VStack(spacing: 14) {
VStack(alignment: .leading, spacing: 6) {
Text("Server URL")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
.padding(.horizontal, 2)
TextField("https://links.example.com", text: $urlText)
.textContentType(.URL)
.keyboardType(.URL)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.focused($focused, equals: .url)
.submitLabel(.next)
.onSubmit { focused = .token }
.padding(14)
.background(Color(.systemGray6))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
VStack(alignment: .leading, spacing: 6) {
Text("API Token")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.secondary)
.padding(.horizontal, 2)
SecureField("Paste your token", text: $token)
.textContentType(.password)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.focused($focused, equals: .token)
.submitLabel(.done)
.onSubmit { Task { await connect() } }
.padding(14)
.background(Color(.systemGray6))
.clipShape(RoundedRectangle(cornerRadius: 12))
Text("Find your token at Settings → API Token in Linkding.")
.font(.system(size: 12))
.foregroundStyle(.tertiary)
.padding(.horizontal, 2)
}
}
.padding(.horizontal, 24)
if let err = errorMessage {
Text(err)
.font(.system(size: 14))
.foregroundStyle(.red)
.padding(.top, 12)
.padding(.horizontal, 28)
}
Button {
Task { await connect() }
} label: {
Group {
if isConnecting {
ProgressView().tint(.white)
} else {
Text("Connect")
.font(.system(size: 17, weight: .semibold))
}
}
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(canConnect ? Color.primary : Color(.systemGray4))
.foregroundStyle(canConnect ? Color(UIColor.systemBackground) : Color(.systemGray2))
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.disabled(!canConnect || isConnecting)
.padding(.horizontal, 24)
.padding(.top, 24)
Spacer()
}
}
private var canConnect: Bool { !urlText.isEmpty && !token.isEmpty }
private func connect() async {
focused = nil
isConnecting = true
errorMessage = nil
do {
let config = try parseConfig()
let api = LinkdingAPI(config: config)
try await api.verifyConnection()
onConnect(config)
} catch APIError.badStatus(401) {
errorMessage = "Invalid token. Check your API token in Linkding settings."
} catch APIError.badStatus(let code) {
errorMessage = "Server returned \(code). Check the URL."
} catch {
errorMessage = "Could not connect. Check the URL and try again."
}
isConnecting = false
}
private func parseConfig() throws -> ServerConfig {
var raw = urlText.trimmingCharacters(in: .whitespacesAndNewlines)
if !raw.hasPrefix("http://") && !raw.hasPrefix("https://") {
raw = "https://" + raw
}
guard let comps = URLComponents(string: raw),
let host = comps.host, !host.isEmpty else {
throw URLError(.badURL)
}
return ServerConfig(
host: host,
port: comps.port,
path: comps.path.hasSuffix("/") ? String(comps.path.dropLast()) : comps.path,
token: token.trimmingCharacters(in: .whitespacesAndNewlines),
useHttps: comps.scheme == "https"
)
}
}