134 lines
4.4 KiB
Swift
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()
|
|
}
|
|
}
|