Add watch view export and update promo video watch display
- Add ExportableWatchViews.swift with exportable versions of watch app views - Add WatchExporter.swift service to export all watch view variations - Add export watch screenshots button to Settings (DEBUG) - Update ConceptB promo: use watch_voting_light.png inside watch frame at 1.2x size Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
365
Shared/Services/ExportableWatchViews.swift
Normal file
365
Shared/Services/ExportableWatchViews.swift
Normal file
@@ -0,0 +1,365 @@
|
||||
//
|
||||
// ExportableWatchViews.swift
|
||||
// Feels
|
||||
//
|
||||
// Exportable watch views that match the real watchOS layouts.
|
||||
// These views accept tint/icon configuration as parameters for batch export.
|
||||
//
|
||||
|
||||
#if DEBUG
|
||||
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("Feels")
|
||||
.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)))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user