Files
wehub-resource-sync 869b84f27c
Build / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:29:30 +08:00

198 lines
6.8 KiB
Swift

import SwiftUI
/// A short looping illustration of what the user is about to do in the real
/// System Settings pane. Built entirely in SwiftUI — no GIFs, no video, no
/// external assets. The point is to make the FDA step feel concrete so the
/// user knows exactly which toggle to flip before they even open Settings.
///
/// Frames cycle every ~5 seconds: idle → row highlights → toggle flips →
/// green check appears → hold → reset.
struct FDADemoView: View {
enum Frame: Int, CaseIterable {
case idle
case highlight
case toggleOn
case granted
case hold
}
@State private var frame: Frame = .idle
@State private var isActive = true
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private var rows: [DemoRow] {
[
DemoRow(name: "Finder", systemImage: "macwindow", granted: true),
DemoRow(name: "PureMac", systemImage: "sparkles",
granted: frame == .toggleOn || frame == .granted || frame == .hold,
isPureMac: true),
DemoRow(name: "Terminal", systemImage: "terminal", granted: true),
]
}
var body: some View {
VStack(spacing: 14) {
settingsCard
HStack(spacing: 6) {
Image(systemName: "lock.shield")
.font(.system(size: 11, weight: .semibold))
Text("Privacy & Security → Full Disk Access")
.font(.system(size: 11.5, weight: .medium))
}
.foregroundStyle(.secondary)
}
.onAppear {
// Reduce Motion: freeze on the end state — the user sees the
// "toggled on, granted" frame as a static illustration instead
// of an infinite loop.
if reduceMotion {
frame = .hold
return
}
isActive = true
cycle()
}
.onDisappear { isActive = false }
}
// MARK: - Settings card
private var settingsCard: some View {
VStack(spacing: 0) {
// Faux titlebar
HStack(spacing: 6) {
Circle().fill(Color(red: 1.0, green: 0.36, blue: 0.34)).frame(width: 9, height: 9)
Circle().fill(Color(red: 1.0, green: 0.78, blue: 0.27)).frame(width: 9, height: 9)
Circle().fill(Color(red: 0.31, green: 0.78, blue: 0.36)).frame(width: 9, height: 9)
Spacer()
Text("Full Disk Access")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(.secondary)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.primary.opacity(0.04))
Divider()
VStack(spacing: 0) {
ForEach(Array(rows.enumerated()), id: \.offset) { idx, row in
rowView(row)
if idx != rows.count - 1 {
Divider().padding(.leading, 46)
}
}
}
}
.background(Color(nsColor: .controlBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.strokeBorder(Color.primary.opacity(0.10), lineWidth: 0.5)
)
.shadow(color: .black.opacity(0.08), radius: 12, y: 4)
}
private func rowView(_ row: DemoRow) -> some View {
let highlight = row.isPureMac && (frame == .highlight)
let succeeded = row.isPureMac && (frame == .granted || frame == .hold)
return HStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(row.isPureMac ? Tint.blue.opacity(0.16) : Color.primary.opacity(0.08))
Image(systemName: row.systemImage)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(row.isPureMac ? Tint.blue : .secondary)
}
.frame(width: 26, height: 26)
Text(row.name)
.font(.system(size: 13, weight: row.isPureMac ? .semibold : .regular))
Spacer()
if succeeded {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 14))
.foregroundStyle(Tint.green)
.transition(.scale.combined(with: .opacity))
}
ToggleSwitch(isOn: row.granted)
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(highlight ? Tint.blue.opacity(0.10) : .clear)
.padding(.horizontal, 6)
.padding(.vertical, 2)
)
.animation(.easeInOut(duration: 0.35), value: highlight)
.animation(.easeInOut(duration: 0.35), value: succeeded)
}
// MARK: - Cycle
private func cycle() {
guard isActive, !reduceMotion else { return }
let schedule: [(Frame, Double)] = [
(.idle, 0.6),
(.highlight, 0.9),
(.toggleOn, 0.8),
(.granted, 0.4),
(.hold, 1.6),
]
var accumulated: Double = 0
for (target, hold) in schedule {
DispatchQueue.main.asyncAfter(deadline: .now() + accumulated) {
guard isActive else { return }
withAnimation(.easeInOut(duration: 0.4)) {
frame = target
}
}
accumulated += hold
}
// Re-arm. The isActive guard at the top of cycle() is what stops the
// chain when the view disappears.
DispatchQueue.main.asyncAfter(deadline: .now() + accumulated + 0.4) {
guard isActive else { return }
withAnimation(.easeInOut(duration: 0.3)) {
frame = .idle
}
cycle()
}
}
private struct DemoRow {
let name: String
let systemImage: String
let granted: Bool
var isPureMac: Bool = false
}
}
/// Tiny stand-in for an OS-style toggle. NSSwitch via SwiftUI would also work
/// but rendering one ourselves gives us full control over the animation.
private struct ToggleSwitch: View {
let isOn: Bool
var body: some View {
ZStack(alignment: isOn ? .trailing : .leading) {
Capsule()
.fill(isOn ? Tint.green : Color.secondary.opacity(0.30))
Circle()
.fill(Color.white)
.shadow(color: .black.opacity(0.15), radius: 1.5, y: 0.5)
.padding(2)
}
.frame(width: 32, height: 19)
.animation(.spring(response: 0.35, dampingFraction: 0.78), value: isOn)
}
}