import AppKit import SwiftUI struct LoginView: View { @State private var code: String = "" @FocusState private var isFocused: Bool @State private var isLoading = false @State private var errorMessage: String? let onSuccess: (AuthSession) -> Void private var isComplete: Bool { code.count == 6 && code.allSatisfy(\.isNumber) } var body: some View { VStack(spacing: 28) { Spacer().frame(height: 20) 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) } } // ─── Code-Input ───────────────────────────────────────────── // EIN unsichtbares TextField empfängt alle Tasten. Die 6 Boxen // sind reine Anzeige → kein Focus-Race, kein System-Focus-Ring. VStack(spacing: 14) { ZStack { HStack(spacing: 10) { ForEach(0..<6, id: \.self) { index in digitBox(index: index) } } TextField("", text: $code) .textFieldStyle(.plain) .focused($isFocused) .focusEffectDisabled() .frame(width: 380, height: 60) .opacity(0.01) .onChange(of: code) { _, newValue in handleCodeChange(newValue) } .onSubmit { handleSubmit() } } .contentShape(Rectangle()) .onTapGesture { isFocused = true } 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 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { isFocused = true } } } // 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 { 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 digitBox(index: Int) -> some View { let chars = Array(code) let digit: String = index < chars.count ? String(chars[index]) : "" let activeIndex = min(chars.count, 5) let isActive = isFocused && index == activeIndex let isFilled = !digit.isEmpty RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color(nsColor: .controlBackgroundColor)) .frame(width: 48, height: 60) .overlay( RoundedRectangle(cornerRadius: 10, style: .continuous) .strokeBorder( isActive ? Color.accentColor : (isFilled ? Color.gray.opacity(0.4) : Color.gray.opacity(0.25)), lineWidth: isActive ? 2 : 1 ) ) .overlay( Text(digit) .font(.system(size: 28, weight: .semibold, design: .rounded)) .foregroundStyle(.primary) ) } // MARK: - Logic private func handleCodeChange(_ newValue: String) { let digits = newValue.filter(\.isNumber) let clipped = String(digits.prefix(6)) if clipped != newValue { code = clipped return } if clipped.count == 6 && !isLoading { handleSubmit() } } private func handleSubmit() { guard isComplete, !isLoading else { return } let toSend = code Task { isLoading = true errorMessage = nil do { let session = try await AuthService.shared.signInWithPairingCode(toSend) onSuccess(session) } catch { errorMessage = error.localizedDescription code = "" isFocused = true } isLoading = false } } } #Preview { LoginView { session in print("Magic-Session: \(session.sessionId)") } .frame(width: 720, height: 600) }