chahinebrini 50425a62ee fix(devices): Magic-Hub zeigt jetzt alle Native-Geraete, Native dedupliziert Mac
Magic-Mac-Hub (/api/magic/devices):
- Filter boundToPlan war zu eng \u2014 iPhone/iPad ohne aktiven Plan-Lock
  fielen raus. Jetzt: alle UserDevice-Rows des Users ausser den
  magic-enrolled, plus ProtectedDevice mit Dedupe.

Native /devices Page:
- MacBook erschien doppelt: einmal als UserDevice (registriert via
  Magic-Mac, model=Mac14,9) und einmal als ProtectedDevice (alter
  DNS-Flow). Dedupe per platform-key (mac/ios/android/win):
  wenn UserDevice mit gleicher Plattform existiert, blende
  ProtectedDevice aus.
- Slot-Counter zaehlt jetzt nach dedupe (totalRegistered).
2026-06-03 19:43:33 +02:00

192 lines
6.6 KiB
Swift

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)
}