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
|
||||||
250
Shared/Services/WatchExporter.swift
Normal file
250
Shared/Services/WatchExporter.swift
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
//
|
||||||
|
// WatchExporter.swift
|
||||||
|
// Feels
|
||||||
|
//
|
||||||
|
// Debug utility to export all watch view previews to PNG files.
|
||||||
|
// Uses the exportable watch views from ExportableWatchViews.swift.
|
||||||
|
//
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Exports watch view previews to PNG files for App Store screenshots
|
||||||
|
@MainActor
|
||||||
|
class WatchExporter {
|
||||||
|
|
||||||
|
// MARK: - Watch Sizes (Apple Watch Series 9 45mm @ 2x)
|
||||||
|
|
||||||
|
/// Main watch app screen size (45mm watch)
|
||||||
|
static let watchAppSize = CGSize(width: 198, height: 242)
|
||||||
|
|
||||||
|
/// Complication sizes
|
||||||
|
enum ComplicationSize {
|
||||||
|
case circular // 50x50 pt
|
||||||
|
case corner // 40x40 pt content area
|
||||||
|
case inline // 230x26 pt
|
||||||
|
case rectangular // 180x70 pt
|
||||||
|
|
||||||
|
var pointSize: CGSize {
|
||||||
|
switch self {
|
||||||
|
case .circular: return CGSize(width: 50, height: 50)
|
||||||
|
case .corner: return CGSize(width: 100, height: 40)
|
||||||
|
case .inline: return CGSize(width: 230, height: 26)
|
||||||
|
case .rectangular: return CGSize(width: 180, height: 70)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String {
|
||||||
|
switch self {
|
||||||
|
case .circular: return "circular"
|
||||||
|
case .corner: return "corner"
|
||||||
|
case .inline: return "inline"
|
||||||
|
case .rectangular: return "rectangular"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Available Theme Combinations (same as WidgetExporter)
|
||||||
|
|
||||||
|
/// All available tint options for export
|
||||||
|
static let allTints: [(name: String, tint: MoodTintable.Type)] = [
|
||||||
|
("Default", DefaultMoodTint.self),
|
||||||
|
("Neon", NeonMoodTint.self),
|
||||||
|
("Pastel", PastelTint.self),
|
||||||
|
("Monochrome", MonoChromeTint.self)
|
||||||
|
]
|
||||||
|
|
||||||
|
/// All available icon options for export
|
||||||
|
static let allIcons: [(name: String, images: MoodImagable.Type)] = [
|
||||||
|
("Emoji", EmojiMoodImages.self),
|
||||||
|
("FontAwesome", FontAwesomeMoodImages.self),
|
||||||
|
("Weather", WeatherMoodImages.self),
|
||||||
|
("Garden", GardenMoodImages.self),
|
||||||
|
("Hearts", HeartsMoodImages.self),
|
||||||
|
("Cosmic", CosmicMoodImages.self),
|
||||||
|
("HandEmoji", HandEmojiMoodImages.self)
|
||||||
|
]
|
||||||
|
|
||||||
|
// MARK: - Export All Watch Views
|
||||||
|
|
||||||
|
/// Exports all watch view variations to disk
|
||||||
|
/// - Returns: URL to the export directory, or nil if failed
|
||||||
|
static func exportAllWatchViews() async -> URL? {
|
||||||
|
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
let exportPath = documentsPath.appendingPathComponent("WatchExports", isDirectory: true)
|
||||||
|
|
||||||
|
// Clean and create export directory
|
||||||
|
try? FileManager.default.removeItem(at: exportPath)
|
||||||
|
try? FileManager.default.createDirectory(at: exportPath, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
var totalExported = 0
|
||||||
|
|
||||||
|
// Export all tint + icon combinations
|
||||||
|
for tintOption in allTints {
|
||||||
|
for iconOption in allIcons {
|
||||||
|
let folderName = "\(tintOption.name)_\(iconOption.name)"
|
||||||
|
let variantPath = exportPath.appendingPathComponent(folderName, isDirectory: true)
|
||||||
|
try? FileManager.default.createDirectory(at: variantPath, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
let config = WatchExportConfig(
|
||||||
|
moodTint: tintOption.tint,
|
||||||
|
moodImages: iconOption.images
|
||||||
|
)
|
||||||
|
|
||||||
|
let count = await exportWatchViewsForConfig(config: config, to: variantPath)
|
||||||
|
totalExported += count
|
||||||
|
print(" Exported \(count) watch images to \(folderName)/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("⌚ Total \(totalExported) watch views exported to: \(exportPath.path)")
|
||||||
|
return exportPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exports watch views for a single tint/icon configuration
|
||||||
|
private static func exportWatchViewsForConfig(config: WatchExportConfig, to folder: URL) async -> Int {
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
for colorScheme in [ColorScheme.light, ColorScheme.dark] {
|
||||||
|
let schemeName = colorScheme == .light ? "light" : "dark"
|
||||||
|
|
||||||
|
// Watch App - Voting View
|
||||||
|
await exportWatchVotingView(config: config, colorScheme: colorScheme, to: folder, name: "watch_voting_\(schemeName)")
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
// Watch App - Already Rated (all moods)
|
||||||
|
for mood in Mood.allValues {
|
||||||
|
await exportWatchAlreadyRatedView(mood: mood, config: config, colorScheme: colorScheme, to: folder, name: "watch_logged_\(schemeName)_\(mood.strValue.lowercased())")
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complications - Empty state
|
||||||
|
await exportCircularComplication(mood: nil, config: config, colorScheme: colorScheme, to: folder, name: "complication_circular_\(schemeName)_empty")
|
||||||
|
await exportCornerComplication(mood: nil, config: config, colorScheme: colorScheme, to: folder, name: "complication_corner_\(schemeName)_empty")
|
||||||
|
await exportInlineComplication(mood: nil, streak: 0, config: config, colorScheme: colorScheme, to: folder, name: "complication_inline_\(schemeName)_empty")
|
||||||
|
await exportRectangularComplication(mood: nil, streak: 0, config: config, colorScheme: colorScheme, to: folder, name: "complication_rectangular_\(schemeName)_empty")
|
||||||
|
count += 4
|
||||||
|
|
||||||
|
// Complications - With streak
|
||||||
|
await exportInlineComplication(mood: nil, streak: 45, config: config, colorScheme: colorScheme, to: folder, name: "complication_inline_\(schemeName)_streak")
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
// Complications - All moods
|
||||||
|
for mood in Mood.allValues {
|
||||||
|
let moodName = mood.strValue.lowercased()
|
||||||
|
await exportCircularComplication(mood: mood, config: config, colorScheme: colorScheme, to: folder, name: "complication_circular_\(schemeName)_\(moodName)")
|
||||||
|
await exportCornerComplication(mood: mood, config: config, colorScheme: colorScheme, to: folder, name: "complication_corner_\(schemeName)_\(moodName)")
|
||||||
|
await exportInlineComplication(mood: mood, streak: 0, config: config, colorScheme: colorScheme, to: folder, name: "complication_inline_\(schemeName)_\(moodName)")
|
||||||
|
await exportRectangularComplication(mood: mood, streak: 45, config: config, colorScheme: colorScheme, to: folder, name: "complication_rectangular_\(schemeName)_\(moodName)")
|
||||||
|
count += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Export Watch App Views
|
||||||
|
|
||||||
|
private static func exportWatchVotingView(config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||||
|
let content = ExportableWatchVotingView(config: config)
|
||||||
|
|
||||||
|
let view = ExportableWatchContainer(
|
||||||
|
width: watchAppSize.width,
|
||||||
|
height: watchAppSize.height,
|
||||||
|
colorScheme: colorScheme
|
||||||
|
) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderAndSave(view: view, size: watchAppSize, to: folder, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func exportWatchAlreadyRatedView(mood: Mood, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||||
|
let content = ExportableWatchAlreadyRatedView(mood: mood, config: config)
|
||||||
|
|
||||||
|
let view = ExportableWatchContainer(
|
||||||
|
width: watchAppSize.width,
|
||||||
|
height: watchAppSize.height,
|
||||||
|
colorScheme: colorScheme
|
||||||
|
) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderAndSave(view: view, size: watchAppSize, to: folder, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Export Complications
|
||||||
|
|
||||||
|
private static func exportCircularComplication(mood: Mood?, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||||
|
let size = ComplicationSize.circular.pointSize
|
||||||
|
let content = ExportableCircularComplication(mood: mood, config: config)
|
||||||
|
|
||||||
|
let view = ExportableComplicationContainer(
|
||||||
|
size: size,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
isCircular: true
|
||||||
|
) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func exportCornerComplication(mood: Mood?, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||||
|
let size = ComplicationSize.corner.pointSize
|
||||||
|
let content = ExportableCornerComplication(mood: mood, config: config)
|
||||||
|
|
||||||
|
let view = ExportableComplicationContainer(
|
||||||
|
size: size,
|
||||||
|
colorScheme: colorScheme
|
||||||
|
) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func exportInlineComplication(mood: Mood?, streak: Int, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||||
|
let size = ComplicationSize.inline.pointSize
|
||||||
|
let content = ExportableInlineComplication(mood: mood, streak: streak, config: config)
|
||||||
|
|
||||||
|
let view = ExportableComplicationContainer(
|
||||||
|
size: size,
|
||||||
|
colorScheme: colorScheme
|
||||||
|
) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func exportRectangularComplication(mood: Mood?, streak: Int, config: WatchExportConfig, colorScheme: ColorScheme, to folder: URL, name: String) async {
|
||||||
|
let size = ComplicationSize.rectangular.pointSize
|
||||||
|
let content = ExportableRectangularComplication(mood: mood, streak: streak, config: config)
|
||||||
|
|
||||||
|
let view = ExportableComplicationContainer(
|
||||||
|
size: size,
|
||||||
|
colorScheme: colorScheme
|
||||||
|
) {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
|
||||||
|
await renderAndSave(view: view, size: size, to: folder, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Render and Save
|
||||||
|
|
||||||
|
private static func renderAndSave<V: View>(view: V, size: CGSize, to folder: URL, name: String) async {
|
||||||
|
let renderer = ImageRenderer(content: view.frame(width: size.width, height: size.height))
|
||||||
|
renderer.scale = 3.0 // 3x for high res
|
||||||
|
|
||||||
|
if let image = renderer.uiImage {
|
||||||
|
let url = folder.appendingPathComponent("\(name).png")
|
||||||
|
if let data = image.pngData() {
|
||||||
|
try? data.write(to: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -21,6 +21,10 @@ struct SettingsContentView: View {
|
|||||||
@State private var showTrialDatePicker = false
|
@State private var showTrialDatePicker = false
|
||||||
@State private var isExportingWidgets = false
|
@State private var isExportingWidgets = false
|
||||||
@State private var widgetExportPath: URL?
|
@State private var widgetExportPath: URL?
|
||||||
|
@State private var isExportingVotingLayouts = false
|
||||||
|
@State private var votingLayoutExportPath: URL?
|
||||||
|
@State private var isExportingWatchViews = false
|
||||||
|
@State private var watchExportPath: URL?
|
||||||
@State private var isDeletingHealthKitData = false
|
@State private var isDeletingHealthKitData = false
|
||||||
@State private var healthKitDeleteResult: String?
|
@State private var healthKitDeleteResult: String?
|
||||||
@StateObject private var healthService = HealthService.shared
|
@StateObject private var healthService = HealthService.shared
|
||||||
@@ -64,6 +68,8 @@ struct SettingsContentView: View {
|
|||||||
tipsPreviewButton
|
tipsPreviewButton
|
||||||
testNotificationsButton
|
testNotificationsButton
|
||||||
exportWidgetsButton
|
exportWidgetsButton
|
||||||
|
exportVotingLayoutsButton
|
||||||
|
exportWatchViewsButton
|
||||||
deleteHealthKitDataButton
|
deleteHealthKitDataButton
|
||||||
|
|
||||||
clearDataButton
|
clearDataButton
|
||||||
@@ -478,6 +484,112 @@ struct SettingsContentView: View {
|
|||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var exportVotingLayoutsButton: some View {
|
||||||
|
ZStack {
|
||||||
|
theme.currentTheme.secondaryBGColor
|
||||||
|
Button {
|
||||||
|
isExportingVotingLayouts = true
|
||||||
|
Task {
|
||||||
|
votingLayoutExportPath = await WidgetExporter.exportAllVotingLayouts()
|
||||||
|
isExportingVotingLayouts = false
|
||||||
|
if let path = votingLayoutExportPath {
|
||||||
|
print("📸 Voting layouts exported to: \(path.path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
if isExportingVotingLayouts {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 32)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "hand.tap.fill")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.blue)
|
||||||
|
.frame(width: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Export Voting Layouts")
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
if let path = votingLayoutExportPath {
|
||||||
|
Text("Saved to Documents/VotingLayoutExports")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
} else {
|
||||||
|
Text("All sizes & theme variations")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "arrow.down.doc.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.disabled(isExportingVotingLayouts)
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
|
}
|
||||||
|
|
||||||
|
private var exportWatchViewsButton: some View {
|
||||||
|
ZStack {
|
||||||
|
theme.currentTheme.secondaryBGColor
|
||||||
|
Button {
|
||||||
|
isExportingWatchViews = true
|
||||||
|
Task {
|
||||||
|
watchExportPath = await WatchExporter.exportAllWatchViews()
|
||||||
|
isExportingWatchViews = false
|
||||||
|
if let path = watchExportPath {
|
||||||
|
print("⌚ Watch views exported to: \(path.path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
if isExportingWatchViews {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 32)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "applewatch.watchface")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.cyan)
|
||||||
|
.frame(width: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Export Watch Screenshots")
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
if let path = watchExportPath {
|
||||||
|
Text("Saved to Documents/WatchExports")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
} else {
|
||||||
|
Text("All styles & complications")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: "arrow.down.doc.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.disabled(isExportingWatchViews)
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
|
}
|
||||||
|
|
||||||
private var deleteHealthKitDataButton: some View {
|
private var deleteHealthKitDataButton: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
|
|||||||
BIN
feels-promo/public/watch_voting_light.png
Normal file
BIN
feels-promo/public/watch_voting_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
@@ -139,7 +139,7 @@ const ProblemScene: React.FC = () => {
|
|||||||
// Scene 2: Single tap voting - Widget, Watch, In-App
|
// Scene 2: Single tap voting - Widget, Watch, In-App
|
||||||
const SingleTapScene: React.FC = () => {
|
const SingleTapScene: React.FC = () => {
|
||||||
const frame = useCurrentFrame();
|
const frame = useCurrentFrame();
|
||||||
const { fps, width } = useVideoConfig();
|
const { fps, width, height } = useVideoConfig();
|
||||||
|
|
||||||
// Title animations (matching other scenes)
|
// Title animations (matching other scenes)
|
||||||
const line1Progress = spring({ frame, fps, config: { damping: 200 } });
|
const line1Progress = spring({ frame, fps, config: { damping: 200 } });
|
||||||
@@ -174,7 +174,7 @@ const SingleTapScene: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const watchSize = width * 0.27;
|
const watchSize = width * 0.27 * 1.2;
|
||||||
const widgetSize = width * 0.33;
|
const widgetSize = width * 0.33;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -239,172 +239,165 @@ const SingleTapScene: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Three device rows - full width, image at 33%, label at 66% */}
|
{/* Watch row - top at 33% */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
top: "33%",
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
top: 450,
|
|
||||||
bottom: 80,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
alignItems: "center",
|
||||||
justifyContent: "space-evenly",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Watch row */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
position: "absolute",
|
||||||
flexDirection: "row",
|
left: "33%",
|
||||||
alignItems: "center",
|
transform: "translateX(-50%)",
|
||||||
width: "100%",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Image at 33% */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "relative",
|
||||||
left: "33%",
|
width: watchSize,
|
||||||
transform: "translateX(-50%)",
|
height: watchSize,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<Img
|
||||||
|
src={staticFile("watch_voting_light.png")}
|
||||||
style={{
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: "48%",
|
||||||
|
height: "auto",
|
||||||
|
borderRadius: 8,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Img
|
||||||
|
src={staticFile("watch.png")}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "contain",
|
||||||
|
filter: "drop-shadow(0 10px 30px rgba(0,0,0,0.3))",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: watchSize,
|
zIndex: 2,
|
||||||
height: watchSize,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={staticFile("watch.png")}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "contain",
|
|
||||||
filter: "drop-shadow(0 10px 30px rgba(0,0,0,0.3))",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Img
|
|
||||||
src={staticFile("vote_light_small_notvoted.png")}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
width: "62%",
|
|
||||||
height: "auto",
|
|
||||||
borderRadius: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Label at 66% */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "66%",
|
|
||||||
fontSize: 58,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: "white",
|
|
||||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
||||||
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
|
||||||
opacity: interpolate(watchLabelProgress, [0, 1], [0, 1]),
|
|
||||||
transform: `translateX(${interpolate(watchLabelProgress, [0, 1], [30, 0])}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Watch
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* In-App row */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Image at 33% */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "33%",
|
|
||||||
transform: "translateX(-50%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Img
|
|
||||||
src={staticFile("voting_header.png")}
|
|
||||||
style={{
|
|
||||||
width: widgetSize * 1.1,
|
|
||||||
height: "auto",
|
|
||||||
borderRadius: 24,
|
|
||||||
boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Label at 66% */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: "66%",
|
|
||||||
fontSize: 58,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: "white",
|
|
||||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
||||||
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
|
||||||
opacity: interpolate(inAppLabelProgress, [0, 1], [0, 1]),
|
|
||||||
transform: `translateX(${interpolate(inAppLabelProgress, [0, 1], [30, 0])}px)`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
In-App
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Widget row */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
position: "absolute",
|
||||||
flexDirection: "row",
|
left: "66%",
|
||||||
alignItems: "center",
|
fontSize: 58,
|
||||||
width: "100%",
|
fontWeight: 700,
|
||||||
|
color: "white",
|
||||||
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||||
|
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
||||||
|
opacity: interpolate(watchLabelProgress, [0, 1], [0, 1]),
|
||||||
|
transform: `translateX(${interpolate(watchLabelProgress, [0, 1], [30, 0])}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Image at 33% */}
|
Watch
|
||||||
<div
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* In-App row - centered at 61.5% (midpoint of 33% and 90%) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "61.5%",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "33%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src={staticFile("voting_light_large.png")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
width: widgetSize * 1.1,
|
||||||
left: "33%",
|
height: "auto",
|
||||||
transform: "translateX(-50%)",
|
borderRadius: 24,
|
||||||
|
boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Img
|
</div>
|
||||||
src={staticFile("vote_dark_medium_notvoted.png")}
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: widgetSize,
|
position: "absolute",
|
||||||
height: "auto",
|
left: "66%",
|
||||||
borderRadius: 24,
|
fontSize: 58,
|
||||||
boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
|
fontWeight: 700,
|
||||||
}}
|
color: "white",
|
||||||
/>
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||||
</div>
|
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
||||||
{/* Label at 66% */}
|
opacity: interpolate(inAppLabelProgress, [0, 1], [0, 1]),
|
||||||
<div
|
transform: `translateX(${interpolate(inAppLabelProgress, [0, 1], [30, 0])}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
In-App
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Widget row - bottom at 90% (10% from bottom) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "10%",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "33%",
|
||||||
|
bottom: 0,
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Img
|
||||||
|
src={staticFile("voting_light_medium.png")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
width: widgetSize,
|
||||||
left: "66%",
|
height: "auto",
|
||||||
fontSize: 58,
|
borderRadius: 24,
|
||||||
fontWeight: 700,
|
boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
|
||||||
color: "white",
|
|
||||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
||||||
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
|
||||||
opacity: interpolate(widgetLabelProgress, [0, 1], [0, 1]),
|
|
||||||
transform: `translateX(${interpolate(widgetLabelProgress, [0, 1], [30, 0])}px)`,
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
Widget
|
</div>
|
||||||
</div>
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "66%",
|
||||||
|
bottom: 0,
|
||||||
|
height: widgetSize * 0.47,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontSize: 58,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "white",
|
||||||
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||||
|
textShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
||||||
|
opacity: interpolate(widgetLabelProgress, [0, 1], [0, 1]),
|
||||||
|
transform: `translateX(${interpolate(widgetLabelProgress, [0, 1], [30, 0])}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Widget
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user