- Remove #if DEBUG from all debug settings, exporters, and IAP bypass so debug options are available in TestFlight builds - Weekly digest card: replace dismiss X with collapsible chevron caret - Weekly digest: generate on-demand when opening Insights tab if no cached digest exists (BGTask + notification kept as bonus path) - Fix digest intention text color (was .secondary, now uses theme textColor) - Add "Generate Weekly Digest" debug button in Settings - Add generating overlay on Insights tab with pulsing sparkles icon that stays visible until all sections finish loading (content at 0.2 opacity) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
5.0 KiB
Swift
139 lines
5.0 KiB
Swift
//
|
|
// WeeklyDigestCardView.swift
|
|
// Reflect
|
|
//
|
|
// Displays the AI-generated weekly emotional digest card in the Insights tab.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct WeeklyDigestCardView: View {
|
|
|
|
let digest: WeeklyDigest
|
|
|
|
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
|
|
|
private var textColor: Color { theme.currentTheme.labelColor }
|
|
private var accentColor: Color { moodTint.color(forMood: .good) }
|
|
|
|
@State private var isExpanded = true
|
|
@State private var appeared = false
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Header — always visible, tappable to toggle
|
|
Button {
|
|
withAnimation(.easeInOut(duration: 0.25)) {
|
|
isExpanded.toggle()
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: digest.iconName)
|
|
.font(.title2)
|
|
.foregroundStyle(accentColor)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(String(localized: "Weekly Digest"))
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(.secondary)
|
|
.textCase(.uppercase)
|
|
|
|
Text(digest.headline)
|
|
.font(.headline)
|
|
.foregroundColor(textColor)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.up")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
.rotationEffect(.degrees(isExpanded ? 0 : 180))
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityIdentifier(AccessibilityID.WeeklyDigest.dismissButton)
|
|
|
|
// Expandable content
|
|
if isExpanded {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
// Summary
|
|
Text(digest.summary)
|
|
.font(.subheadline)
|
|
.foregroundColor(textColor)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
|
|
Divider()
|
|
|
|
// Highlight
|
|
HStack(alignment: .top, spacing: 10) {
|
|
Image(systemName: "star.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.yellow)
|
|
.padding(.top, 2)
|
|
|
|
Text(digest.highlight)
|
|
.font(.subheadline)
|
|
.foregroundColor(textColor)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
|
|
// Intention
|
|
HStack(alignment: .top, spacing: 10) {
|
|
Image(systemName: "arrow.right.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(accentColor)
|
|
.padding(.top, 2)
|
|
|
|
Text(digest.intention)
|
|
.font(.subheadline)
|
|
.foregroundColor(textColor)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
|
|
// Date range
|
|
Text(dateRangeString)
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
.padding(.top, 16)
|
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
}
|
|
}
|
|
.padding(20)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.fill(Color(.secondarySystemBackground))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 20)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [accentColor.opacity(0.3), .purple.opacity(0.2)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
),
|
|
lineWidth: 1
|
|
)
|
|
)
|
|
)
|
|
.padding(.horizontal)
|
|
.opacity(appeared ? 1 : 0)
|
|
.offset(y: appeared ? 0 : 10)
|
|
.onAppear {
|
|
withAnimation(.easeOut(duration: 0.4)) {
|
|
appeared = true
|
|
}
|
|
}
|
|
.accessibilityIdentifier(AccessibilityID.WeeklyDigest.card)
|
|
}
|
|
|
|
private var dateRangeString: String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
return "\(formatter.string(from: digest.weekStartDate)) - \(formatter.string(from: digest.weekEndDate))"
|
|
}
|
|
}
|