Near-monochrome palette (ink tokens), black primary buttons, spring animations, 0.5pt hairline dividers, shadow cards, squircle corners. Replaces all blue/coloured accents. Adds ButtonStyles component. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
4.4 KiB
Swift
125 lines
4.4 KiB
Swift
import SwiftUI
|
|
|
|
struct HistoryView: View {
|
|
@EnvironmentObject var store: ConnectionStore
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.white.ignoresSafeArea()
|
|
|
|
if store.historyEntries.isEmpty {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "clock.arrow.circlepath")
|
|
.font(.system(size: 36, weight: .light))
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
Text("No backup history yet")
|
|
.font(AppTheme.body())
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
}
|
|
} else {
|
|
List {
|
|
ForEach(store.historyEntries) { entry in
|
|
HistoryRowView(entry: entry)
|
|
.listRowSeparator(.hidden)
|
|
.listRowInsets(EdgeInsets())
|
|
}
|
|
.onDelete { offsets in
|
|
store.removeHistoryEntries(at: offsets)
|
|
}
|
|
}
|
|
.listStyle(.plain)
|
|
}
|
|
}
|
|
.navigationTitle("History")
|
|
.toolbar {
|
|
if !store.historyEntries.isEmpty {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button("Clear") { store.clearHistory() }
|
|
.font(AppTheme.caption())
|
|
.foregroundStyle(AppTheme.destructive)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct HistoryRowView: View {
|
|
let entry: BackupHistoryEntry
|
|
|
|
private var hasErrors: Bool { entry.failedCount > 0 }
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
HStack(alignment: .top, spacing: 14) {
|
|
Circle()
|
|
.fill(hasErrors ? AppTheme.destructive : AppTheme.positive)
|
|
.frame(width: 7, height: 7)
|
|
.padding(.top, 5)
|
|
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
Text(entry.date, style: .date)
|
|
.font(.system(size: 15, weight: .medium))
|
|
.foregroundStyle(AppTheme.ink)
|
|
|
|
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)
|
|
)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Text(entry.date, style: .time)
|
|
.font(AppTheme.micro(11))
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
}
|
|
|
|
HStack(spacing: 8) {
|
|
Text("\(entry.uploadedCount) uploaded")
|
|
.font(AppTheme.caption())
|
|
.foregroundStyle(AppTheme.inkSecondary)
|
|
|
|
if entry.skippedCount > 0 {
|
|
dot
|
|
Text("\(entry.skippedCount) skipped")
|
|
.font(AppTheme.caption())
|
|
.foregroundStyle(AppTheme.inkTertiary)
|
|
}
|
|
|
|
if entry.failedCount > 0 {
|
|
dot
|
|
Text("\(entry.failedCount) errors")
|
|
.font(AppTheme.caption())
|
|
.foregroundStyle(AppTheme.destructive)
|
|
}
|
|
}
|
|
|
|
Text(entry.nasHost)
|
|
.font(AppTheme.micro(11))
|
|
.foregroundStyle(AppTheme.inkQuaternary)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 14)
|
|
|
|
Rectangle()
|
|
.fill(AppTheme.separator)
|
|
.frame(height: 0.5)
|
|
.padding(.leading, 37)
|
|
}
|
|
}
|
|
|
|
private var dot: some View {
|
|
Circle()
|
|
.fill(AppTheme.inkQuaternary)
|
|
.frame(width: 3, height: 3)
|
|
}
|
|
}
|