154 lines
4.6 KiB
Swift
154 lines
4.6 KiB
Swift
import SwiftUI
|
|
#if os(iOS)
|
|
import SafariServices
|
|
#endif
|
|
|
|
struct SetupStep: Identifiable {
|
|
enum Status {
|
|
case done, active, todo
|
|
}
|
|
|
|
let name: String
|
|
var status: Status
|
|
var detail: String?
|
|
var signIn = false
|
|
var signInURL: URL?
|
|
|
|
var id: String {
|
|
name
|
|
}
|
|
}
|
|
|
|
func signInStep(_ name: String, hostname: String, state: String, authURL: URL?) -> SetupStep {
|
|
if state == "Running" {
|
|
return SetupStep(name: name, status: .done)
|
|
}
|
|
if state == "NeedsMachineAuth" {
|
|
return SetupStep(name: name, status: .active, detail: "\(hostname) is waiting for a tailnet admin to approve it")
|
|
}
|
|
if state == "NeedsLogin" {
|
|
return SetupStep(name: name, status: .active, detail: authURL == nil ? "Waiting for Tailscale's sign-in link…" : "\(hostname) is waiting for you", signIn: true, signInURL: authURL)
|
|
}
|
|
return SetupStep(name: name, status: .active, detail: "Waiting for Tailscale (\(state))…")
|
|
}
|
|
|
|
struct StepsCard: View {
|
|
let title: String
|
|
let detail: String
|
|
let steps: [SetupStep]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(title)
|
|
.font(Fonts.font(17, weight: 700))
|
|
Text(detail)
|
|
.foregroundStyle(Palette.muted)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.padding(.top, 4)
|
|
.padding(.bottom, 10)
|
|
ForEach(Array(steps.enumerated()), id: \.element.id) { i, step in
|
|
if i > 0 {
|
|
Rectangle().fill(Palette.border).frame(height: 1)
|
|
}
|
|
StepRow(step: step)
|
|
}
|
|
}
|
|
.padding(20)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 10))
|
|
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border))
|
|
.frame(maxWidth: 460)
|
|
.padding(16)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
#if os(macOS)
|
|
.onAppear {
|
|
Tailnet.shared.serviceWatchers += 1
|
|
}
|
|
.onDisappear {
|
|
Tailnet.shared.serviceWatchers -= 1
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private struct StepRow: View {
|
|
let step: SetupStep
|
|
@State private var signingIn = false
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 10) {
|
|
icon
|
|
.frame(width: 18, height: 18)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(step.name)
|
|
.foregroundStyle(step.status == .todo ? Palette.muted : Palette.text)
|
|
if let detail = step.detail {
|
|
Text(detail)
|
|
.font(Fonts.font(12))
|
|
.foregroundStyle(Palette.muted)
|
|
.textSelection(.enabled)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
if step.signIn {
|
|
Button("Sign in") {
|
|
#if os(macOS)
|
|
if let url = step.signInURL {
|
|
openExternal(url)
|
|
}
|
|
#else
|
|
signingIn = true
|
|
#endif
|
|
}
|
|
.buttonStyle(WebButtonStyle(primary: true))
|
|
.disabled(step.signInURL == nil)
|
|
.fixedSize()
|
|
}
|
|
}
|
|
.padding(.vertical, 8)
|
|
#if os(iOS)
|
|
.sheet(isPresented: $signingIn) {
|
|
if let url = step.signInURL {
|
|
SafariView(url: url)
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
.onChange(of: step.signIn) {
|
|
if !step.signIn {
|
|
signingIn = false
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var icon: some View {
|
|
switch step.status {
|
|
case .done:
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundStyle(Palette.ok)
|
|
case .active:
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
case .todo:
|
|
Image(systemName: "circle")
|
|
.font(.system(size: 8))
|
|
.foregroundStyle(Palette.muted)
|
|
}
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
private struct SafariView: UIViewControllerRepresentable {
|
|
let url: URL
|
|
|
|
func makeUIViewController(context: Context) -> SFSafariViewController {
|
|
SFSafariViewController(url: url)
|
|
}
|
|
|
|
func updateUIViewController(_ controller: SFSafariViewController, context: Context) {}
|
|
}
|
|
#endif
|