Files
openless/Sources/OpenLessApp/Onboarding/OnboardingView.swift
T

134 lines
4.4 KiB
Swift

import SwiftUI
import OpenLessHotkey
import OpenLessRecorder
struct OnboardingView: View {
let onComplete: () -> Void
@State private var accessibilityGranted = AccessibilityPermission.isGranted()
@State private var microphoneGranted = MicrophonePermission.isGranted()
@State private var pollingTimer: Timer?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
VStack(alignment: .leading, spacing: 6) {
Text("欢迎使用 OpenLess")
.font(.system(size: 22, weight: .semibold))
Text("还差两个权限就可以开始用语音输入。")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
permissionRow(
index: 1,
title: "辅助功能",
detail: "用于全局快捷键和把整理后的文字写回当前输入框。",
granted: accessibilityGranted,
action: requestAccessibility
)
permissionRow(
index: 2,
title: "麦克风",
detail: "用于录音转写。音频默认不保存。",
granted: microphoneGranted,
action: requestMicrophone
)
Spacer()
HStack {
Spacer()
if accessibilityGranted && microphoneGranted {
Button(action: complete) {
Text("完成并重启 OpenLess")
.frame(minWidth: 180)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
} else {
Text("两项都打勾后再继续")
.font(.system(size: 12))
.foregroundStyle(.secondary)
}
}
}
.padding(28)
.frame(width: 480, height: 360)
.onAppear { startPolling() }
.onDisappear { stopPolling() }
}
@ViewBuilder
private func permissionRow(
index: Int,
title: String,
detail: String,
granted: Bool,
action: @escaping () -> Void
) -> some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: granted ? "checkmark.circle.fill" : "\(index).circle")
.font(.system(size: 20))
.foregroundStyle(granted ? Color.green : Color.accentColor)
.frame(width: 24)
VStack(alignment: .leading, spacing: 4) {
Text(title).font(.system(size: 14, weight: .semibold))
Text(detail)
.font(.system(size: 12))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Button(action: action) {
Text(granted ? "已授权" : "授权")
.frame(minWidth: 64)
}
.controlSize(.regular)
.disabled(granted)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.primary.opacity(0.04))
)
}
// MARK: - 动作
private func requestAccessibility() {
// AXIsProcessTrustedWithOptions 首次会开「系统设置 → 隐私与安全 → 辅助功能」
AccessibilityPermission.request()
}
private func requestMicrophone() {
Task {
let granted = await MicrophonePermission.request()
await MainActor.run { microphoneGranted = granted }
}
}
/// 开「系统设置 → 辅助功能」需要用户手动勾,没法回调;这里轮询 1s 一次刷新状态。
private func startPolling() {
pollingTimer?.invalidate()
pollingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
let ax = AccessibilityPermission.isGranted()
let mic = MicrophonePermission.isGranted()
DispatchQueue.main.async {
if ax != accessibilityGranted { accessibilityGranted = ax }
if mic != microphoneGranted { microphoneGranted = mic }
}
}
}
private func stopPolling() {
pollingTimer?.invalidate()
pollingTimer = nil
}
private func complete() {
stopPolling()
onComplete()
}
}