import SwiftUI // MARK: - SettingsView /// A compact settings hub. Detailed controls live behind a single function list. struct SettingsView: View { @ObservedObject private var authManager: AuthManager = .shared @State private var usedStorageBytes: Int64 = 0 @State private var availableStorageBytes: Int64 = 0 @AppStorage(DeveloperLogStore.enabledKey) private var isDeveloperModeEnabled = false var body: some View { NavigationStack { List { NavigationLink { ProfileView() } label: { settingsRow( icon: "person.crop.circle", title: "账号", detail: accountSummary ) } NavigationLink { RecordingDevicesSettingsView() } label: { settingsRow( icon: "mic", title: "我的录音设备" ) } NavigationLink { StorageSettingsView( usedStorageBytes: $usedStorageBytes, availableStorageBytes: $availableStorageBytes, reloadStorageMetrics: reloadStorageMetrics ) } label: { settingsRow( icon: "internaldrive", title: "存储空间", detail: formattedBytes(usedStorageBytes) ) } NavigationLink { AboutSettingsView() } label: { settingsRow( icon: "info.circle", title: "关于", detail: appVersion ) } Section { Toggle(isOn: $isDeveloperModeEnabled) { settingsRow( icon: "hammer", title: "开发者模式" ) } .tint(.celestiaCyan) } } .scrollContentBackground(.hidden) .background(Color.spaceBlack) .listStyle(.insetGrouped) .navigationTitle("设置") .navigationBarTitleDisplayMode(.inline) .task { reloadStorageMetrics() } } } private var accountSummary: String { authManager.currentUser?.username ?? "未登录" } private var appVersion: String { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0" } private func settingsRow(icon: String, title: String, detail: String? = nil) -> some View { HStack(spacing: 14) { Image(systemName: icon) .font(.system(size: 17, weight: .regular)) .foregroundStyle(Color.celestiaCyan) .frame(width: 24) Text(title) .foregroundStyle(Color.primary) Spacer() if let detail { Text(detail) .font(.system(size: 13)) .foregroundStyle(Color.secondary) .lineLimit(1) } } .padding(.vertical, 5) } private func formattedBytes(_ bytes: Int64) -> String { ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) } private func reloadStorageMetrics() { let fileManager = FileManager.default let roots = [ fileManager.urls(for: .documentDirectory, in: .userDomainMask).first, fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ].compactMap { $0 } usedStorageBytes = roots.reduce(0) { total, root in let keys: Set = [.isRegularFileKey, .fileSizeKey] guard let enumerator = fileManager.enumerator( at: root, includingPropertiesForKeys: Array(keys) ) else { return total } var subtotal: Int64 = 0 for case let url as URL in enumerator { guard let values = try? url.resourceValues(forKeys: keys), values.isRegularFile == true else { continue } subtotal += Int64(values.fileSize ?? 0) } return total + subtotal } let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first availableStorageBytes = Int64( (try? documents?.resourceValues( forKeys: [.volumeAvailableCapacityForImportantUsageKey] ).volumeAvailableCapacityForImportantUsage) ?? 0 ) } } // MARK: - Storage private struct StorageSettingsView: View { @Binding var usedStorageBytes: Int64 @Binding var availableStorageBytes: Int64 let reloadStorageMetrics: () -> Void @State private var showClearCacheAlert = false var body: some View { List { Section { metricRow(label: "App 数据", value: formattedBytes(usedStorageBytes)) metricRow(label: "设备可用", value: formattedBytes(availableStorageBytes)) } Section { Button("清理缓存", role: .destructive) { showClearCacheAlert = true } } footer: { Text("只清除系统缓存目录,不会删除录音、照片或会话记录。") } } .scrollContentBackground(.hidden) .background(Color.spaceBlack) .navigationTitle("存储空间") .navigationBarTitleDisplayMode(.inline) .alert("清理缓存", isPresented: $showClearCacheAlert) { Button("取消", role: .cancel) {} Button("清理", role: .destructive) { clearTemporaryCache() HapticManager.trigger(.tapFeedback) } } message: { Text("只清除系统缓存目录,不会删除录音、照片或会话记录。") } } private func metricRow(label: String, value: String) -> some View { HStack { Text(label) Spacer() Text(value) .foregroundStyle(Color.secondary) } } private func formattedBytes(_ bytes: Int64) -> String { ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) } private func clearTemporaryCache() { let fileManager = FileManager.default guard let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first, let children = try? fileManager.contentsOfDirectory( at: cacheURL, includingPropertiesForKeys: nil ) else { return } for child in children { try? fileManager.removeItem(at: child) } reloadStorageMetrics() } } // MARK: - Recording Devices private struct RecordingDevicesSettingsView: View { @ObservedObject private var authManager: AuthManager = .shared @ObservedObject private var bleManager: BLEManager = .shared @State private var showAuthModal = false @State private var showScanDeviceModal = false var body: some View { ScrollView { DeviceListView( bleManager: bleManager, authManager: authManager, onAddDeviceClicked: handleAddDevice ) .padding(16) } .background(Color.spaceBlack) .navigationTitle("我的录音设备") .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $showAuthModal) { AuthModalView(authManager: authManager) } .sheet(isPresented: $showScanDeviceModal) { DeviceScanView(bleManager: bleManager, authManager: authManager) } } private func handleAddDevice() { if authManager.isLoggedIn { showScanDeviceModal = true } else { showAuthModal = true } } } // MARK: - About private struct AboutSettingsView: View { var body: some View { List { Section { infoRow(label: "应用名称", value: "星痕 CelestiaTrace") infoRow(label: "版本信息", value: appVersion) infoRow(label: "系统支持", value: "iOS 17.0+") } Section { guidanceRow( title: "自动锁定", detail: "系统设置 → 显示与亮度 → 自动锁定" ) guidanceRow( title: "定位权限", detail: "系统设置 → 隐私与安全性 → 定位服务" ) } header: { Text("后台运行保障") } footer: { Text("长时间现场记录前,请确认系统权限与电量充足。") } } .scrollContentBackground(.hidden) .background(Color.spaceBlack) .navigationTitle("关于") .navigationBarTitleDisplayMode(.inline) } private var appVersion: String { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0" } private func infoRow(label: String, value: String) -> some View { HStack { Text(label) Spacer() Text(value) .foregroundStyle(Color.secondary) } } private func guidanceRow(title: String, detail: String) -> some View { VStack(alignment: .leading, spacing: 5) { Text(title) Text(detail) .font(.system(size: 12)) .foregroundStyle(Color.secondary) } .padding(.vertical, 3) } } // MARK: - Preview #Preview { SettingsView() }