- 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>
363 lines
11 KiB
Swift
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)))
|
|
}
|
|
}
|