import SwiftUI import SwiftData // MARK: - SessionListView /// The session archive screen showing all recorded sessions /// in a searchable, card-style list with sync status indicators. /// Redesigned with a minimalist wireframe business style. struct SessionListView: View { @Environment(\.modelContext) private var modelContext @Query(sort: \CelestiaSession.startTime, order: .reverse) private var sessions: [CelestiaSession] @State private var searchText = "" @State private var showSyncAuthPrompt = false @State private var showAuthModal = false var body: some View { NavigationStack { ZStack { Color.spaceBlack.ignoresSafeArea() if filteredSessions.isEmpty { emptyState } else { sessionList } } .navigationTitle("会话档案") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { AuthManager.shared.syncRecordings { showSyncAuthPrompt = true } onComplete: { _ in } } label: { HStack(spacing: 4) { Image(systemName: "icloud.and.arrow.up") .font(.system(size: 13)) Text("同步") .font(.system(size: 13, weight: .medium)) } .foregroundStyle(Color.primary) } } } .searchable( text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "搜索会话或笔记..." ) .sheet(isPresented: $showSyncAuthPrompt) { SyncAuthPromptModal(onLoginClick: { showAuthModal = true }) } .sheet(isPresented: $showAuthModal) { AuthModalView() } } } // MARK: - Filtered Sessions private var filteredSessions: [CelestiaSession] { guard !searchText.isEmpty else { return sessions } let query = searchText.lowercased() return sessions.filter { session in if session.title.lowercased().contains(query) { return true } return session.events.contains { event in event.textContent?.lowercased().contains(query) == true } } } // MARK: - Session List private var sessionList: some View { ScrollView { LazyVStack(spacing: 12) { // Stats header statsHeader .padding(.horizontal, 16) .padding(.top, 12) ForEach(filteredSessions) { session in NavigationLink(destination: SessionDetailView(session: session)) { SessionRowCard(session: session) } .buttonStyle(.plain) .padding(.horizontal, 16) } } .padding(.bottom, 24) } } // MARK: - Stats Header private var statsHeader: some View { HStack(spacing: 20) { statBadge( value: "\(filteredSessions.count)", label: "总会话", icon: "waveform" ) statBadge( value: "\(filteredSessions.reduce(0) { $0 + $1.photoCount })", label: "已获图片", icon: "camera" ) statBadge( value: "\(filteredSessions.filter { $0.isSynced }.count)", label: "已同步", icon: "icloud" ) } .padding(.vertical, 14) .background(Color.cardBackground.opacity(0.3)) .businessBorder(cornerRadius: 10) } private func statBadge(value: String, label: String, icon: String) -> some View { VStack(spacing: 4) { Image(systemName: icon) .font(.system(size: 13, weight: .light)) .foregroundStyle(Color.secondary) Text(value) .font(.system(size: 16, weight: .medium, design: .monospaced)) .foregroundStyle(Color.primary) Text(label) .font(.system(size: 10, weight: .regular)) .foregroundStyle(Color.secondary.opacity(0.8)) } .frame(maxWidth: .infinity) } // MARK: - Empty State private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "square.dashed") .font(.system(size: 36)) .foregroundStyle(Color.secondary.opacity(0.4)) Text("暂无记录") .font(.system(size: 14, weight: .medium)) .foregroundStyle(Color.primary) if !searchText.isEmpty { Text("未找到与「\(searchText)」匹配的会话") .font(.system(size: 12)) .foregroundStyle(Color.secondary) } else { Text("启动全局录音来保存你的第一条记录") .font(.system(size: 12)) .foregroundStyle(Color.secondary.opacity(0.7)) } } .padding(.bottom, 60) } } // MARK: - Session Row Card private struct SessionRowCard: View { let session: CelestiaSession var body: some View { HStack(spacing: 12) { // Sync status indicator (plain circular dot) Circle() .fill(session.isSynced ? Color.secondary.opacity(0.4) : Color.recordingRed.opacity(0.6)) .frame(width: 6, height: 6) VStack(alignment: .leading, spacing: 4) { Text(session.title) .font(.system(size: 14, weight: .medium)) .foregroundStyle(Color.primary) .lineLimit(1) HStack(spacing: 6) { Text(session.startTime.formatted(.dateTime.month().day().hour().minute())) .font(.system(size: 11)) .foregroundStyle(Color.secondary) Text("·") .foregroundStyle(Color.secondary.opacity(0.4)) Text(session.durationFormatted) .font(.system(size: 11, weight: .regular, design: .monospaced)) .foregroundStyle(Color.secondary) } } Spacer() // Count badges (plain line style) HStack(spacing: 8) { countBadge(icon: "camera", count: session.photoCount) countBadge(icon: "doc.text", count: session.noteCount) } .padding(.trailing, 4) // Chevron Image(systemName: "chevron.right") .font(.system(size: 10, weight: .light)) .foregroundStyle(Color.secondary.opacity(0.5)) } .padding(14) .background(Color.cardBackground.opacity(0.2)) .businessBorder(cornerRadius: 8) } private func countBadge(icon: String, count: Int) -> some View { HStack(spacing: 3) { Image(systemName: icon) .font(.system(size: 10, weight: .light)) Text("\(count)") .font(.system(size: 11, weight: .regular, design: .monospaced)) } .foregroundStyle(Color.secondary) .padding(.horizontal, 6) .padding(.vertical, 2) .businessBorder(cornerRadius: 4) } } // MARK: - Preview #Preview { SessionListView() .modelContainer(for: CelestiaSession.self, inMemory: true) }