import SwiftUI struct PrivacyConsentView: View { @ObservedObject var session: RemoteSessionModel @State private var hasReadPolicy = false var body: some View { GeometryReader { geometry in ZStack { Color.black.opacity(0.32).ignoresSafeArea() VStack(spacing: 16) { Text("隐私政策") .font(.title3.bold()) Text("请阅读以下隐私政策。同意后将处理连接所需的设备与网络信息;画面预览保存需另行开启。") .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) ScrollView { PrivacyPolicyDocument() .padding(14) } .frame(height: min(220, max(100, geometry.size.height - 330))) .background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 12)) .overlay { RoundedRectangle(cornerRadius: 12) .stroke(Color(.separator).opacity(0.3), lineWidth: 0.5) } Toggle(isOn: $hasReadPolicy) { Text("我已阅读并同意《隐私政策》") .font(.footnote) } .toggleStyle(ConsentCheckboxStyle()) HStack(spacing: 12) { Button { session.declineNetworkConsent() } label: { Text("拒绝") .frame(maxWidth: .infinity, minHeight: 44) .background(Color(.secondarySystemGroupedBackground), in: RoundedRectangle(cornerRadius: 10)) .contentShape(RoundedRectangle(cornerRadius: 10)) } Button { guard hasReadPolicy else { return } session.acceptNetworkConsent() } label: { Text("同意并继续") .frame(maxWidth: .infinity, minHeight: 44) .foregroundStyle(.white) .background(hasReadPolicy ? Color.accentColor : Color.gray.opacity(0.4), in: RoundedRectangle(cornerRadius: 10)) .contentShape(RoundedRectangle(cornerRadius: 10)) } .disabled(!hasReadPolicy || !PrivacyPolicyDocument.isAvailable) } .font(.subheadline.weight(.semibold)) .buttonStyle(.plain) Text("拒绝不影响本地功能;同意后可在设置中撤回。") .font(.caption2) .foregroundStyle(.secondary) } .padding(20) .frame(width: min(360, geometry.size.width - 40)) .background(Color(.systemBackground), in: RoundedRectangle(cornerRadius: 22)) .compositingGroup() .shadow(color: .black.opacity(0.16), radius: 24, y: 8) .accessibilityElement(children: .contain) .accessibilityAddTraits(.isModal) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } } private struct ConsentCheckboxStyle: ToggleStyle { func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { HStack(alignment: .center, spacing: 9) { Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square") .font(.title3) .foregroundStyle(configuration.isOn ? Color.accentColor : .secondary) configuration.label .foregroundStyle(.primary) .multilineTextAlignment(.leading) } .frame(maxWidth: .infinity, minHeight: 36, alignment: .leading) .contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityLabel("我已阅读并同意隐私政策") .accessibilityValue(configuration.isOn ? "已勾选" : "未勾选") } } struct PrivacySettingsSection: View { @ObservedObject var session: RemoteSessionModel @State private var confirmThumbnails = false @State private var confirmClear = false var body: some View { Section { NavigationLink { PrivacyAuthorizationView(session: session) } label: { HStack { Text("隐私与授权") Spacer() Text(session.hasNetworkConsent ? "已授权" : "未授权") .foregroundStyle(.secondary) } } Toggle("保存远程画面预览", isOn: Binding( get: { session.savesConnectionThumbnails }, set: { enabled in if enabled { confirmThumbnails = true } else { session.setSavesConnectionThumbnails(false) } } )) .disabled(!session.hasNetworkConsent || session.thumbnailCleanupInProgress) Button(session.thumbnailCleanupInProgress ? "正在清除…" : "清除预览图", role: .destructive) { confirmClear = true } .disabled(session.thumbnailCleanupInProgress) if let error = session.thumbnailCleanupError { Text(error).font(.footnote).foregroundStyle(.secondary) } } header: { Text("隐私") } footer: { Text("画面预览默认关闭。开启后,从下一次连接起保存一张远程画面到本机,显示在最近连接中,不上传且不纳入设备备份。关闭开关会清除已有预览;连接记录和接收的文件不受影响。") } .alert("保存远程画面预览?", isPresented: $confirmThumbnails) { Button("开启保存") { session.setSavesConnectionThumbnails(true) } Button("取消", role: .cancel) { } } message: { Text("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。") } .alert("清除预览图?", isPresented: $confirmClear) { Button("清除", role: .destructive) { session.clearConnectionThumbnails() } Button("取消", role: .cancel) { } } message: { Text(session.savesConnectionThumbnails ? "将删除本机保存的所有远程预览图,保留连接记录、密码和接收的文件。后续连接仍会保存新的预览图。" : "将删除本机保存的所有远程预览图,保留连接记录、密码和接收的文件。") } } } private struct PrivacyAuthorizationView: View { @ObservedObject var session: RemoteSessionModel @State private var confirmWithdrawal = false var body: some View { Form { Section { LabeledContent("授权状态", value: session.hasNetworkConsent ? "已授权" : "未授权") NavigationLink("隐私政策") { PrivacyPolicyView() } } footer: { if !session.hasNetworkConsent { Text("授权后可使用远程连接。") } } Section { if session.hasNetworkConsent { Button("撤回隐私授权", role: .destructive) { confirmWithdrawal = true } } else { Button("阅读并授权") { session.showPrivacyNotice() } } } } .navigationTitle("隐私与授权") .navigationBarTitleDisplayMode(.inline) .alert("是否撤回隐私授权?", isPresented: $confirmWithdrawal) { Button("撤回授权", role: .destructive) { session.revokeNetworkConsent() } Button("取消", role: .cancel) { } } message: { Text("将停止远程连接、关闭画面预览保存并清除已有预览。连接记录和接收文件会保留,已有服务端数据不会删除。您可随时重新授权。") } } } /// Bundled policy is readable before any network consent or external request. private struct PrivacyPolicyDocument: View { private static let text: String? = { guard let url = Bundle.main.url(forResource: "PRIVACY", withExtension: "md"), let text = try? String(contentsOf: url, encoding: .utf8), !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } // The app's interface is Chinese; show the complete Chinese translation. if let start = text.range(of: "## 中文\n"), let end = text.range(of: "## English", range: start.upperBound..