Files
MobileMusicAssistant/Mobile Music Assistant/ViewsLoginView.swift
T

165 lines
5.4 KiB
Swift

//
// LoginView.swift
// Mobile Music Assistant
//
// Created by Sven Hanold on 26.03.26.
//
import SwiftUI
struct LoginView: View {
@Environment(MAService.self) private var service
@State private var serverURL = "https://"
@State private var token = ""
@State private var showToken = false
@State private var isLoading = false
@State private var errorMessage: String?
@State private var showError = false
var body: some View {
NavigationStack {
Form {
// Server URL Section
Section {
TextField("Server URL", text: $serverURL)
.textContentType(.URL)
.keyboardType(.URL)
.autocapitalization(.none)
.autocorrectionDisabled()
} header: {
Text("Server")
} footer: {
Text("Enter your Music Assistant server URL (e.g., https://music.example.com)")
}
// Token Section
Section {
HStack {
Group {
if showToken {
TextField("Long-Lived Access Token", text: $token)
.textContentType(.password)
.autocapitalization(.none)
.autocorrectionDisabled()
} else {
SecureField("Long-Lived Access Token", text: $token)
.textContentType(.password)
}
}
Button {
showToken.toggle()
} label: {
Image(systemName: showToken ? "eye.slash.fill" : "eye.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
} header: {
Text("Authentication")
} footer: {
VStack(alignment: .leading, spacing: 8) {
Text("How to get a token:")
Text("1. Open Music Assistant in a browser")
Text("2. Go to Settings → Users")
Text("3. Create a new long-lived access token")
Text("4. Copy and paste the token here")
}
.font(.caption)
.foregroundStyle(.secondary)
}
// Connect Button
Section {
Button {
Task {
await login()
}
} label: {
if isLoading {
HStack {
Spacer()
ProgressView()
Text("Connecting...")
.padding(.leading, 8)
Spacer()
}
} else {
HStack {
Spacer()
Text("Connect")
.fontWeight(.semibold)
Spacer()
}
}
}
.disabled(isLoading || !isFormValid)
}
}
.navigationTitle("Music Assistant")
.alert("Connection Error", isPresented: $showError) {
Button("OK", role: .cancel) { }
} message: {
if let errorMessage {
Text(errorMessage)
}
}
}
.applyTheme()
}
// MARK: - Computed Properties
private var isFormValid: Bool {
!serverURL.isEmpty &&
serverURL.starts(with: "http") &&
!token.isEmpty
}
// MARK: - Actions
private func login() async {
guard let url = URL(string: serverURL) else {
showError(message: "Invalid server URL")
return
}
isLoading = true
errorMessage = nil
print("🔵 LoginView: Starting login with long-lived token")
print("🔵 LoginView: Server URL = \(url.absoluteString)")
print("🔵 LoginView: Token length = \(token.count)")
do {
// Save token to keychain
try service.authManager.saveToken(serverURL: url, token: token)
print("✅ LoginView: Token saved to keychain")
// Connect WebSocket with token
print("🔵 LoginView: Connecting WebSocket")
try await service.connect(serverURL: url, token: token)
print("✅ LoginView: Connected successfully")
isLoading = false
} catch {
print("❌ LoginView: Login failed - \(error)")
isLoading = false
showError(message: error.localizedDescription)
}
}
private func showError(message: String) {
errorMessage = message
showError = true
}
}
#Preview {
LoginView()
.environment(MAService())
}