SettingsView.swift 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import SwiftUI
  2. // MARK: - SettingsView
  3. /// A compact settings hub. Detailed controls live behind a single function list.
  4. struct SettingsView: View {
  5. @ObservedObject private var authManager: AuthManager = .shared
  6. @State private var usedStorageBytes: Int64 = 0
  7. @State private var availableStorageBytes: Int64 = 0
  8. @AppStorage(DeveloperLogStore.enabledKey) private var isDeveloperModeEnabled = false
  9. var body: some View {
  10. NavigationStack {
  11. List {
  12. NavigationLink {
  13. ProfileView()
  14. } label: {
  15. settingsRow(
  16. icon: "person.crop.circle",
  17. title: "账号",
  18. detail: accountSummary
  19. )
  20. }
  21. NavigationLink {
  22. RecordingDevicesSettingsView()
  23. } label: {
  24. settingsRow(
  25. icon: "mic",
  26. title: "我的录音设备"
  27. )
  28. }
  29. NavigationLink {
  30. StorageSettingsView(
  31. usedStorageBytes: $usedStorageBytes,
  32. availableStorageBytes: $availableStorageBytes,
  33. reloadStorageMetrics: reloadStorageMetrics
  34. )
  35. } label: {
  36. settingsRow(
  37. icon: "internaldrive",
  38. title: "存储空间",
  39. detail: formattedBytes(usedStorageBytes)
  40. )
  41. }
  42. NavigationLink {
  43. AboutSettingsView()
  44. } label: {
  45. settingsRow(
  46. icon: "info.circle",
  47. title: "关于",
  48. detail: appVersion
  49. )
  50. }
  51. Section {
  52. Toggle(isOn: $isDeveloperModeEnabled) {
  53. settingsRow(
  54. icon: "hammer",
  55. title: "开发者模式"
  56. )
  57. }
  58. .tint(.celestiaCyan)
  59. }
  60. }
  61. .scrollContentBackground(.hidden)
  62. .background(Color.spaceBlack)
  63. .listStyle(.insetGrouped)
  64. .navigationTitle("设置")
  65. .navigationBarTitleDisplayMode(.inline)
  66. .task {
  67. reloadStorageMetrics()
  68. }
  69. }
  70. }
  71. private var accountSummary: String {
  72. authManager.currentUser?.username ?? "未登录"
  73. }
  74. private var appVersion: String {
  75. Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0"
  76. }
  77. private func settingsRow(icon: String, title: String, detail: String? = nil) -> some View {
  78. HStack(spacing: 14) {
  79. Image(systemName: icon)
  80. .font(.system(size: 17, weight: .regular))
  81. .foregroundStyle(Color.celestiaCyan)
  82. .frame(width: 24)
  83. Text(title)
  84. .foregroundStyle(Color.primary)
  85. Spacer()
  86. if let detail {
  87. Text(detail)
  88. .font(.system(size: 13))
  89. .foregroundStyle(Color.secondary)
  90. .lineLimit(1)
  91. }
  92. }
  93. .padding(.vertical, 5)
  94. }
  95. private func formattedBytes(_ bytes: Int64) -> String {
  96. ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)
  97. }
  98. private func reloadStorageMetrics() {
  99. let fileManager = FileManager.default
  100. let roots = [
  101. fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
  102. fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
  103. ].compactMap { $0 }
  104. usedStorageBytes = roots.reduce(0) { total, root in
  105. let keys: Set<URLResourceKey> = [.isRegularFileKey, .fileSizeKey]
  106. guard let enumerator = fileManager.enumerator(
  107. at: root,
  108. includingPropertiesForKeys: Array(keys)
  109. ) else {
  110. return total
  111. }
  112. var subtotal: Int64 = 0
  113. for case let url as URL in enumerator {
  114. guard let values = try? url.resourceValues(forKeys: keys),
  115. values.isRegularFile == true else {
  116. continue
  117. }
  118. subtotal += Int64(values.fileSize ?? 0)
  119. }
  120. return total + subtotal
  121. }
  122. let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first
  123. availableStorageBytes = Int64(
  124. (try? documents?.resourceValues(
  125. forKeys: [.volumeAvailableCapacityForImportantUsageKey]
  126. ).volumeAvailableCapacityForImportantUsage) ?? 0
  127. )
  128. }
  129. }
  130. // MARK: - Storage
  131. private struct StorageSettingsView: View {
  132. @Binding var usedStorageBytes: Int64
  133. @Binding var availableStorageBytes: Int64
  134. let reloadStorageMetrics: () -> Void
  135. @State private var showClearCacheAlert = false
  136. var body: some View {
  137. List {
  138. Section {
  139. metricRow(label: "App 数据", value: formattedBytes(usedStorageBytes))
  140. metricRow(label: "设备可用", value: formattedBytes(availableStorageBytes))
  141. }
  142. Section {
  143. Button("清理缓存", role: .destructive) {
  144. showClearCacheAlert = true
  145. }
  146. } footer: {
  147. Text("只清除系统缓存目录,不会删除录音、照片或会话记录。")
  148. }
  149. }
  150. .scrollContentBackground(.hidden)
  151. .background(Color.spaceBlack)
  152. .navigationTitle("存储空间")
  153. .navigationBarTitleDisplayMode(.inline)
  154. .alert("清理缓存", isPresented: $showClearCacheAlert) {
  155. Button("取消", role: .cancel) {}
  156. Button("清理", role: .destructive) {
  157. clearTemporaryCache()
  158. HapticManager.trigger(.tapFeedback)
  159. }
  160. } message: {
  161. Text("只清除系统缓存目录,不会删除录音、照片或会话记录。")
  162. }
  163. }
  164. private func metricRow(label: String, value: String) -> some View {
  165. HStack {
  166. Text(label)
  167. Spacer()
  168. Text(value)
  169. .foregroundStyle(Color.secondary)
  170. }
  171. }
  172. private func formattedBytes(_ bytes: Int64) -> String {
  173. ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)
  174. }
  175. private func clearTemporaryCache() {
  176. let fileManager = FileManager.default
  177. guard let cacheURL = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first,
  178. let children = try? fileManager.contentsOfDirectory(
  179. at: cacheURL,
  180. includingPropertiesForKeys: nil
  181. ) else {
  182. return
  183. }
  184. for child in children {
  185. try? fileManager.removeItem(at: child)
  186. }
  187. reloadStorageMetrics()
  188. }
  189. }
  190. // MARK: - Recording Devices
  191. private struct RecordingDevicesSettingsView: View {
  192. @ObservedObject private var authManager: AuthManager = .shared
  193. @ObservedObject private var bleManager: BLEManager = .shared
  194. @State private var showAuthModal = false
  195. @State private var showScanDeviceModal = false
  196. var body: some View {
  197. ScrollView {
  198. DeviceListView(
  199. bleManager: bleManager,
  200. authManager: authManager,
  201. onAddDeviceClicked: handleAddDevice
  202. )
  203. .padding(16)
  204. }
  205. .background(Color.spaceBlack)
  206. .navigationTitle("我的录音设备")
  207. .navigationBarTitleDisplayMode(.inline)
  208. .sheet(isPresented: $showAuthModal) {
  209. AuthModalView(authManager: authManager)
  210. }
  211. .sheet(isPresented: $showScanDeviceModal) {
  212. DeviceScanView(bleManager: bleManager, authManager: authManager)
  213. }
  214. }
  215. private func handleAddDevice() {
  216. if authManager.isLoggedIn {
  217. showScanDeviceModal = true
  218. } else {
  219. showAuthModal = true
  220. }
  221. }
  222. }
  223. // MARK: - About
  224. private struct AboutSettingsView: View {
  225. var body: some View {
  226. List {
  227. Section {
  228. infoRow(label: "应用名称", value: "星痕 CelestiaTrace")
  229. infoRow(label: "版本信息", value: appVersion)
  230. infoRow(label: "系统支持", value: "iOS 17.0+")
  231. }
  232. Section {
  233. guidanceRow(
  234. title: "自动锁定",
  235. detail: "系统设置 → 显示与亮度 → 自动锁定"
  236. )
  237. guidanceRow(
  238. title: "定位权限",
  239. detail: "系统设置 → 隐私与安全性 → 定位服务"
  240. )
  241. } header: {
  242. Text("后台运行保障")
  243. } footer: {
  244. Text("长时间现场记录前,请确认系统权限与电量充足。")
  245. }
  246. }
  247. .scrollContentBackground(.hidden)
  248. .background(Color.spaceBlack)
  249. .navigationTitle("关于")
  250. .navigationBarTitleDisplayMode(.inline)
  251. }
  252. private var appVersion: String {
  253. Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0"
  254. }
  255. private func infoRow(label: String, value: String) -> some View {
  256. HStack {
  257. Text(label)
  258. Spacer()
  259. Text(value)
  260. .foregroundStyle(Color.secondary)
  261. }
  262. }
  263. private func guidanceRow(title: String, detail: String) -> some View {
  264. VStack(alignment: .leading, spacing: 5) {
  265. Text(title)
  266. Text(detail)
  267. .font(.system(size: 12))
  268. .foregroundStyle(Color.secondary)
  269. }
  270. .padding(.vertical, 3)
  271. }
  272. }
  273. // MARK: - Preview
  274. #Preview {
  275. SettingsView()
  276. }