All checks were successful
CI / Scraper / Lint (pull_request) Successful in 16s
CI / Scraper / Test (pull_request) Successful in 20s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Build (pull_request) Successful in 21s
CI / UI / Docker Push (pull_request) Has been skipped
iOS CI / Build (push) Successful in 7m18s
iOS CI / Build (pull_request) Successful in 5m22s
iOS CI / Test (push) Successful in 10m55s
iOS CI / Test (pull_request) Successful in 9m32s
- Remove dead structs: ReadingProgress, HomeData, PBList (Models.swift) - Remove unused APIClient members: fetchRaw, setSessionId, BrowseParams; simplify logout(); drop no-op CodingKeys from BrowseResponse - Remove absolutePrevChapter/absoluteNextChapter computed props (replaced by prevChapter/nextChapter); replace URLSession cover prefetch with Kingfisher - Drop scrollOffset state var and dead subtitle block in ChapterRow (BookDetailView) - Remove never-set chaptersLoading published prop (BookDetailViewModel) - Add unified ChipButton(.filled/.outlined) to CommonViews replacing three near-identical pill chip types - Replace FilterChipView+SortChip in LibraryView and FilterChip in BrowseView with ChipButton - Replace inline KFImage usage in HomeView with AsyncCoverImage - Fix deprecated .navigationBarHidden(true) → .toolbar(.hidden, for: .navigationBar) in AuthView
124 lines
4.5 KiB
Swift
124 lines
4.5 KiB
Swift
import SwiftUI
|
|
|
|
struct AuthView: View {
|
|
@EnvironmentObject var authStore: AuthStore
|
|
@State private var mode: Mode = .login
|
|
@State private var username: String = ""
|
|
@State private var password: String = ""
|
|
@State private var confirmPassword: String = ""
|
|
@FocusState private var focusedField: Field?
|
|
|
|
enum Mode { case login, register }
|
|
enum Field { case username, password, confirmPassword }
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 0) {
|
|
// Logo / header
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "books.vertical.fill")
|
|
.font(.system(size: 56))
|
|
.foregroundStyle(.amber)
|
|
Text("LibNovel")
|
|
.font(.largeTitle.bold())
|
|
}
|
|
.padding(.top, 60)
|
|
.padding(.bottom, 40)
|
|
|
|
// Tab switcher
|
|
Picker("Mode", selection: $mode) {
|
|
Text("Sign In").tag(Mode.login)
|
|
Text("Create Account").tag(Mode.register)
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.padding(.horizontal, 24)
|
|
.padding(.bottom, 32)
|
|
|
|
// Form
|
|
VStack(spacing: 16) {
|
|
TextField("Username", text: $username)
|
|
.textFieldStyle(.roundedBorder)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
.focused($focusedField, equals: .username)
|
|
.submitLabel(.next)
|
|
.onSubmit { focusedField = .password }
|
|
|
|
SecureField("Password", text: $password)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($focusedField, equals: .password)
|
|
.submitLabel(mode == .register ? .next : .go)
|
|
.onSubmit {
|
|
if mode == .register { focusedField = .confirmPassword }
|
|
else { submit() }
|
|
}
|
|
|
|
if mode == .register {
|
|
SecureField("Confirm Password", text: $confirmPassword)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($focusedField, equals: .confirmPassword)
|
|
.submitLabel(.go)
|
|
.onSubmit { submit() }
|
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.animation(.easeInOut(duration: 0.2), value: mode)
|
|
|
|
if let error = authStore.error {
|
|
Text(error)
|
|
.font(.footnote)
|
|
.foregroundStyle(.red)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, 8)
|
|
}
|
|
|
|
Button(action: submit) {
|
|
Group {
|
|
if authStore.isLoading {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.tint(.white)
|
|
} else {
|
|
Text(mode == .login ? "Sign In" : "Create Account")
|
|
.fontWeight(.semibold)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 50)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(.amber)
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, 24)
|
|
.disabled(authStore.isLoading || !formIsValid)
|
|
|
|
Spacer()
|
|
}
|
|
.toolbar(.hidden, for: .navigationBar)
|
|
}
|
|
.onChange(of: mode) { _, _ in
|
|
authStore.error = nil
|
|
confirmPassword = ""
|
|
}
|
|
}
|
|
|
|
private var formIsValid: Bool {
|
|
let base = !username.isEmpty && password.count >= 4
|
|
if mode == .register { return base && password == confirmPassword }
|
|
return base
|
|
}
|
|
|
|
private func submit() {
|
|
focusedField = nil
|
|
Task {
|
|
if mode == .login {
|
|
await authStore.login(username: username, password: password)
|
|
} else {
|
|
await authStore.register(username: username, password: password)
|
|
}
|
|
}
|
|
}
|
|
}
|