HomeView.swift 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import SwiftUI
  2. import SwiftData
  3. // MARK: - HomeView
  4. /// The main launch screen of CelestiaTrace.
  5. /// Features a minimalist, line-drawn aesthetic with a central record button,
  6. /// bordered session cards, and a subtle technical grid background.
  7. struct HomeView: View {
  8. @Environment(\.modelContext) private var modelContext
  9. @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession]
  10. @State private var navigateToRecording = false
  11. @State private var activeSession: CelestiaSession?
  12. @State private var selectedSessionForDetail: CelestiaSession?
  13. @State private var detector = RecordingEnvironmentDetector()
  14. @State private var dismissedWarnings: Set<String> = []
  15. /// Callback to switch to the History tab from the parent TabView.
  16. var onSwitchToHistory: (() -> Void)?
  17. var body: some View {
  18. NavigationStack {
  19. ZStack {
  20. // Dynamic clean background
  21. Color.spaceBlack
  22. .ignoresSafeArea()
  23. // Subtle technical grid background
  24. businessGrid
  25. // Main content
  26. VStack(spacing: 0) {
  27. topBar
  28. .padding(.top, 16)
  29. if let firstWarning = detector.activeWarnings.first(where: { !dismissedWarnings.contains($0) }) {
  30. DiscreetWarningBanner(warning: firstWarning) {
  31. withAnimation {
  32. _ = dismissedWarnings.insert(firstWarning)
  33. }
  34. }
  35. .padding(.top, 12)
  36. }
  37. Spacer()
  38. recordSection
  39. Spacer()
  40. recentSessionsSection
  41. .padding(.bottom, 16)
  42. historyLink
  43. .padding(.bottom, 24)
  44. }
  45. .padding(.horizontal, 24)
  46. }
  47. .navigationBarHidden(true)
  48. .onAppear {
  49. detector.checkEnvironment()
  50. }
  51. .fullScreenCover(isPresented: $navigateToRecording) {
  52. if let session = activeSession {
  53. ActiveRecordingView(session: session) {
  54. selectedSessionForDetail = session
  55. }
  56. }
  57. }
  58. .navigationDestination(item: $selectedSessionForDetail) { session in
  59. SessionDetailView(session: session)
  60. }
  61. }
  62. }
  63. // MARK: - Top Bar
  64. private var topBar: some View {
  65. HStack(alignment: .top) {
  66. VStack(alignment: .leading, spacing: 4) {
  67. Text("星痕现场记录")
  68. .font(.system(size: 11, weight: .bold, design: .monospaced))
  69. .foregroundStyle(Color.primary.opacity(0.8))
  70. .tracking(3)
  71. Text(currentDateFormatted)
  72. .font(.system(size: 13, weight: .regular))
  73. .foregroundStyle(Color.secondary)
  74. }
  75. Spacer()
  76. // Status indicator
  77. HStack(spacing: 6) {
  78. Circle()
  79. .fill(Color.primary.opacity(0.7))
  80. .frame(width: 5, height: 5)
  81. Text("就绪")
  82. .font(.system(size: 9, weight: .semibold, design: .monospaced))
  83. .foregroundStyle(Color.secondary)
  84. }
  85. .padding(.horizontal, 8)
  86. .padding(.vertical, 4)
  87. .businessBorder(cornerRadius: 4)
  88. }
  89. }
  90. // MARK: - Record Section
  91. private var recordSection: some View {
  92. VStack(spacing: 24) {
  93. PulsingRecordButton {
  94. createSessionAndNavigate()
  95. }
  96. VStack(spacing: 6) {
  97. Text("新建现场记录")
  98. .font(.system(size: 16, weight: .medium))
  99. .foregroundStyle(Color.primary)
  100. Text("点击开启全新现场录音")
  101. .font(.system(size: 9, weight: .medium, design: .monospaced))
  102. .foregroundStyle(Color.secondary.opacity(0.7))
  103. .tracking(2)
  104. }
  105. }
  106. }
  107. // MARK: - Recent Sessions
  108. private var recentSessionsSection: some View {
  109. VStack(alignment: .leading, spacing: 12) {
  110. HStack {
  111. Image(systemName: "list.dash")
  112. .font(.system(size: 12))
  113. .foregroundStyle(Color.secondary)
  114. Text("最近记录")
  115. .font(.system(size: 13, weight: .semibold))
  116. .foregroundStyle(Color.primary.opacity(0.8))
  117. }
  118. if sessions.isEmpty {
  119. emptyRecentState
  120. } else {
  121. ScrollView(.horizontal, showsIndicators: false) {
  122. HStack(spacing: 12) {
  123. ForEach(sessions.prefix(3)) { session in
  124. Button {
  125. selectedSessionForDetail = session
  126. } label: {
  127. RecentSessionCard(session: session)
  128. }
  129. .buttonStyle(.plain)
  130. }
  131. }
  132. .padding(.vertical, 2)
  133. }
  134. }
  135. }
  136. }
  137. private var emptyRecentState: some View {
  138. HStack {
  139. Spacer()
  140. VStack(spacing: 8) {
  141. Image(systemName: "plus.square.dashed")
  142. .font(.system(size: 20))
  143. .foregroundStyle(Color.secondary.opacity(0.4))
  144. Text("暂无记录")
  145. .font(.system(size: 12))
  146. .foregroundStyle(Color.secondary.opacity(0.5))
  147. }
  148. .padding(.vertical, 32)
  149. Spacer()
  150. }
  151. .background(Color.cardBackground.opacity(0.3))
  152. .businessBorder(cornerRadius: 8)
  153. }
  154. // MARK: - History Link
  155. private var historyLink: some View {
  156. Button {
  157. onSwitchToHistory?()
  158. } label: {
  159. HStack(spacing: 6) {
  160. Text("查看历史列表")
  161. .font(.system(size: 13, weight: .regular))
  162. Image(systemName: "chevron.right")
  163. .font(.system(size: 10))
  164. }
  165. .foregroundStyle(Color.primary.opacity(0.7))
  166. .padding(.horizontal, 14)
  167. .padding(.vertical, 8)
  168. .businessBorder(cornerRadius: 6)
  169. }
  170. }
  171. // MARK: - Technical Grid Background
  172. private var businessGrid: some View {
  173. Canvas { context, size in
  174. let step: CGFloat = 40
  175. let cols = Int(size.width / step)
  176. let rows = Int(size.height / step)
  177. for col in 0...cols {
  178. let x = CGFloat(col) * step
  179. var path = Path()
  180. path.move(to: CGPoint(x: x, y: 0))
  181. path.addLine(to: CGPoint(x: x, y: size.height))
  182. context.stroke(path, with: .color(Color.primary.opacity(0.015)), lineWidth: 0.5)
  183. }
  184. for row in 0...rows {
  185. let y = CGFloat(row) * step
  186. var path = Path()
  187. path.move(to: CGPoint(x: 0, y: y))
  188. path.addLine(to: CGPoint(x: size.width, y: y))
  189. context.stroke(path, with: .color(Color.primary.opacity(0.015)), lineWidth: 0.5)
  190. }
  191. }
  192. .ignoresSafeArea()
  193. .allowsHitTesting(false)
  194. }
  195. // MARK: - Actions
  196. private func createSessionAndNavigate() {
  197. let formatter = DateFormatter()
  198. formatter.dateFormat = "yyyy.MM.dd HH:mm"
  199. let title = "现场记录 · \(formatter.string(from: Date()))"
  200. let session = CelestiaSession(title: title)
  201. modelContext.insert(session)
  202. try? modelContext.save()
  203. activeSession = session
  204. HapticManager.trigger(.recordStart)
  205. navigateToRecording = true
  206. }
  207. // MARK: - Helpers
  208. private var currentDateFormatted: String {
  209. let formatter = DateFormatter()
  210. formatter.locale = Locale(identifier: "zh_Hans")
  211. formatter.dateFormat = "yyyy年M月d日 EEEE"
  212. return formatter.string(from: Date())
  213. }
  214. }
  215. // MARK: - Recent Session Card
  216. private struct RecentSessionCard: View {
  217. let session: CelestiaSession
  218. var body: some View {
  219. VStack(alignment: .leading, spacing: 8) {
  220. Text(session.title)
  221. .font(.system(size: 13, weight: .medium))
  222. .foregroundStyle(Color.primary)
  223. .lineLimit(1)
  224. HStack(spacing: 4) {
  225. Image(systemName: "waveform")
  226. .font(.system(size: 9))
  227. .foregroundStyle(Color.secondary)
  228. Text(session.durationFormatted)
  229. .font(.system(size: 11, weight: .regular, design: .monospaced))
  230. .foregroundStyle(Color.secondary)
  231. }
  232. HStack(spacing: 12) {
  233. HStack(spacing: 3) {
  234. Image(systemName: "camera")
  235. .font(.system(size: 9))
  236. Text("\(session.photoCount)")
  237. .font(.system(size: 10, design: .monospaced))
  238. }
  239. .foregroundStyle(Color.secondary)
  240. HStack(spacing: 3) {
  241. Image(systemName: "doc.text")
  242. .font(.system(size: 9))
  243. Text("\(session.noteCount)")
  244. .font(.system(size: 10, design: .monospaced))
  245. }
  246. .foregroundStyle(Color.secondary)
  247. }
  248. .padding(.top, 4)
  249. }
  250. .padding(12)
  251. .frame(width: 150, alignment: .leading)
  252. .background(Color.cardBackground.opacity(0.3))
  253. .businessBorder(cornerRadius: 8)
  254. }
  255. }
  256. // MARK: - Preview
  257. #Preview {
  258. HomeView()
  259. .modelContainer(for: CelestiaSession.self, inMemory: true)
  260. }