import AppKit import SwiftUI struct LoginView: View { @State private var digits: [String] = Array(repeating: "", count: 6) @FocusState private var focusedField: Int? @State private var isLoading = false @State private var errorMessage: String? let onSuccess: (AuthSession) -> Void private var enteredCode: String { digits.joined() } private var isComplete: Bool { enteredCode.count == 6 && enteredCode.allSatisfy(\.isNumber) } var body: some View { VStack(spacing: 28) { Spacer().frame(height: 20) // App-Icon + Header VStack(spacing: 14) { appIconView .frame(width: 84, height: 84) Text("Rebreak Magic") .font(.title.bold()) VStack(spacing: 4) { Text("Mit der Rebreak-App verbinden") .font(.subheadline) .foregroundStyle(.secondary) Text("Öffne in der App: Einstellungen → Rebreak Magic") .font(.caption) .foregroundStyle(.tertiary) } } // 6-stelliger Code-Input VStack(spacing: 14) { HStack(spacing: 10) { ForEach(0..<6, id: \.self) { index in digitField(index: index) } } if let error = errorMessage { HStack(spacing: 6) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.red) Text(error) .font(.caption) .foregroundStyle(.red) } } Button(action: handleSubmit) { HStack(spacing: 8) { if isLoading { ProgressView() .controlSize(.small) .tint(.white) } Text(isLoading ? "Verbinde…" : "Verbinden") .fontWeight(.medium) } .frame(maxWidth: .infinity) .padding(.vertical, 6) } .buttonStyle(.borderedProminent) .keyboardShortcut(.return, modifiers: []) .disabled(!isComplete || isLoading) } .frame(maxWidth: 380) Spacer() Text("Noch keine Rebreak-App? Lade sie im App Store / Play Store.") .font(.caption2) .foregroundStyle(.tertiary) .padding(.bottom, 18) } .padding(.horizontal, 32) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(nsColor: .windowBackgroundColor)) .onAppear { focusedField = 0 } } // MARK: - Components @ViewBuilder private var appIconView: some View { if let icon = NSApplication.shared.applicationIconImage, icon.size.width > 2 { Image(nsImage: icon) .resizable() .interpolation(.high) .frame(width: 84, height: 84) } else { // Fallback: gefärbtes RoundedRect (entspricht macOS-Stil) RoundedRectangle(cornerRadius: 18, style: .continuous) .fill(LinearGradient(colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing)) .overlay( Image(systemName: "wand.and.stars") .font(.system(size: 40, weight: .semibold)) .foregroundStyle(.white) ) } } @ViewBuilder private func digitField(index: Int) -> some View { TextField("", text: Binding( get: { digits[index] }, set: { newValue in handleDigitInput(newValue, at: index) } )) .textFieldStyle(.plain) .multilineTextAlignment(.center) .font(.system(size: 28, weight: .semibold, design: .rounded)) .frame(width: 48, height: 60) .background( RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color(nsColor: .controlBackgroundColor)) ) .overlay( RoundedRectangle(cornerRadius: 10, style: .continuous) .strokeBorder( focusedField == index ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: focusedField == index ? 2 : 1 ) ) .focused($focusedField, equals: index) } // MARK: - Logic private func handleDigitInput(_ raw: String, at index: Int) { // Erlaubt: 0–9. Mehrere Zeichen (Paste) → über alle Felder verteilen. let onlyDigits = raw.filter(\.isNumber) if onlyDigits.count > 1 { // Paste / Auto-Fill: über Felder ab `index` verteilen let chars = Array(onlyDigits.prefix(6 - index)) for (offset, ch) in chars.enumerated() { let target = index + offset if target < 6 { digits[target] = String(ch) } } let nextFocus = min(index + chars.count, 5) focusedField = nextFocus if digits.allSatisfy({ !$0.isEmpty }) && !isLoading { handleSubmit() } return } if onlyDigits.isEmpty { // Backspace digits[index] = "" if index > 0 { focusedField = index - 1 } return } digits[index] = String(onlyDigits.prefix(1)) if index < 5 { focusedField = index + 1 } else if isComplete && !isLoading { // Letztes Feld gefüllt → automatisch absenden handleSubmit() } } private func handleSubmit() { guard isComplete, !isLoading else { return } let code = enteredCode Task { isLoading = true errorMessage = nil do { let session = try await AuthService.shared.signInWithPairingCode(code) onSuccess(session) } catch { errorMessage = error.localizedDescription // Felder leeren bei Fehler digits = Array(repeating: "", count: 6) focusedField = 0 } isLoading = false } } } #Preview { LoginView { session in print("Magic-Session: \(session.sessionId)") } .frame(width: 720, height: 600) }