import SwiftUI struct HistoryView: View { @EnvironmentObject var store: ConnectionStore var body: some View { ZStack { AppTheme.background.ignoresSafeArea() if store.historyEntries.isEmpty { emptyState } else { List { ForEach(store.historyEntries) { entry in HistoryRowView(entry: entry) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets()) .listRowBackground(AppTheme.background) } .onDelete { store.removeHistoryEntries(at: $0) } } .listStyle(.plain) .scrollContentBackground(.hidden) } } .navigationTitle("History") .toolbar { if !store.historyEntries.isEmpty { ToolbarItem(placement: .navigationBarTrailing) { Button("Clear") { store.clearHistory() } .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) } } } } private var emptyState: some View { VStack(spacing: 14) { Image(systemName: "clock.arrow.circlepath") .font(.system(size: 32, weight: .light)) .foregroundStyle(AppTheme.inkQuaternary) VStack(spacing: 5) { Text("No backups yet") .font(.system(size: 15, weight: .medium)) .foregroundStyle(AppTheme.inkSecondary) Text("Your backup history will appear here") .font(AppTheme.caption()) .foregroundStyle(AppTheme.inkTertiary) } } } } // MARK: — Row struct HistoryRowView: View { let entry: BackupHistoryEntry private var hasErrors: Bool { entry.failedCount > 0 } private var statusColor: Color { hasErrors ? AppTheme.destructive : AppTheme.positive } var body: some View { VStack(spacing: 0) { HStack(alignment: .top, spacing: 14) { // Status indicator Circle() .fill(statusColor) .frame(width: 6, height: 6) .padding(.top, 6) // Content VStack(alignment: .leading, spacing: 5) { // Date + badges HStack(alignment: .firstTextBaseline, spacing: 6) { Text(entry.date, format: .dateTime.month(.abbreviated).day().year()) .font(.system(size: 14, weight: .medium)) .foregroundStyle(AppTheme.ink) Text(entry.date, format: .dateTime.hour().minute()) .font(.system(size: 13, weight: .regular)) .foregroundStyle(AppTheme.inkTertiary) Spacer() if entry.triggeredByLAN { Text("AUTO") .font(.system(size: 9, weight: .semibold)) .foregroundStyle(AppTheme.inkSecondary) .padding(.horizontal, 5) .padding(.vertical, 2) .overlay( RoundedRectangle(cornerRadius: 3, style: .continuous) .stroke(AppTheme.inkQuaternary, lineWidth: 0.75) ) } } // Stat line statLine // Secondary info: host · duration · size HStack(spacing: 6) { Text(entry.nasHost) .foregroundStyle(AppTheme.inkQuaternary) midDot Text(formatDuration(entry.durationSeconds)) .foregroundStyle(AppTheme.inkQuaternary) if entry.totalBytes > 0 { midDot Text(formatBytes(entry.totalBytes)) .foregroundStyle(AppTheme.inkQuaternary) } } .font(.system(size: 11, weight: .regular)) } } .padding(.horizontal, 16) .padding(.vertical, 14) Rectangle() .fill(AppTheme.separator) .frame(height: 0.5) .padding(.leading, 36) } } @ViewBuilder private var statLine: some View { HStack(spacing: 6) { Text("\(entry.uploadedCount) uploaded") .foregroundStyle(AppTheme.inkSecondary) if entry.skippedCount > 0 { midDot Text("\(entry.skippedCount) skipped") .foregroundStyle(AppTheme.inkTertiary) } if hasErrors { midDot Text("\(entry.failedCount) errors") .foregroundStyle(AppTheme.destructive) } } .font(AppTheme.caption()) } private var midDot: some View { Circle() .fill(AppTheme.inkQuaternary) .frame(width: 3, height: 3) } private func formatDuration(_ s: Double) -> String { let m = Int(s) / 60 let sec = Int(s) % 60 return m > 0 ? "\(m)m \(sec)s" : "\(sec)s" } private func formatBytes(_ bytes: Int64) -> String { let gb = Double(bytes) / 1_073_741_824 if gb >= 1 { return String(format: "%.1f GB", gb) } let mb = Double(bytes) / 1_048_576 if mb >= 1 { return String(format: "%.0f MB", mb) } return String(format: "%.0f KB", Double(bytes) / 1024) } }