Răsfoiți Sursa

feat(recording): 简化主页录音入口、优化全屏录音跳转与记录名称修改体验

bob.yuxinyang 1 lună în urmă
părinte
comite
5973c81754

+ 1 - 1
CelestiaTrace/Views/ContentView.swift

@@ -12,7 +12,7 @@ struct ContentView: View {
 
     var body: some View {
         TabView(selection: $selectedTab) {
-            HomeView(onSwitchToHistory: { selectedTab = .history })
+            HomeView()
                 .tabItem {
                     Label(Tab.home.title, systemImage: Tab.home.icon)
                 }

+ 35 - 167
CelestiaTrace/Views/Home/HomeView.swift

@@ -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 {

+ 143 - 13
CelestiaTrace/Views/Recording/ActiveRecordingView.swift

@@ -22,6 +22,9 @@ struct ActiveRecordingView: View {
     @State private var latestEventVisible = false
     @State private var isEndingRecording = false
     @State private var stopErrorMessage: String?
+    @State private var recordName: String
+    @State private var showRecordNameEditor = false
+    @FocusState private var isRecordNameFocused: Bool
 
     init(
         session: CelestiaSession,
@@ -34,17 +37,20 @@ struct ActiveRecordingView: View {
         self.recordingSource = recordingSource
         self.onFinishRecording = onFinishRecording
         _recordingVM = State(initialValue: RecordingViewModel(source: recordingSource))
+        _recordName = State(initialValue: session.title)
     }
 
     var body: some View {
-        ZStack {
+        ZStack(alignment: .top) {
             // Dynamic clean background
             Color.spaceBlack
                 .ignoresSafeArea()
 
             VStack(spacing: 0) {
-                Spacer()
-                    .frame(height: 20)
+                recordNameSection
+                    .padding(.horizontal, 24)
+                    .padding(.top, 16)
+                    .padding(.bottom, 14)
 
                 // Top Header Block (Timecode + Indicator)
                 VStack(spacing: 16) {
@@ -74,11 +80,17 @@ struct ActiveRecordingView: View {
                 endRecordingButton
                     .padding(.bottom, 24)
             }
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
+            .ignoresSafeArea(.keyboard, edges: .bottom)
         }
+        .ignoresSafeArea(.keyboard, edges: .bottom)
         .toolbar(.hidden, for: .tabBar)
         .toolbar(.hidden, for: .navigationBar)
         .navigationBarHidden(true)
         .navigationBarBackButtonHidden(true)
+        .sheet(isPresented: $showRecordNameEditor) {
+            recordNameEditorSheet
+        }
         .sheet(isPresented: $showNoteSheet) {
             noteInputSheet
         }
@@ -93,6 +105,7 @@ struct ActiveRecordingView: View {
         }
         .onDisappear {
             UIApplication.shared.isIdleTimerDisabled = false
+            commitRecordName()
             if recordingVM.isRecording {
                 recordingVM.stopRecording()
             }
@@ -108,6 +121,104 @@ struct ActiveRecordingView: View {
         }
     }
 
+    // MARK: - Record Name
+
+    private var recordNameSection: some View {
+        Button {
+            recordName = session.title
+            showRecordNameEditor = true
+        } label: {
+            VStack(alignment: .leading, spacing: 8) {
+                HStack(spacing: 6) {
+                    Text("记录名称")
+                        .font(.system(size: 10, weight: .semibold, design: .monospaced))
+                        .tracking(1.4)
+
+                    Spacer()
+
+                    Image(systemName: "pencil")
+                        .font(.system(size: 11, weight: .semibold))
+                }
+                .foregroundStyle(Color.secondary)
+
+                Text(session.title)
+                    .font(.system(size: 19, weight: .semibold))
+                    .foregroundStyle(Color.primary)
+                    .lineLimit(1)
+                    .frame(maxWidth: .infinity, alignment: .leading)
+
+                Rectangle()
+                    .fill(Color.primary.opacity(0.16))
+                    .frame(height: 1)
+            }
+            .padding(.horizontal, 14)
+            .padding(.vertical, 12)
+            .background(Color.cardBackground.opacity(0.45))
+            .businessBorder(cornerRadius: 8)
+            .contentShape(Rectangle())
+        }
+        .buttonStyle(.plain)
+    }
+
+    private var recordNameEditorSheet: some View {
+        NavigationStack {
+            ZStack {
+                Color.spaceBlack.ignoresSafeArea()
+
+                VStack(alignment: .leading, spacing: 10) {
+                    Text("记录名称")
+                        .font(.system(size: 10, weight: .semibold, design: .monospaced))
+                        .foregroundStyle(Color.secondary)
+                        .tracking(1.4)
+
+                    TextField("输入记录名称", text: $recordName)
+                        .focused($isRecordNameFocused)
+                        .font(.system(size: 18, weight: .semibold))
+                        .foregroundStyle(Color.primary)
+                        .textInputAutocapitalization(.never)
+                        .autocorrectionDisabled()
+                        .submitLabel(.done)
+                        .padding(.horizontal, 14)
+                        .padding(.vertical, 12)
+                        .background(Color.cardBackground.opacity(0.5))
+                        .businessBorder(cornerRadius: 8)
+                        .onSubmit {
+                            saveRecordNameAndCloseEditor()
+                        }
+
+                    Spacer()
+                }
+                .padding(20)
+            }
+            .navigationTitle("修改记录名称")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("取消") {
+                        recordName = session.title
+                        showRecordNameEditor = false
+                    }
+                    .foregroundStyle(Color.secondary)
+                }
+
+                ToolbarItem(placement: .confirmationAction) {
+                    Button("保存") {
+                        saveRecordNameAndCloseEditor()
+                    }
+                    .disabled(recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
+                }
+            }
+        }
+        .presentationDetents([.height(190)])
+        .presentationDragIndicator(.visible)
+        .presentationBackground(Color.spaceBlack)
+        .onAppear {
+            DispatchQueue.main.async {
+                isRecordNameFocused = true
+            }
+        }
+    }
+
     // MARK: - Timecode
 
     private var timecodeSection: some View {
@@ -138,11 +249,6 @@ struct ActiveRecordingView: View {
                     .font(.system(size: 11, weight: .semibold, design: .monospaced))
                     .foregroundStyle(recordingVM.isRecording ? Color.recordingRed : Color.secondary)
                     .tracking(1.2)
-
-                Text("· \(session.title)")
-                    .font(.system(size: 11, weight: .regular))
-                    .foregroundStyle(Color.secondary.opacity(0.7))
-                    .lineLimit(1)
             }
 
             HStack(spacing: 6) {
@@ -175,11 +281,6 @@ struct ActiveRecordingView: View {
 
             // Graphical VU Meter + Numeric Display
             HStack(spacing: 10) {
-                Text("音量电平")
-                    .font(.system(size: 9, weight: .semibold, design: .monospaced))
-                    .foregroundStyle(Color.secondary.opacity(0.6))
-                    .tracking(1.5)
-
                 AmplitudeLevelMeterView(amplitude: recordingVM.currentAmplitude)
 
                 Spacer(minLength: 0)
@@ -402,6 +503,7 @@ struct ActiveRecordingView: View {
 
     private func endRecording() {
         guard !isEndingRecording else { return }
+        commitRecordName()
         isEndingRecording = true
 
         recordingVM.stopRecording { result in
@@ -471,6 +573,34 @@ struct ActiveRecordingView: View {
         default: return "事件"
         }
     }
+
+    private func commitRecordName() {
+        let trimmedName = recordName.trimmingCharacters(in: .whitespacesAndNewlines)
+        if trimmedName.isEmpty {
+            recordName = session.title
+            return
+        }
+
+        guard session.title != trimmedName else {
+            if recordName != trimmedName {
+                recordName = trimmedName
+            }
+            return
+        }
+
+        recordName = trimmedName
+        session.title = trimmedName
+        session.isSynced = false
+        session.syncState = .pending
+        try? modelContext.save()
+    }
+
+    private func saveRecordNameAndCloseEditor() {
+        commitRecordName()
+        guard !recordName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
+        isRecordNameFocused = false
+        showRecordNameEditor = false
+    }
 }
 
 // MARK: - Amplitude Level Meter View