|
@@ -0,0 +1,165 @@
|
|
|
|
|
+import SwiftUI
|
|
|
|
|
+
|
|
|
|
|
+struct HistoryView: View {
|
|
|
|
|
+ @StateObject private var viewModel = HistoryViewModel()
|
|
|
|
|
+ let onOpenEntry: (HistoryEntry) -> Void
|
|
|
|
|
+
|
|
|
|
|
+ var body: some View {
|
|
|
|
|
+ GeometryReader { proxy in
|
|
|
|
|
+ let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
|
|
|
|
|
+ let dynamicTopPadding = min(max(proxy.size.height * 0.015, 8), 16)
|
|
|
|
|
+ let dynamicBottomPadding = min(max(proxy.size.height * 0.04, 20), 40)
|
|
|
|
|
+
|
|
|
|
|
+ ScrollView(.vertical, showsIndicators: false) {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 0) {
|
|
|
|
|
+ header
|
|
|
|
|
+ .padding(.bottom, 24)
|
|
|
|
|
+
|
|
|
|
|
+ if viewModel.isEmpty {
|
|
|
|
|
+ emptyState
|
|
|
|
|
+ } else {
|
|
|
|
|
+ LazyVStack(spacing: 12) {
|
|
|
|
|
+ ForEach(viewModel.entries) { entry in
|
|
|
|
|
+ historyRow(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: 900, alignment: .topLeading)
|
|
|
|
|
+ .frame(maxWidth: .infinity, alignment: .topLeading)
|
|
|
|
|
+ .padding(.horizontal, dynamicHorizontalPadding)
|
|
|
|
|
+ .padding(.top, dynamicTopPadding)
|
|
|
|
|
+ .padding(.bottom, dynamicBottomPadding)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .confirmationDialog(
|
|
|
|
|
+ "Clear all history?",
|
|
|
|
|
+ isPresented: $viewModel.isShowingClearConfirmation,
|
|
|
|
|
+ titleVisibility: .visible
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Button("Clear All", role: .destructive) {
|
|
|
|
|
+ viewModel.clearAll()
|
|
|
|
|
+ }
|
|
|
|
|
+ Button("Cancel", role: .cancel) {}
|
|
|
|
|
+ } message: {
|
|
|
|
|
+ Text("This will permanently remove all saved history items.")
|
|
|
|
|
+ }
|
|
|
|
|
+ .confirmationDialog(
|
|
|
|
|
+ "Delete this history item?",
|
|
|
|
|
+ isPresented: Binding(
|
|
|
|
|
+ get: { viewModel.entryPendingDeletion != nil },
|
|
|
|
|
+ set: { if !$0 { viewModel.entryPendingDeletion = nil } }
|
|
|
|
|
+ ),
|
|
|
|
|
+ titleVisibility: .visible
|
|
|
|
|
+ ) {
|
|
|
|
|
+ Button("Delete", role: .destructive) {
|
|
|
|
|
+ viewModel.performPendingDeletion()
|
|
|
|
|
+ }
|
|
|
|
|
+ Button("Cancel", role: .cancel) {
|
|
|
|
|
+ viewModel.entryPendingDeletion = nil
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var header: some View {
|
|
|
|
|
+ HStack(alignment: .top) {
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 8) {
|
|
|
|
|
+ Text("History")
|
|
|
|
|
+ .font(.system(size: 30, weight: .bold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ Text("Review, reopen, or remove your past tool results.")
|
|
|
|
|
+ .font(.system(size: 15))
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Spacer()
|
|
|
|
|
+
|
|
|
|
|
+ if !viewModel.isEmpty {
|
|
|
|
|
+ ClearButton {
|
|
|
|
|
+ viewModel.isShowingClearConfirmation = true
|
|
|
|
|
+ }
|
|
|
|
|
+ .accessibilityLabel("Clear all history")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var emptyState: some View {
|
|
|
|
|
+ VStack(spacing: 14) {
|
|
|
|
|
+ Image(systemName: "clock")
|
|
|
|
|
+ .font(.system(size: 40))
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal.opacity(0.45))
|
|
|
|
|
+
|
|
|
|
|
+ Text("No history yet")
|
|
|
|
|
+ .font(.system(size: 20, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+
|
|
|
|
|
+ Text("Results from tools like Grammar Checker and Journal Finder will appear here.")
|
|
|
|
|
+ .font(.system(size: 14))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ .multilineTextAlignment(.center)
|
|
|
|
|
+ .frame(maxWidth: 360)
|
|
|
|
|
+ }
|
|
|
|
|
+ .frame(maxWidth: .infinity)
|
|
|
|
|
+ .padding(.vertical, 80)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private func historyRow(_ entry: HistoryEntry) -> some View {
|
|
|
|
|
+ HStack(alignment: .top, spacing: 14) {
|
|
|
|
|
+ ZStack {
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
|
|
|
+ .fill(AppTheme.tealLight)
|
|
|
|
|
+ .frame(width: 40, height: 40)
|
|
|
|
|
+
|
|
|
|
|
+ Image(systemName: entry.tool.iconName)
|
|
|
|
|
+ .font(.system(size: 16, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ VStack(alignment: .leading, spacing: 6) {
|
|
|
|
|
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
|
|
|
|
|
+ Text(entry.title)
|
|
|
|
|
+ .font(.system(size: 15, weight: .semibold))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textPrimary)
|
|
|
|
|
+ .lineLimit(1)
|
|
|
|
|
+
|
|
|
|
|
+ Spacer(minLength: 8)
|
|
|
|
|
+
|
|
|
|
|
+ Text(viewModel.formattedDate(for: entry))
|
|
|
|
|
+ .font(.system(size: 12))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textMuted)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Text(entry.tool.title)
|
|
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
|
|
+ .foregroundStyle(AppTheme.teal)
|
|
|
|
|
+
|
|
|
|
|
+ Text(entry.preview)
|
|
|
|
|
+ .font(.system(size: 13))
|
|
|
|
|
+ .foregroundStyle(AppTheme.textSecondary)
|
|
|
|
|
+ .lineLimit(2)
|
|
|
|
|
+ .fixedSize(horizontal: false, vertical: true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ HStack(spacing: 4) {
|
|
|
|
|
+ IconButton(iconName: "arrow.up.right.square") {
|
|
|
|
|
+ onOpenEntry(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ .accessibilityLabel("Open history item")
|
|
|
|
|
+
|
|
|
|
|
+ IconButton(iconName: "trash") {
|
|
|
|
|
+ viewModel.confirmDelete(entry)
|
|
|
|
|
+ }
|
|
|
|
|
+ .accessibilityLabel("Delete history item")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .padding(16)
|
|
|
|
|
+ .background(AppTheme.cardBackground)
|
|
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius, style: .continuous))
|
|
|
|
|
+ .overlay(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius, style: .continuous)
|
|
|
|
|
+ .stroke(AppTheme.border.opacity(0.6), lineWidth: 1)
|
|
|
|
|
+ )
|
|
|
|
|
+ .shadow(color: AppTheme.cardShadow, radius: 8, y: 2)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|