All checks were successful
CI / build-and-deploy (push) Successful in 14s
Swap hardcoded systemGray/Color.blue pill and button fills for the iOS 26 Liquid Glass APIs so controls read native under the new design system: - BookmarkRow tag chips: glassEffect in a GlassEffectContainer - Podcast speed button: glassEffect capsule - Podcast retry + Onboarding Connect: glassProminent button style Onboarding text-field fills left as systemGray6 (standard adaptive field fill; glass behind editable text hurts legibility). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
141 lines
5.1 KiB
Swift
141 lines
5.1 KiB
Swift
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)
|
|
}
|
|
.buttonStyle(.glassProminent)
|
|
.buttonBorderShape(.roundedRectangle(radius: 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 {
|
|
let raw = urlText.normalizedURL
|
|
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"
|
|
)
|
|
}
|
|
}
|