| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- 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<URLResourceKey> = [.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()
- }
|