|
|
@@ -3,24 +3,20 @@ import SwiftData
|
|
|
|
|
|
// MARK: - HomeView
|
|
|
/// The main launch screen of CelestiaTrace.
|
|
|
-/// Features a minimalist, line-drawn aesthetic with a central record button,
|
|
|
-/// bordered session cards, and a subtle technical grid background.
|
|
|
+/// Features a minimalist, line-drawn aesthetic with a central record button
|
|
|
+/// and a subtle technical grid background.
|
|
|
struct HomeView: View {
|
|
|
@Environment(\.modelContext) private var modelContext
|
|
|
- @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession]
|
|
|
@ObservedObject private var bleManager: BLEManager = .shared
|
|
|
@ObservedObject private var authManager: AuthManager = .shared
|
|
|
|
|
|
- @State private var navigateToRecording = false
|
|
|
- @State private var activeSession: CelestiaSession?
|
|
|
+ @State private var activeRecording: ActiveRecordingPresentation?
|
|
|
+ @State private var completedRecordingSession: CelestiaSession?
|
|
|
@State private var selectedSessionForDetail: CelestiaSession?
|
|
|
@State private var detector = RecordingEnvironmentDetector()
|
|
|
@State private var dismissedWarnings: Set<String> = []
|
|
|
@State private var showRecordingSourcePicker = false
|
|
|
- @State private var selectedRecordingSource: RecordingSourceChoice = .iPhone
|
|
|
-
|
|
|
- /// Callback to switch to the History tab from the parent TabView.
|
|
|
- var onSwitchToHistory: (() -> Void)?
|
|
|
+ @State private var pendingRecordingSource: RecordingSourceChoice?
|
|
|
|
|
|
var body: some View {
|
|
|
NavigationStack {
|
|
|
@@ -51,12 +47,6 @@ struct HomeView: View {
|
|
|
recordSection
|
|
|
|
|
|
Spacer()
|
|
|
-
|
|
|
- recentSessionsSection
|
|
|
- .padding(.bottom, 16)
|
|
|
-
|
|
|
- historyLink
|
|
|
- .padding(.bottom, 24)
|
|
|
}
|
|
|
.padding(.horizontal, 24)
|
|
|
}
|
|
|
@@ -64,17 +54,27 @@ struct HomeView: View {
|
|
|
.onAppear {
|
|
|
detector.checkEnvironment()
|
|
|
}
|
|
|
- .fullScreenCover(isPresented: $navigateToRecording) {
|
|
|
- if let session = activeSession {
|
|
|
- ActiveRecordingView(session: session, recordingSource: selectedRecordingSource) {
|
|
|
- selectedSessionForDetail = session
|
|
|
- }
|
|
|
+ .fullScreenCover(item: $activeRecording, onDismiss: {
|
|
|
+ if let session = completedRecordingSession {
|
|
|
+ selectedSessionForDetail = session
|
|
|
+ completedRecordingSession = nil
|
|
|
+ }
|
|
|
+ }) { recording in
|
|
|
+ ActiveRecordingView(
|
|
|
+ session: recording.session,
|
|
|
+ recordingSource: recording.source
|
|
|
+ ) {
|
|
|
+ completedRecordingSession = recording.session
|
|
|
}
|
|
|
}
|
|
|
- .sheet(isPresented: $showRecordingSourcePicker) {
|
|
|
+ .sheet(isPresented: $showRecordingSourcePicker, onDismiss: {
|
|
|
+ guard let source = pendingRecordingSource else { return }
|
|
|
+ pendingRecordingSource = nil
|
|
|
+ createSessionAndNavigate(source: source)
|
|
|
+ }) {
|
|
|
RecordingSourcePickerView(devices: connectedSparkDevices) { source in
|
|
|
+ pendingRecordingSource = source
|
|
|
showRecordingSourcePicker = false
|
|
|
- createSessionAndNavigate(source: source)
|
|
|
}
|
|
|
}
|
|
|
.navigationDestination(item: $selectedSessionForDetail) { session in
|
|
|
@@ -98,19 +98,6 @@ struct HomeView: View {
|
|
|
.foregroundStyle(Color.secondary)
|
|
|
}
|
|
|
Spacer()
|
|
|
-
|
|
|
- // Status indicator
|
|
|
- HStack(spacing: 6) {
|
|
|
- Circle()
|
|
|
- .fill(Color.primary.opacity(0.7))
|
|
|
- .frame(width: 5, height: 5)
|
|
|
- Text("就绪")
|
|
|
- .font(.system(size: 9, weight: .semibold, design: .monospaced))
|
|
|
- .foregroundStyle(Color.secondary)
|
|
|
- }
|
|
|
- .padding(.horizontal, 8)
|
|
|
- .padding(.vertical, 4)
|
|
|
- .businessBorder(cornerRadius: 4)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -122,86 +109,10 @@ struct HomeView: View {
|
|
|
beginNewRecording()
|
|
|
}
|
|
|
|
|
|
- VStack(spacing: 6) {
|
|
|
- Text("新建现场记录")
|
|
|
- .font(.system(size: 16, weight: .medium))
|
|
|
- .foregroundStyle(Color.primary)
|
|
|
-
|
|
|
- Text("点击开启全新现场录音")
|
|
|
- .font(.system(size: 9, weight: .medium, design: .monospaced))
|
|
|
- .foregroundStyle(Color.secondary.opacity(0.7))
|
|
|
- .tracking(2)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // MARK: - Recent Sessions
|
|
|
-
|
|
|
- private var recentSessionsSection: some View {
|
|
|
- VStack(alignment: .leading, spacing: 12) {
|
|
|
- HStack {
|
|
|
- Image(systemName: "list.dash")
|
|
|
- .font(.system(size: 12))
|
|
|
- .foregroundStyle(Color.secondary)
|
|
|
- Text("最近记录")
|
|
|
- .font(.system(size: 13, weight: .semibold))
|
|
|
- .foregroundStyle(Color.primary.opacity(0.8))
|
|
|
- }
|
|
|
-
|
|
|
- if sessions.isEmpty {
|
|
|
- emptyRecentState
|
|
|
- } else {
|
|
|
- ScrollView(.horizontal, showsIndicators: false) {
|
|
|
- HStack(spacing: 12) {
|
|
|
- ForEach(sessions.prefix(3)) { session in
|
|
|
- Button {
|
|
|
- selectedSessionForDetail = session
|
|
|
- } label: {
|
|
|
- RecentSessionCard(session: session)
|
|
|
- }
|
|
|
- .buttonStyle(.plain)
|
|
|
- }
|
|
|
- }
|
|
|
- .padding(.vertical, 2)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private var emptyRecentState: some View {
|
|
|
- HStack {
|
|
|
- Spacer()
|
|
|
- VStack(spacing: 8) {
|
|
|
- Image(systemName: "plus.square.dashed")
|
|
|
- .font(.system(size: 20))
|
|
|
- .foregroundStyle(Color.secondary.opacity(0.4))
|
|
|
- Text("暂无记录")
|
|
|
- .font(.system(size: 12))
|
|
|
- .foregroundStyle(Color.secondary.opacity(0.5))
|
|
|
- }
|
|
|
- .padding(.vertical, 32)
|
|
|
- Spacer()
|
|
|
- }
|
|
|
- .background(Color.cardBackground.opacity(0.3))
|
|
|
- .businessBorder(cornerRadius: 8)
|
|
|
- }
|
|
|
-
|
|
|
- // MARK: - History Link
|
|
|
-
|
|
|
- private var historyLink: some View {
|
|
|
- Button {
|
|
|
- onSwitchToHistory?()
|
|
|
- } label: {
|
|
|
- HStack(spacing: 6) {
|
|
|
- Text("查看历史列表")
|
|
|
- .font(.system(size: 13, weight: .regular))
|
|
|
- Image(systemName: "chevron.right")
|
|
|
- .font(.system(size: 10))
|
|
|
- }
|
|
|
- .foregroundStyle(Color.primary.opacity(0.7))
|
|
|
- .padding(.horizontal, 14)
|
|
|
- .padding(.vertical, 8)
|
|
|
- .businessBorder(cornerRadius: 6)
|
|
|
+ Text("开始新的现场记录")
|
|
|
+ .font(.system(size: 16, weight: .medium))
|
|
|
+ .foregroundStyle(Color.primary.opacity(0.88))
|
|
|
+ .tracking(0.4)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -244,17 +155,15 @@ struct HomeView: View {
|
|
|
|
|
|
private func createSessionAndNavigate(source: RecordingSourceChoice) {
|
|
|
let formatter = DateFormatter()
|
|
|
- formatter.dateFormat = "yyyy.MM.dd HH:mm"
|
|
|
- let title = "现场记录 · \(formatter.string(from: Date()))"
|
|
|
+ formatter.dateFormat = "yyyyMMdd_HHmm"
|
|
|
+ let title = "现场记录_\(formatter.string(from: Date()))"
|
|
|
|
|
|
let session = CelestiaSession(title: title)
|
|
|
modelContext.insert(session)
|
|
|
try? modelContext.save()
|
|
|
|
|
|
- activeSession = session
|
|
|
- selectedRecordingSource = source
|
|
|
HapticManager.trigger(.recordStart)
|
|
|
- navigateToRecording = true
|
|
|
+ activeRecording = ActiveRecordingPresentation(session: session, source: source)
|
|
|
}
|
|
|
|
|
|
private var connectedSparkDevices: [BoundDevice] {
|
|
|
@@ -273,6 +182,12 @@ struct HomeView: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private struct ActiveRecordingPresentation: Identifiable {
|
|
|
+ let id = UUID()
|
|
|
+ let session: CelestiaSession
|
|
|
+ let source: RecordingSourceChoice
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Recording source picker
|
|
|
|
|
|
/// Shared by new recordings and continuation recordings. When at least one
|
|
|
@@ -405,53 +320,6 @@ struct RecordingSourcePickerView: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// MARK: - Recent Session Card
|
|
|
-
|
|
|
-private struct RecentSessionCard: View {
|
|
|
- let session: CelestiaSession
|
|
|
-
|
|
|
- var body: some View {
|
|
|
- VStack(alignment: .leading, spacing: 8) {
|
|
|
- Text(session.title)
|
|
|
- .font(.system(size: 13, weight: .medium))
|
|
|
- .foregroundStyle(Color.primary)
|
|
|
- .lineLimit(1)
|
|
|
-
|
|
|
- HStack(spacing: 4) {
|
|
|
- Image(systemName: "waveform")
|
|
|
- .font(.system(size: 9))
|
|
|
- .foregroundStyle(Color.secondary)
|
|
|
- Text(session.durationFormatted)
|
|
|
- .font(.system(size: 11, weight: .regular, design: .monospaced))
|
|
|
- .foregroundStyle(Color.secondary)
|
|
|
- }
|
|
|
-
|
|
|
- HStack(spacing: 12) {
|
|
|
- HStack(spacing: 3) {
|
|
|
- Image(systemName: "camera")
|
|
|
- .font(.system(size: 9))
|
|
|
- Text("\(session.photoCount)")
|
|
|
- .font(.system(size: 10, design: .monospaced))
|
|
|
- }
|
|
|
- .foregroundStyle(Color.secondary)
|
|
|
-
|
|
|
- HStack(spacing: 3) {
|
|
|
- Image(systemName: "doc.text")
|
|
|
- .font(.system(size: 9))
|
|
|
- Text("\(session.noteCount)")
|
|
|
- .font(.system(size: 10, design: .monospaced))
|
|
|
- }
|
|
|
- .foregroundStyle(Color.secondary)
|
|
|
- }
|
|
|
- .padding(.top, 4)
|
|
|
- }
|
|
|
- .padding(12)
|
|
|
- .frame(width: 150, alignment: .leading)
|
|
|
- .background(Color.cardBackground.opacity(0.3))
|
|
|
- .businessBorder(cornerRadius: 8)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
// MARK: - Preview
|
|
|
|
|
|
#Preview {
|