SettingsView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import SwiftUI
  2. // MARK: - SettingsView
  3. /// The settings screen with storage info, debug toggles,
  4. /// background keep-alive guide, and app branding.
  5. /// Redesigned to use a minimalist wireframe business style.
  6. struct SettingsView: View {
  7. @State private var showClearCacheAlert = false
  8. // Mock storage values
  9. private let usedStorage: Double = 1.2
  10. private let totalStorage: Double = 5.0
  11. var body: some View {
  12. NavigationStack {
  13. ZStack {
  14. Color.spaceBlack.ignoresSafeArea()
  15. ScrollView {
  16. VStack(spacing: 20) {
  17. // Section: Storage
  18. storageSection
  19. .padding(.top, 12)
  20. // Section: Debug
  21. debugSection
  22. // Section: Keep-Alive Guide
  23. keepAliveSection
  24. // Section: About
  25. aboutSection
  26. // Footer
  27. footer
  28. .padding(.top, 12)
  29. .padding(.bottom, 40)
  30. }
  31. .padding(.horizontal, 16)
  32. }
  33. }
  34. .navigationTitle("设置")
  35. .navigationBarTitleDisplayMode(.inline)
  36. .alert("清理缓存", isPresented: $showClearCacheAlert) {
  37. Button("取消", role: .cancel) { }
  38. Button("清理", role: .destructive) {
  39. HapticManager.trigger(.tapFeedback)
  40. }
  41. } message: {
  42. Text("将清除所有本地缓存数据。此操作不可撤销。")
  43. }
  44. }
  45. }
  46. // MARK: - Storage Section
  47. private var storageSection: some View {
  48. settingsCard {
  49. VStack(alignment: .leading, spacing: 16) {
  50. sectionHeader(icon: "internaldrive", title: "存储空间")
  51. // Thin storage bar
  52. VStack(alignment: .leading, spacing: 8) {
  53. GeometryReader { geometry in
  54. ZStack(alignment: .leading) {
  55. // Background bar
  56. RoundedRectangle(cornerRadius: 3)
  57. .fill(Color.primary.opacity(0.06))
  58. .frame(height: 5)
  59. // Used portion
  60. RoundedRectangle(cornerRadius: 3)
  61. .fill(Color.primary)
  62. .frame(
  63. width: geometry.size.width * (usedStorage / totalStorage),
  64. height: 5
  65. )
  66. }
  67. }
  68. .frame(height: 5)
  69. HStack {
  70. Text(String(format: "已使用 %.1f GB", usedStorage))
  71. .font(.system(size: 11, weight: .regular))
  72. .foregroundStyle(Color.primary.opacity(0.8))
  73. Spacer()
  74. Text(String(format: "总容量 %.0f GB", totalStorage))
  75. .font(.system(size: 11, weight: .regular))
  76. .foregroundStyle(Color.secondary)
  77. }
  78. }
  79. // Clear cache button (minimal red outline)
  80. Button {
  81. showClearCacheAlert = true
  82. } label: {
  83. HStack(spacing: 6) {
  84. Image(systemName: "trash")
  85. .font(.system(size: 12, weight: .light))
  86. Text("清理缓存")
  87. .font(.system(size: 13, weight: .regular))
  88. }
  89. .foregroundStyle(Color.recordingRed)
  90. .padding(.vertical, 8)
  91. .frame(maxWidth: .infinity)
  92. .overlay(
  93. RoundedRectangle(cornerRadius: 6)
  94. .stroke(Color.recordingRed.opacity(0.3), lineWidth: 1)
  95. )
  96. }
  97. .padding(.top, 4)
  98. }
  99. }
  100. }
  101. // MARK: - Debug Section
  102. private var debugSection: some View {
  103. settingsCard {
  104. VStack(alignment: .leading, spacing: 14) {
  105. sectionHeader(icon: "hammer", title: "开发调试")
  106. // Data Storage Mode
  107. VStack(alignment: .leading, spacing: 4) {
  108. HStack {
  109. Text("数据存储模式")
  110. .font(.system(size: 13, weight: .regular))
  111. .foregroundStyle(Color.primary)
  112. Spacer()
  113. Text("纯本地保存")
  114. .font(.system(size: 13, weight: .medium))
  115. .foregroundStyle(Color.secondary)
  116. }
  117. Text("所有会议记录均保存在设备沙盒 SQLite 中,无网络上传")
  118. .font(.system(size: 10))
  119. .foregroundStyle(Color.secondary.opacity(0.6))
  120. }
  121. }
  122. }
  123. }
  124. // MARK: - Keep-Alive Section
  125. private var keepAliveSection: some View {
  126. settingsCard {
  127. VStack(alignment: .leading, spacing: 14) {
  128. sectionHeader(icon: "shield", title: "后台运行保障")
  129. Text("为保障全局不中断录音正常运行,请手动开启以下系统权限:")
  130. .font(.system(size: 12))
  131. .foregroundStyle(Color.secondary)
  132. VStack(alignment: .leading, spacing: 12) {
  133. keepAliveStep(
  134. number: 1,
  135. title: "开启后台音频",
  136. description: "Xcode 开启后台模式 -> 音频与 AirPlay",
  137. isChecked: true
  138. )
  139. keepAliveStep(
  140. number: 2,
  141. title: "禁用自动锁屏",
  142. description: "系统设置 -> 显示与亮度 -> 自动锁定 -> 永不",
  143. isChecked: true
  144. )
  145. keepAliveStep(
  146. number: 3,
  147. title: "允许始终访问定位",
  148. description: "系统设置 -> 隐私与安全性 -> 定位服务 -> 现场记录 -> 始终允许",
  149. isChecked: false
  150. )
  151. }
  152. .padding(.top, 4)
  153. }
  154. }
  155. }
  156. private func keepAliveStep(number: Int, title: String, description: String, isChecked: Bool) -> some View {
  157. HStack(alignment: .top, spacing: 10) {
  158. ZStack {
  159. Circle()
  160. .fill(Color.primary.opacity(isChecked ? 0.05 : 0.01))
  161. .frame(width: 24, height: 24)
  162. .businessBorder(cornerRadius: 12)
  163. if isChecked {
  164. Image(systemName: "checkmark")
  165. .font(.system(size: 10, weight: .semibold))
  166. .foregroundStyle(Color.primary)
  167. } else {
  168. Text("\(number)")
  169. .font(.system(size: 10, weight: .medium, design: .monospaced))
  170. .foregroundStyle(Color.secondary)
  171. }
  172. }
  173. VStack(alignment: .leading, spacing: 2) {
  174. Text(title)
  175. .font(.system(size: 13, weight: .medium))
  176. .foregroundStyle(Color.primary)
  177. Text(description)
  178. .font(.system(size: 11))
  179. .foregroundStyle(Color.secondary.opacity(0.8))
  180. }
  181. }
  182. }
  183. // MARK: - About Section
  184. private var aboutSection: some View {
  185. settingsCard {
  186. VStack(spacing: 14) {
  187. sectionHeader(icon: "info.circle", title: "关于")
  188. VStack(spacing: 10) {
  189. infoRow(label: "应用名称", value: "现场记录 (Celestia Trace)")
  190. infoRow(label: "版本信息", value: "0.1.0 (本地持久化单机版)")
  191. infoRow(label: "底层框架", value: "SwiftUI + SwiftData")
  192. infoRow(label: "系统支持", value: "iOS 17.0+ (模拟器)")
  193. }
  194. .padding(.top, 4)
  195. }
  196. }
  197. }
  198. private func infoRow(label: String, value: String) -> some View {
  199. HStack {
  200. Text(label)
  201. .font(.system(size: 12))
  202. .foregroundStyle(Color.secondary)
  203. Spacer()
  204. Text(value)
  205. .font(.system(size: 12, weight: .regular, design: .monospaced))
  206. .foregroundStyle(Color.primary)
  207. }
  208. }
  209. // MARK: - Footer
  210. private var footer: some View {
  211. VStack(spacing: 4) {
  212. Text("星痕现场记录")
  213. .font(.system(size: 10, weight: .bold, design: .monospaced))
  214. .foregroundStyle(Color.secondary.opacity(0.4))
  215. .tracking(2)
  216. Text("极简专业现场记录工作台")
  217. .font(.system(size: 9))
  218. .foregroundStyle(Color.secondary.opacity(0.3))
  219. }
  220. }
  221. // MARK: - Reusable Components
  222. private func settingsCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
  223. VStack(alignment: .leading, spacing: 0) {
  224. content()
  225. }
  226. .padding(16)
  227. .background(Color.cardBackground.opacity(0.15))
  228. .businessBorder(cornerRadius: 10)
  229. }
  230. private func sectionHeader(icon: String, title: String) -> some View {
  231. HStack(spacing: 8) {
  232. Image(systemName: icon)
  233. .font(.system(size: 12, weight: .light))
  234. .foregroundStyle(Color.secondary)
  235. Text(title)
  236. .font(.system(size: 14, weight: .semibold))
  237. .foregroundStyle(Color.primary)
  238. }
  239. }
  240. }
  241. // MARK: - Preview
  242. #Preview {
  243. SettingsView()
  244. }