Explorar el Código

style(ui): 简化实时活动卡片文案、优化空状态与登录弹窗说明

bob.yuxinyang hace 1 mes
padre
commit
7b6b5fb5aa

+ 3 - 2
CelestiaTrace/Views/Components/DeveloperLogOverlay.swift

@@ -21,6 +21,7 @@ struct DeveloperLogEntry: Identifiable, Equatable {
 final class DeveloperLogStore: ObservableObject, @unchecked Sendable {
     static let shared = DeveloperLogStore()
     static let enabledKey = "com.celestia.trace.developer_mode"
+    private static let maximumEntryCount = 2_000
 
     @Published private(set) var entries: [DeveloperLogEntry] = []
 
@@ -40,8 +41,8 @@ final class DeveloperLogStore: ObservableObject, @unchecked Sendable {
         )
         DispatchQueue.main.async {
             shared.entries.append(entry)
-            if shared.entries.count > 500 {
-                shared.entries.removeFirst(shared.entries.count - 500)
+            if shared.entries.count > maximumEntryCount {
+                shared.entries.removeFirst(shared.entries.count - maximumEntryCount)
             }
         }
     }

+ 42 - 2
CelestiaTrace/Views/ContentView.swift

@@ -16,13 +16,15 @@ struct ContentView: View {
     @State private var selectedTab: Tab = .home
     /// Session selected for detail playback navigation in Field Records tab.
     @State private var fieldRecordsSelectedSession: CelestiaSession?
+    @State private var showFieldRecordsAuthentication = false
+    @State private var shouldOpenFieldRecordsAfterLogin = false
 
     var body: some View {
         ZStack(alignment: .bottomTrailing) {
-            TabView(selection: $selectedTab) {
+            TabView(selection: protectedTabSelection) {
                 HomeView { session in
                     fieldRecordsSelectedSession = session
-                    selectedTab = .fieldRecords
+                    selectTab(.fieldRecords)
                 }
                 .tabItem {
                     Label(Tab.home.title, systemImage: Tab.home.icon)
@@ -55,6 +57,15 @@ struct ContentView: View {
             }
         }
         .animation(.easeInOut(duration: 0.2), value: isDeveloperModeEnabled)
+        .sheet(isPresented: $showFieldRecordsAuthentication, onDismiss: {
+            if !authManager.isLoggedIn {
+                shouldOpenFieldRecordsAfterLogin = false
+            }
+        }) {
+            AuthModalView(
+                guidanceMessage: "登录或注册后,才能查看现场记录"
+            )
+        }
         .onAppear {
             if isDeveloperModeEnabled {
                 DeveloperLogStore.log("App", "开发者模式已启用,开始记录本次运行日志", level: .success)
@@ -67,6 +78,15 @@ struct ContentView: View {
         .task(id: authManager.currentUser?.id) {
             await synchronizeFieldRecords()
         }
+        .onChange(of: authManager.currentUser?.id) { _, userID in
+            if userID != nil, shouldOpenFieldRecordsAfterLogin {
+                shouldOpenFieldRecordsAfterLogin = false
+                selectedTab = .fieldRecords
+            } else if userID == nil, selectedTab == .fieldRecords {
+                selectedTab = .home
+                requestFieldRecordsAuthentication()
+            }
+        }
         .onChange(of: scenePhase) { _, newPhase in
             guard newPhase == .active else { return }
             Task {
@@ -84,6 +104,26 @@ struct ContentView: View {
         }
     }
 
+    private var protectedTabSelection: Binding<Tab> {
+        Binding(
+            get: { selectedTab },
+            set: { selectTab($0) }
+        )
+    }
+
+    private func selectTab(_ tab: Tab) {
+        guard tab != .fieldRecords || authManager.isLoggedIn else {
+            requestFieldRecordsAuthentication()
+            return
+        }
+        selectedTab = tab
+    }
+
+    private func requestFieldRecordsAuthentication() {
+        shouldOpenFieldRecordsAfterLogin = true
+        showFieldRecordsAuthentication = true
+    }
+
     @MainActor
     private func synchronizeFieldRecords() async {
         guard let userID = authManager.currentUser?.id else { return }

+ 28 - 6
CelestiaTrace/Views/Detail/SessionDetailView.swift

@@ -238,6 +238,10 @@ struct SessionDetailView: View {
                 scheduleAutoSync()
             }
         }
+        .onDisappear {
+            playbackVM.pause()
+            playbackVM.cancelAudioAnalysis()
+        }
         .onChange(of: session.syncStateRaw) { _, newValue in
             guard let state = SessionSyncState(rawValue: newValue),
                   state == .pending || state == .localOnly,
@@ -696,7 +700,8 @@ struct SessionDetailView: View {
     }
 
     private var isThisSessionSyncing: Bool {
-        syncManager.isSyncing && syncManager.activeSessionID == session.id
+        session.syncState == .syncing
+            || (syncManager.isSyncing && syncManager.activeSessionID == session.id)
     }
 
     private var isDisplayedSyncing: Bool {
@@ -704,7 +709,10 @@ struct SessionDetailView: View {
     }
 
     private var isDisplayedSynced: Bool {
-        !isDisplayedSyncing && session.hasConfirmedCloudSync
+        !isDisplayedSyncing
+            && session.hasConfirmedCloudSync
+            && session.syncState != .failed
+            && session.syncState != .conflict
     }
 
     private var sessionSyncProgress: Double {
@@ -810,17 +818,20 @@ struct SessionDetailView: View {
                 events: session.events,
                 currentTimeMs: Binding(
                     get: { playbackVM.currentPlaybackTimeMs },
-                    set: { playbackVM.seekTo(timeMs: $0) }
+                    set: { playbackVM.previewScrub(to: $0) }
                 ),
                 totalDurationMs: playbackVM.totalDurationMs,
                 waveformSamples: playbackVM.waveformSamples,
+                waveformRevision: playbackVM.waveformRevision,
                 silentRanges: playbackVM.silentRanges,
                 onEmptyTrackTap: handleEmptyTrackTap,
                 onEventTap: { event in
                     playbackVM.seekTo(timeMs: Double(event.relativeTimeMs))
                     openTimelineEvent(event)
                     HapticManager.trigger(.tapFeedback)
-                }
+                },
+                onScrubBegan: playbackVM.beginScrubbing,
+                onScrubEnded: playbackVM.endScrubbing
             )
             .frame(height: 220)
         }
@@ -848,10 +859,21 @@ struct SessionDetailView: View {
                 value: Binding(
                     get: { playbackVM.progress },
                     set: { newValue in
-                        playbackVM.seekTo(timeMs: newValue * playbackVM.totalDurationMs)
+                        playbackVM.previewScrub(
+                            to: newValue * playbackVM.totalDurationMs
+                        )
                     }
                 ),
-                in: 0...1
+                in: 0...1,
+                onEditingChanged: { isEditing in
+                    if isEditing {
+                        playbackVM.beginScrubbing()
+                    } else {
+                        playbackVM.endScrubbing(
+                            at: playbackVM.currentPlaybackTimeMs
+                        )
+                    }
+                }
             )
             .tint(Color.primary)
 

+ 10 - 3
CelestiaTrace/Views/FieldRecords/SessionListView.swift

@@ -292,18 +292,25 @@ private struct SessionRowCard: View {
         } else {
             Circle()
                 .fill(
-                    session.hasConfirmedCloudSync
+                    isDisplayedSynced
                         ? Color.lessCosmosGold
                         : Color.secondary.opacity(0.4)
                 )
                 .frame(width: 6, height: 6)
                 .frame(width: 10, height: 10)
-                .accessibilityLabel(session.hasConfirmedCloudSync ? "已同步" : "未同步")
+                .accessibilityLabel(isDisplayedSynced ? "已同步" : "未同步")
         }
     }
 
     private var isThisSessionSyncing: Bool {
-        syncManager.isSyncing && syncManager.activeSessionID == session.id
+        session.syncState == .syncing
+            || (syncManager.isSyncing && syncManager.activeSessionID == session.id)
+    }
+
+    private var isDisplayedSynced: Bool {
+        session.hasConfirmedCloudSync
+            && session.syncState != .failed
+            && session.syncState != .conflict
     }
 
     private func countBadge(icon: String, count: Int) -> some View {

+ 52 - 1
CelestiaTrace/Views/Home/HomeView.swift

@@ -19,6 +19,8 @@ struct HomeView: View {
     @State private var dismissedWarnings: Set<String> = []
     @State private var showRecordingSourcePicker = false
     @State private var pendingRecordingSource: RecordingSourceChoice?
+    @State private var showAuthentication = false
+    @State private var pendingProtectedAction: ProtectedRecordingAction?
 
     var onRecordingFinished: ((CelestiaSession) -> Void)? = nil
 
@@ -59,6 +61,20 @@ struct HomeView: View {
                 detector.checkEnvironment()
                 restoreActiveRecordingIfNeeded()
             }
+            .onChange(of: authManager.currentUser?.id) { _, userID in
+                guard userID != nil, let action = pendingProtectedAction else { return }
+                pendingProtectedAction = nil
+
+                switch action {
+                case .startNew:
+                    beginNewRecording()
+                case .restore(let sessionID, let recordingAction):
+                    restoreActiveRecordingIfNeeded(
+                        sessionID: sessionID,
+                        action: recordingAction
+                    )
+                }
+            }
             .onOpenURL(perform: handleRecordingURL)
             .fullScreenCover(item: $activeRecording, onDismiss: {
                 if let session = completedRecordingSession {
@@ -85,6 +101,15 @@ struct HomeView: View {
                     showRecordingSourcePicker = false
                 }
             }
+            .sheet(isPresented: $showAuthentication, onDismiss: {
+                if !authManager.isLoggedIn {
+                    pendingProtectedAction = nil
+                }
+            }) {
+                AuthModalView(
+                    guidanceMessage: "登录或注册后,才能开始和继续现场记录"
+                )
+            }
         }
     }
 
@@ -156,6 +181,11 @@ struct HomeView: View {
     // MARK: - Actions
 
     private func beginNewRecording() {
+        guard authManager.isLoggedIn else {
+            requestAuthentication(for: .startNew)
+            return
+        }
+
         if connectedSparkDevices.isEmpty {
             createSessionAndNavigate(source: .iPhone)
         } else {
@@ -164,6 +194,11 @@ struct HomeView: View {
     }
 
     private func createSessionAndNavigate(source: RecordingSourceChoice) {
+        guard authManager.isLoggedIn else {
+            requestAuthentication(for: .startNew)
+            return
+        }
+
         let formatter = DateFormatter()
         formatter.dateFormat = "yyyyMMdd_HHmm"
         let title = "\(formatter.string(from: Date()))_现场记录"
@@ -186,7 +221,7 @@ struct HomeView: View {
         sessionID requestedSessionID: UUID? = nil,
         action: RecordingLiveActivityAction? = nil
     ) {
-        guard activeRecording == nil else { return }
+        guard authManager.isLoggedIn, activeRecording == nil else { return }
 
         let persistedID = RecordingRecoveryStore.activeSessionID
         let activityID = RecordingLiveActivityManager.shared.recoverableSessionID
@@ -267,12 +302,23 @@ struct HomeView: View {
 
     private func handleRecordingURL(_ url: URL) {
         guard let route = RecordingLiveActivityRoute(url: url) else { return }
+        guard authManager.isLoggedIn else {
+            requestAuthentication(
+                for: .restore(sessionID: route.sessionID, action: route.action)
+            )
+            return
+        }
         restoreActiveRecordingIfNeeded(
             sessionID: route.sessionID,
             action: route.action
         )
     }
 
+    private func requestAuthentication(for action: ProtectedRecordingAction) {
+        pendingProtectedAction = action
+        showAuthentication = true
+    }
+
     private var connectedSparkDevices: [BoundDevice] {
         guard let userID = authManager.currentUser?.id else { return [] }
         return bleManager.connectedDevices(forUserId: userID)
@@ -289,6 +335,11 @@ struct HomeView: View {
     }
 }
 
+private enum ProtectedRecordingAction {
+    case startNew
+    case restore(sessionID: UUID?, action: RecordingLiveActivityAction?)
+}
+
 private struct ActiveRecordingPresentation: Identifiable {
     let id = UUID()
     let session: CelestiaSession

+ 7 - 1
CelestiaTrace/Views/Profile/Auth/AuthModalView.swift

@@ -4,6 +4,7 @@ import SwiftUI
 struct AuthModalView: View {
     @Environment(\.dismiss) private var dismiss
     @ObservedObject var authManager: AuthManager = .shared
+    var guidanceMessage: String? = nil
     
     @State private var selectedTab: AuthTab = .login
     @State private var usernameInput: String = ""
@@ -51,7 +52,12 @@ struct AuthModalView: View {
                         .font(.system(size: 22, weight: .bold))
                         .foregroundStyle(Color.primary)
                     
-                    Text(selectedTab == .login ? "登录以同步录音并管理专属设备" : "注册后可将本机记录安全同步至云端")
+                    Text(
+                        guidanceMessage
+                            ?? (selectedTab == .login
+                                ? "登录以同步录音并管理专属设备"
+                                : "注册后可将本机记录安全同步至云端")
+                    )
                         .font(.system(size: 12))
                         .foregroundStyle(Color.secondary)
                         .multilineTextAlignment(.center)

+ 4 - 4
CelestiaTraceLiveActivity/RecordingLiveActivityWidget.swift

@@ -102,12 +102,12 @@ struct RecordingLiveActivityWidget: Widget {
             VStack(spacing: 8) {
                 HStack(spacing: 10) {
                     quickAction(
-                        title: context.state.isPaused ? "继续记录" : "暂停记录",
+                        title: context.state.isPaused ? "继续" : "暂停",
                         systemImage: context.state.isPaused ? "play.fill" : "pause.fill",
                         url: actionURL("toggle-pause", context.attributes)
                     )
                     quickAction(
-                        title: "结束记录",
+                        title: "结束",
                         systemImage: "stop.fill",
                         url: actionURL("stop", context.attributes),
                         tint: .red
@@ -115,12 +115,12 @@ struct RecordingLiveActivityWidget: Widget {
                 }
                 HStack(spacing: 10) {
                     quickAction(
-                        title: "拍照记录",
+                        title: "拍照",
                         systemImage: "camera.fill",
                         url: actionURL("photo", context.attributes)
                     )
                     quickAction(
-                        title: "文字记录",
+                        title: "文字",
                         systemImage: "note.text",
                         url: actionURL("note", context.attributes)
                     )