import SwiftUI struct LoginView: View { @EnvironmentObject var serverManager: ServerManager @State private var showingAddServer = false @State private var editingServer: ServerConfig? @State private var isConnecting = false @State private var errorMessage: String? // iOS 8 Music app accent private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) var body: some View { NavigationStack { ZStack { // Gradient background LinearGradient( gradient: Gradient(colors: [ Color(white: 0.12), Color(white: 0.06) ]), startPoint: .top, endPoint: .bottom ) .ignoresSafeArea() VStack(spacing: 0) { // Header VStack(spacing: 12) { Image(systemName: "music.note.house.fill") .font(.system(size: 56)) .foregroundColor(accentPink) Text("Navidrome") .font(.system(size: 34, weight: .bold)) .foregroundColor(.white) Text("Connect to your music server") .font(.system(size: 15)) .foregroundColor(.gray) } .padding(.top, 60) .padding(.bottom, 40) // Server list if !serverManager.servers.isEmpty { VStack(spacing: 1) { ForEach(serverManager.servers) { server in ServerRow( server: server, isActive: serverManager.activeServer?.id == server.id, isConnecting: isConnecting && serverManager.activeServer?.id == server.id, onConnect: { connectTo(server) }, onEdit: { editingServer = server }, onDelete: { delete(server) } ) } } .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal, 20) } // Error message if let error = errorMessage { Text(error) .font(.system(size: 13)) .foregroundColor(.red) .padding(.top, 12) .padding(.horizontal, 20) } Spacer() // Add Server Button Button(action: { showingAddServer = true }) { HStack { Image(systemName: "plus.circle.fill") Text("Add Server") .fontWeight(.semibold) } .frame(maxWidth: .infinity) .padding(.vertical, 14) .background(accentPink) .foregroundColor(.white) .cornerRadius(12) } .padding(.horizontal, 20) .padding(.bottom, 40) } } .navigationBarHidden(true) .sheet(isPresented: $showingAddServer) { AddServerSheet(isPresented: $showingAddServer) } .sheet(item: $editingServer) { server in AddServerSheet(isPresented: Binding( get: { editingServer != nil }, set: { if !$0 { editingServer = nil } } ), existingServer: server) } } } private func connectTo(_ server: ServerConfig) { isConnecting = true errorMessage = nil Task { let success = await serverManager.connect(to: server) await MainActor.run { isConnecting = false if !success { if case .error(let msg) = serverManager.connectionState { errorMessage = msg } else { errorMessage = "Connection failed" } } } } } private func delete(_ server: ServerConfig) { serverManager.removeServer(server) } } // MARK: - Server Row struct ServerRow: View { let server: ServerConfig let isActive: Bool let isConnecting: Bool let onConnect: () -> Void let onEdit: () -> Void let onDelete: () -> Void private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) var body: some View { Button(action: onConnect) { HStack(spacing: 14) { // Server icon ZStack { RoundedRectangle(cornerRadius: 10) .fill( isActive ? accentPink.opacity(0.2) : Color.white.opacity(0.08) ) .frame(width: 44, height: 44) Image(systemName: "server.rack") .font(.system(size: 18)) .foregroundColor(isActive ? accentPink : .gray) } VStack(alignment: .leading, spacing: 3) { Text(server.name) .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) Text(server.url) .font(.system(size: 12)) .foregroundColor(.gray) .lineLimit(1) Text(server.username) .font(.system(size: 11)) .foregroundColor(.gray.opacity(0.7)) } Spacer() if isConnecting { ProgressView() .tint(accentPink) } else if isActive { Image(systemName: "checkmark.circle.fill") .foregroundColor(accentPink) } } .padding(.horizontal, 16) .padding(.vertical, 14) .background(Color(white: 0.15)) } .contextMenu { Button("Edit", action: onEdit) Button("Delete", role: .destructive, action: onDelete) } } } // MARK: - Add/Edit Server Sheet struct AddServerSheet: View { @EnvironmentObject var serverManager: ServerManager @Binding var isPresented: Bool var existingServer: ServerConfig? @State private var name: String = "" @State private var url: String = "" @State private var username: String = "" @State private var password: String = "" @State private var isTesting = false @State private var testResult: String? @State private var testSuccess = false private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) var body: some View { NavigationStack { ZStack { Color(white: 0.1).ignoresSafeArea() ScrollView { VStack(spacing: 24) { // Server Name VStack(alignment: .leading, spacing: 8) { Text("SERVER NAME") .font(.system(size: 12, weight: .medium)) .foregroundColor(.gray) TextField("My Server", text: $name) .textFieldStyle(DarkFieldStyle()) } // URL VStack(alignment: .leading, spacing: 8) { Text("SERVER URL") .font(.system(size: 12, weight: .medium)) .foregroundColor(.gray) TextField("https://music.example.com", text: $url) .textFieldStyle(DarkFieldStyle()) .keyboardType(.URL) .autocapitalization(.none) .disableAutocorrection(true) } // Username VStack(alignment: .leading, spacing: 8) { Text("USERNAME") .font(.system(size: 12, weight: .medium)) .foregroundColor(.gray) TextField("username", text: $username) .textFieldStyle(DarkFieldStyle()) .autocapitalization(.none) .disableAutocorrection(true) } // Password VStack(alignment: .leading, spacing: 8) { Text("PASSWORD") .font(.system(size: 12, weight: .medium)) .foregroundColor(.gray) SecureField("password", text: $password) .textFieldStyle(DarkFieldStyle()) } // Test Connection Button(action: testConnection) { HStack { if isTesting { ProgressView() .tint(.white) .scaleEffect(0.8) } else { Image(systemName: "antenna.radiowaves.left.and.right") } Text("Test Connection") .fontWeight(.medium) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(Color.white.opacity(0.15)) .foregroundColor(.white) .cornerRadius(10) } .disabled(url.isEmpty || username.isEmpty) if let result = testResult { HStack(spacing: 6) { Image(systemName: testSuccess ? "checkmark.circle.fill" : "xmark.circle.fill") Text(result) } .font(.system(size: 13)) .foregroundColor(testSuccess ? .green : .red) } // Tip about multiple servers HStack(spacing: 8) { Image(systemName: "info.circle") .foregroundColor(.gray) Text("You can add multiple servers. For example, a public and private URL to the same Navidrome instance.") .font(.system(size: 12)) .foregroundColor(.gray) } .padding(12) .background(Color.white.opacity(0.05)) .cornerRadius(8) } .padding(20) } } .navigationTitle(existingServer == nil ? "Add Server" : "Edit Server") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { isPresented = false } .foregroundColor(accentPink) } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { save() } .fontWeight(.semibold) .foregroundColor(accentPink) .disabled(name.isEmpty || url.isEmpty || username.isEmpty) } } .onAppear { if let s = existingServer { name = s.name url = s.url username = s.username password = s.password } } } } private func testConnection() { isTesting = true testResult = nil let testServer = ServerConfig( name: name, url: url, username: username, password: password ) Task { do { let success = try await serverManager.client.ping(server: testServer) await MainActor.run { isTesting = false testSuccess = success testResult = success ? "Connection successful!" : "Server returned error" } } catch { await MainActor.run { isTesting = false testSuccess = false testResult = error.localizedDescription } } } } private func save() { if var existing = existingServer { existing.name = name existing.url = url existing.username = username existing.password = password serverManager.updateServer(existing) } else { let server = ServerConfig( name: name, url: url, username: username, password: password ) serverManager.addServer(server) } isPresented = false } } // MARK: - Dark Text Field Style struct DarkFieldStyle: TextFieldStyle { func _body(configuration: TextField) -> some View { configuration .padding(12) .background(Color.white.opacity(0.08)) .cornerRadius(10) .foregroundColor(.white) } } // MARK: - Preview #if DEBUG struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() .environmentObject(ServerManager.shared) .preferredColorScheme(.dark) } } #endif