Files
Reflect/Shared/Services/ExportableWatchViews.swift
Trey t 329fb7c671 Remove #if DEBUG guards for TestFlight, polish weekly digest and insights UX
- 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>
2026-04-04 11:15:23 -05:00

363 lines
11 KiB
Swift

//
// ExportableWatchViews.swift
// Reflect
//
// Exportable watch views that match the real watchOS layouts.
// These views accept tint/icon configuration as parameters for batch export.
//
import SwiftUI
// MARK: - Watch Export Configuration
/// Configuration for watch view export styling
struct WatchExportConfig {
let moodTint: MoodTintable.Type
let moodImages: MoodImagable.Type
/// Get emoji for a mood based on the image style
func emoji(for mood: Mood) -> String {
// Map MoodImagable type to WatchMoodImageStyle equivalent
switch String(describing: moodImages) {
case "FontAwesomeMoodImages":
return fontAwesomeEmoji(for: mood)
case "EmojiMoodImages":
return emojiStyle(for: mood)
case "HandEmojiMoodImages":
return handEmoji(for: mood)
case "WeatherMoodImages":
return weatherEmoji(for: mood)
case "GardenMoodImages":
return gardenEmoji(for: mood)
case "HeartsMoodImages":
return heartsEmoji(for: mood)
case "CosmicMoodImages":
return cosmicEmoji(for: mood)
default:
return emojiStyle(for: mood)
}
}
private func fontAwesomeEmoji(for mood: Mood) -> String {
switch mood {
case .great: return "😁"
case .good: return "🙂"
case .average: return "😐"
case .bad: return "🙁"
case .horrible: return "😫"
case .missing, .placeholder: return ""
}
}
private func emojiStyle(for mood: Mood) -> String {
switch mood {
case .great: return "😀"
case .good: return "🙂"
case .average: return "😑"
case .bad: return "😕"
case .horrible: return "💩"
case .missing, .placeholder: return ""
}
}
private func handEmoji(for mood: Mood) -> String {
switch mood {
case .great: return "🙏"
case .good: return "👍"
case .average: return "🖖"
case .bad: return "👎"
case .horrible: return "🖕"
case .missing, .placeholder: return ""
}
}
private func weatherEmoji(for mood: Mood) -> String {
switch mood {
case .great: return "☀️"
case .good: return ""
case .average: return "☁️"
case .bad: return "🌧️"
case .horrible: return "⛈️"
case .missing: return "🌫️"
case .placeholder: return ""
}
}
private func gardenEmoji(for mood: Mood) -> String {
switch mood {
case .great: return "🌸"
case .good: return "🌿"
case .average: return "🌱"
case .bad: return "🍂"
case .horrible: return "🥀"
case .missing: return "🕳️"
case .placeholder: return ""
}
}
private func heartsEmoji(for mood: Mood) -> String {
switch mood {
case .great: return "💖"
case .good: return "🩷"
case .average: return "🤍"
case .bad: return "🩶"
case .horrible: return "💔"
case .missing: return "🖤"
case .placeholder: return ""
}
}
private func cosmicEmoji(for mood: Mood) -> String {
switch mood {
case .great: return ""
case .good: return "🌕"
case .average: return "🌓"
case .bad: return "🌑"
case .horrible: return "🕳️"
case .missing: return ""
case .placeholder: return ""
}
}
}
// MARK: - Exportable Watch Voting View
/// Watch voting interface - matches ContentView from watch app
struct ExportableWatchVotingView: View {
let config: WatchExportConfig
var body: some View {
VStack(spacing: 8) {
Text("How do you feel?")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.secondary)
// Top row: Great, Good, Average
HStack(spacing: 8) {
ExportableWatchMoodButton(mood: .great, config: config)
ExportableWatchMoodButton(mood: .good, config: config)
ExportableWatchMoodButton(mood: .average, config: config)
}
// Bottom row: Bad, Horrible
HStack(spacing: 8) {
ExportableWatchMoodButton(mood: .bad, config: config)
ExportableWatchMoodButton(mood: .horrible, config: config)
}
}
}
}
// MARK: - Exportable Watch Mood Button
struct ExportableWatchMoodButton: View {
let mood: Mood
let config: WatchExportConfig
var body: some View {
Text(config.emoji(for: mood))
.font(.system(size: 28))
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(config.moodTint.color(forMood: mood).opacity(0.3))
.cornerRadius(12)
}
}
// MARK: - Exportable Watch Already Rated View
struct ExportableWatchAlreadyRatedView: View {
let mood: Mood
let config: WatchExportConfig
var body: some View {
VStack(spacing: 12) {
Text(config.emoji(for: mood))
.font(.system(size: 50))
Text("Logged!")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.secondary)
}
}
}
// MARK: - Exportable Circular Complication
struct ExportableCircularComplication: View {
let mood: Mood?
let config: WatchExportConfig
var body: some View {
ZStack {
Circle()
.fill(Color(white: 0.15))
if let mood = mood {
Text(config.emoji(for: mood))
.font(.system(size: 24))
} else {
VStack(spacing: 0) {
Image(systemName: "face.smiling")
.font(.system(size: 18))
Text("Log")
.font(.system(size: 10))
}
}
}
}
}
// MARK: - Exportable Corner Complication
struct ExportableCornerComplication: View {
let mood: Mood?
let config: WatchExportConfig
var body: some View {
HStack(spacing: 4) {
if let mood = mood {
Text(config.emoji(for: mood))
.font(.system(size: 20))
Text(mood.widgetDisplayName)
.font(.system(size: 12))
.foregroundColor(.secondary)
} else {
Image(systemName: "face.smiling")
.font(.system(size: 20))
Text("Log mood")
.font(.system(size: 12))
.foregroundColor(.secondary)
}
}
}
}
// MARK: - Exportable Inline Complication
struct ExportableInlineComplication: View {
let mood: Mood?
let streak: Int
let config: WatchExportConfig
var body: some View {
HStack(spacing: 4) {
if streak > 0 {
Image(systemName: "flame.fill")
.foregroundColor(.orange)
Text("\(streak) day streak")
} else if let mood = mood {
Text("\(config.emoji(for: mood)) \(mood.widgetDisplayName)")
} else {
Image(systemName: "face.smiling")
Text("Log your mood")
}
}
.font(.system(size: 14))
}
}
// MARK: - Exportable Rectangular Complication
struct ExportableRectangularComplication: View {
let mood: Mood?
let streak: Int
let config: WatchExportConfig
var body: some View {
HStack {
if let mood = mood {
Text(config.emoji(for: mood))
.font(.system(size: 28))
VStack(alignment: .leading, spacing: 2) {
Text("Today")
.font(.system(size: 12))
.foregroundColor(.secondary)
Text(mood.widgetDisplayName)
.font(.system(size: 14, weight: .semibold))
if streak > 1 {
HStack(spacing: 2) {
Image(systemName: "flame.fill")
Text("\(streak) days")
}
.font(.system(size: 10))
.foregroundColor(.orange)
}
}
} else {
Image(systemName: "face.smiling")
.font(.system(size: 24))
VStack(alignment: .leading, spacing: 2) {
Text("Reflect")
.font(.system(size: 14, weight: .semibold))
Text("Tap to log mood")
.font(.system(size: 12))
.foregroundColor(.secondary)
}
}
Spacer()
}
}
}
// MARK: - Watch Container for Export
struct ExportableWatchContainer<Content: View>: View {
let width: CGFloat
let height: CGFloat
let colorScheme: ColorScheme
let cornerRadius: CGFloat
let content: Content
init(width: CGFloat, height: CGFloat, colorScheme: ColorScheme, cornerRadius: CGFloat = 20, @ViewBuilder content: () -> Content) {
self.width = width
self.height = height
self.colorScheme = colorScheme
self.cornerRadius = cornerRadius
self.content = content()
}
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color(red: 0.95, green: 0.95, blue: 0.97)
}
var body: some View {
content
.environment(\.colorScheme, colorScheme)
.frame(width: width, height: height)
.background(backgroundColor)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
}
}
// MARK: - Watch Complication Container
struct ExportableComplicationContainer<Content: View>: View {
let size: CGSize
let colorScheme: ColorScheme
let isCircular: Bool
let content: Content
init(size: CGSize, colorScheme: ColorScheme, isCircular: Bool = false, @ViewBuilder content: () -> Content) {
self.size = size
self.colorScheme = colorScheme
self.isCircular = isCircular
self.content = content()
}
private var backgroundColor: Color {
colorScheme == .dark ? Color(white: 0.1) : Color(white: 0.95)
}
var body: some View {
content
.environment(\.colorScheme, colorScheme)
.frame(width: size.width, height: size.height)
.background(backgroundColor)
.clipShape(isCircular ? AnyShape(Circle()) : AnyShape(RoundedRectangle(cornerRadius: 12, style: .continuous)))
}
}