Merge iOS and Mac app into one
This commit is contained in:
200
ABS Client/Audiobookshelf swift/Views/LoginView.swift
Normal file
200
ABS Client/Audiobookshelf swift/Views/LoginView.swift
Normal file
@@ -0,0 +1,200 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LoginView: View {
|
||||
@Environment(AppState.self) private var app
|
||||
|
||||
@State private var serverURL: String = ""
|
||||
@State private var username: String = ""
|
||||
@State private var password: String = ""
|
||||
@State private var remember: Bool = true
|
||||
@State private var isLoading: Bool = false
|
||||
|
||||
var body: some View {
|
||||
#if os(iOS)
|
||||
iOSBody
|
||||
#else
|
||||
macOSBody
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - iOS
|
||||
|
||||
#if os(iOS)
|
||||
private var iOSBody: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header with green gradient background
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [Color.accentColor.opacity(0.85), Color.accentColor.opacity(0.55)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.ignoresSafeArea(edges: .top)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: "books.vertical.fill")
|
||||
.font(.system(size: 52, weight: .regular))
|
||||
.foregroundStyle(.white)
|
||||
Text("ABS Client")
|
||||
.font(.title.bold())
|
||||
.foregroundStyle(.white)
|
||||
Text("Verbinde dich mit deinem Audiobookshelf-Server")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.white.opacity(0.85))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.top, 56)
|
||||
.padding(.bottom, 32)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
// Form fields — uses native iOS Form appearance
|
||||
Form {
|
||||
Section {
|
||||
TextField("https://abs.example.com", text: $serverURL)
|
||||
.textContentType(.URL)
|
||||
.keyboardType(.URL)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.submitLabel(.next)
|
||||
} header: {
|
||||
Text("Server-URL")
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField("Benutzername", text: $username)
|
||||
.textContentType(.username)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.submitLabel(.next)
|
||||
SecureField("Passwort", text: $password)
|
||||
.textContentType(.password)
|
||||
.submitLabel(.go)
|
||||
.onSubmit { if canLogin { doLogin() } }
|
||||
} header: {
|
||||
Text("Anmeldedaten")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Anmeldung merken", isOn: $remember)
|
||||
}
|
||||
|
||||
if let err = app.auth.errorMessage {
|
||||
Section {
|
||||
Label(err, systemImage: "exclamationmark.triangle.fill")
|
||||
.foregroundStyle(.red)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: doLogin) {
|
||||
HStack {
|
||||
Spacer()
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("Einloggen").bold()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(!canLogin)
|
||||
}
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
}
|
||||
|
||||
private var canLogin: Bool {
|
||||
!isLoading && !serverURL.isEmpty && !username.isEmpty && !password.isEmpty
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - macOS
|
||||
|
||||
#if os(macOS)
|
||||
private var macOSBody: some View {
|
||||
VStack(spacing: 16) {
|
||||
Spacer()
|
||||
Image(systemName: "books.vertical.fill")
|
||||
.font(.system(size: 48))
|
||||
.foregroundStyle(.tint)
|
||||
Text("ABS Client")
|
||||
.font(.largeTitle).bold()
|
||||
Text("Verbinde dich mit deinem Audiobookshelf-Server")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
labeledField(label: "Server-URL") {
|
||||
TextField("", text: $serverURL)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.disableAutocorrection(true)
|
||||
}
|
||||
labeledField(label: "Benutzername") {
|
||||
TextField("", text: $username)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.disableAutocorrection(true)
|
||||
}
|
||||
labeledField(label: "Passwort") {
|
||||
SecureField("", text: $password)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
Toggle("Anmeldung merken", isOn: $remember)
|
||||
.toggleStyle(.checkbox)
|
||||
}
|
||||
.frame(maxWidth: 380)
|
||||
|
||||
if let err = app.auth.errorMessage {
|
||||
Text(err)
|
||||
.foregroundStyle(.red)
|
||||
.font(.callout)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: 380)
|
||||
}
|
||||
|
||||
Button(action: doLogin) {
|
||||
if isLoading {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Text("Einloggen").frame(maxWidth: 200)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.large)
|
||||
.disabled(isLoading || serverURL.isEmpty || username.isEmpty || password.isEmpty)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(32)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func labeledField<C: View>(label: String, @ViewBuilder content: () -> C) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(label).font(.subheadline).foregroundStyle(.secondary)
|
||||
content()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Shared
|
||||
|
||||
private func doLogin() {
|
||||
isLoading = true
|
||||
Task {
|
||||
await app.auth.login(
|
||||
serverURL: serverURL,
|
||||
username: username,
|
||||
password: password,
|
||||
remember: remember
|
||||
)
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user