Add sharing style picker for design variation selection
Users can now swipe between design variations (e.g. Gradient vs Color Block) when sharing from month/year views and the sharing templates list. Removes #if DEBUG wrappers from variation files and disables auto-start demo animation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// AllMoodsVariations.swift
|
||||
// Feels
|
||||
//
|
||||
// 3 design variations for the All Moods Total sharing template.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - V2 "Gradient" — Warm gradient, rounded cards, bold
|
||||
|
||||
struct AllMoodsV2: View {
|
||||
let metrics: [MoodMetrics]
|
||||
let totalCount: Int
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
ZStack {
|
||||
// Gradient bg
|
||||
LinearGradient(
|
||||
colors: [Color(hex: "FFF5EE"), Color(hex: "FFE4D6"), Color(hex: "FFD4C2")],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer().frame(height: 80)
|
||||
|
||||
// Title card
|
||||
VStack(spacing: 12) {
|
||||
Text("All Time Moods")
|
||||
.font(.system(size: 22, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C"))
|
||||
|
||||
Text("\(totalCount)")
|
||||
.font(.system(size: 96, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(Color(hex: "4A2810"))
|
||||
|
||||
Text("entries tracked")
|
||||
.font(.system(size: 16, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.7))
|
||||
}
|
||||
|
||||
Spacer().frame(height: 50)
|
||||
|
||||
// Mood circles row
|
||||
HStack(spacing: 20) {
|
||||
ForEach(metrics.sorted(by: { $0.mood.rawValue > $1.mood.rawValue }), id: \.mood) { metric in
|
||||
VStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
.frame(width: 90, height: 90)
|
||||
.shadow(color: moodTint.color(forMood: metric.mood).opacity(0.4), radius: 12, y: 6)
|
||||
|
||||
metric.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Text("\(metric.percent, specifier: "%.0f")%")
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "4A2810"))
|
||||
|
||||
Text("\(metric.total)")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.6))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Bottom card
|
||||
HStack(alignment: .bottom) {
|
||||
ForEach(metrics.sorted(by: { $0.percent > $1.percent }), id: \.mood) { metric in
|
||||
VStack(spacing: 4) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
.frame(height: CGFloat(metric.percent) * 2.5)
|
||||
|
||||
Text(metric.mood.strValue)
|
||||
.font(.system(size: 11, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C"))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.frame(height: 300, alignment: .bottom)
|
||||
.padding(24)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.fill(.white.opacity(0.6))
|
||||
)
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer().frame(height: 50)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 22))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 28, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.4))
|
||||
}
|
||||
|
||||
Spacer().frame(height: 50)
|
||||
}
|
||||
}
|
||||
.frame(width: 666, height: 1190)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
|
||||
// MARK: - V5 "Color Block" — Mood colors fill proportional strips, white text
|
||||
|
||||
struct AllMoodsV5: View {
|
||||
let metrics: [MoodMetrics]
|
||||
let totalCount: Int
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
let sorted = metrics.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })
|
||||
let headerHeight: CGFloat = 80
|
||||
let footerHeight: CGFloat = 180
|
||||
let blocksHeight: CGFloat = 1190 - headerHeight - footerHeight
|
||||
|
||||
return VStack(spacing: 0) {
|
||||
// Header — dark band
|
||||
ZStack {
|
||||
Color(hex: "1C1C1E")
|
||||
|
||||
Text("\(totalCount) moods")
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
.tracking(2)
|
||||
.textCase(.uppercase)
|
||||
}
|
||||
.frame(height: headerHeight)
|
||||
|
||||
// Color blocks — fill proportionally, no gaps
|
||||
ForEach(sorted, id: \.mood) { metric in
|
||||
let blockHeight = max(blocksHeight * CGFloat(metric.percent / 100), 60)
|
||||
|
||||
ZStack {
|
||||
moodTint.color(forMood: metric.mood)
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(metric.mood.strValue.uppercased())
|
||||
.font(.system(size: 14, weight: .bold, design: .rounded))
|
||||
.tracking(2)
|
||||
|
||||
Text("\(metric.total) days")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.opacity(0.7)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(metric.percent, specifier: "%.0f")%")
|
||||
.font(.system(size: 42, weight: .heavy, design: .rounded))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 40)
|
||||
}
|
||||
.frame(height: blockHeight)
|
||||
}
|
||||
|
||||
// Footer — dark band
|
||||
ZStack {
|
||||
Color(hex: "1C1C1E")
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 22))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.3))
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.background(Color(hex: "1C1C1E"))
|
||||
.frame(width: 666, height: 1190)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// CurrentStreakVariations.swift
|
||||
// Feels
|
||||
//
|
||||
// 2 design variations for the Current Streak (Last 10 Days) sharing template.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - V2 "Gradient Cards" — 2x5 grid of colored cards
|
||||
|
||||
struct CurrentStreakV2: View {
|
||||
let moodEntries: [MoodEntryModel]
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
private let dayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEE"
|
||||
return f
|
||||
}()
|
||||
|
||||
private let numFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d"
|
||||
return f
|
||||
}()
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
}
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(), spacing: 16),
|
||||
GridItem(.flexible(), spacing: 16)
|
||||
]
|
||||
|
||||
var shareView: some View {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [Color(hex: "FFF5EE"), Color(hex: "FFE8D6")],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
Text("Last 10 Days")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "4A2810"))
|
||||
|
||||
Spacer().frame(height: 6)
|
||||
|
||||
Text("Your recent moods")
|
||||
.font(.system(size: 16, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.7))
|
||||
|
||||
Spacer().frame(height: 24)
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 12) {
|
||||
ForEach(moodEntries) { entry in
|
||||
VStack(spacing: 8) {
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 36, height: 36)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(dayFormatter.string(from: entry.forDate ?? Date()))
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Text(numFormatter.string(from: entry.forDate ?? Date()))
|
||||
.font(.system(size: 28, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(moodTint.color(forMood: entry.mood))
|
||||
.shadow(color: moodTint.color(forMood: entry.mood).opacity(0.3), radius: 10, y: 5)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 36)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 22))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 28, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.4))
|
||||
}
|
||||
|
||||
Spacer().frame(height: 50)
|
||||
}
|
||||
}
|
||||
.frame(width: 666, height: 1190)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
|
||||
// MARK: - V5 "Color Strips" — Full-width mood-colored horizontal strips
|
||||
|
||||
struct CurrentStreakV5: View {
|
||||
let moodEntries: [MoodEntryModel]
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
private let dayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "EEEE"
|
||||
return f
|
||||
}()
|
||||
|
||||
private let numFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "d"
|
||||
return f
|
||||
}()
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
ZStack {
|
||||
Color(hex: "1C1C1E")
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
VStack(spacing: 4) {
|
||||
Text("10 DAYS")
|
||||
.font(.system(size: 16, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
.tracking(4)
|
||||
|
||||
Text("of feeling")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.5))
|
||||
}
|
||||
.frame(height: 80)
|
||||
|
||||
// Color strips
|
||||
ForEach(moodEntries) { entry in
|
||||
ZStack {
|
||||
moodTint.color(forMood: entry.mood)
|
||||
|
||||
HStack {
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
|
||||
Text(entry.mood.strValue)
|
||||
.font(.system(size: 18, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(dayFormatter.string(from: entry.forDate ?? Date()))
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
|
||||
Text(numFormatter.string(from: entry.forDate ?? Date()))
|
||||
.font(.system(size: 28, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 84)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Footer
|
||||
VStack(spacing: 10) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 22))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.3))
|
||||
}
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
}
|
||||
}
|
||||
.frame(width: 666, height: 1190)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
//
|
||||
// LongestStreakVariations.swift
|
||||
// Feels
|
||||
//
|
||||
// 2 design variations for the Longest Streak sharing template.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - V2 "Gradient Bar" — Warm gradient bg, horizontal progress
|
||||
|
||||
struct LongestStreakV2: View {
|
||||
let streakEntries: [MoodEntryModel]
|
||||
let selectedMood: Mood
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "MMM d, yyyy"
|
||||
return f
|
||||
}()
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 650, height: 400))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [Color(hex: "FFF5EE"), Color(hex: "FFE4D6")],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
VStack(spacing: 20) {
|
||||
// Top row
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Longest Streak")
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C"))
|
||||
.textCase(.uppercase)
|
||||
.tracking(2)
|
||||
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text("\(streakEntries.count)")
|
||||
.font(.system(size: 56, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(Color(hex: "4A2810"))
|
||||
|
||||
Text("days")
|
||||
.font(.system(size: 20, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C"))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: selectedMood))
|
||||
.frame(width: 72, height: 72)
|
||||
.shadow(color: moodTint.color(forMood: selectedMood).opacity(0.4), radius: 12)
|
||||
|
||||
selectedMood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 36)
|
||||
|
||||
// Progress bar
|
||||
VStack(spacing: 8) {
|
||||
GeometryReader { geo in
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(Color(hex: "4A2810").opacity(0.1))
|
||||
.frame(height: 24)
|
||||
|
||||
Capsule()
|
||||
.fill(moodTint.color(forMood: selectedMood))
|
||||
.frame(width: geo.size.width * 0.85, height: 24)
|
||||
.shadow(color: moodTint.color(forMood: selectedMood).opacity(0.3), radius: 6)
|
||||
}
|
||||
}
|
||||
.frame(height: 24)
|
||||
|
||||
HStack {
|
||||
Text(dateFormatter.string(from: streakEntries.first?.forDate ?? Date()))
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.6))
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(dateFormatter.string(from: streakEntries.last?.forDate ?? Date()))
|
||||
.font(.system(size: 12, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.6))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 36)
|
||||
|
||||
// Footer
|
||||
HStack {
|
||||
Text(selectedMood.strValue)
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C"))
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 6) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 72, height: 72)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 22, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8B5E3C").opacity(0.4))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 36)
|
||||
}
|
||||
.padding(.vertical, 32)
|
||||
}
|
||||
.frame(width: 650, height: 400)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
|
||||
// MARK: - V3 "Dark Badge" — Dark bg, glowing badge, neon accent
|
||||
|
||||
struct LongestStreakV3: View {
|
||||
let streakEntries: [MoodEntryModel]
|
||||
let selectedMood: Mood
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .medium
|
||||
return f
|
||||
}()
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 650, height: 400))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
ZStack {
|
||||
Color(hex: "0A0A0F")
|
||||
|
||||
HStack(spacing: 0) {
|
||||
// Left half — badge centered
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(moodTint.color(forMood: selectedMood).opacity(0.15))
|
||||
.frame(width: 220, height: 220)
|
||||
|
||||
Circle()
|
||||
.stroke(moodTint.color(forMood: selectedMood).opacity(0.3), lineWidth: 2)
|
||||
.frame(width: 180, height: 180)
|
||||
|
||||
Circle()
|
||||
.stroke(moodTint.color(forMood: selectedMood), lineWidth: 3)
|
||||
.frame(width: 150, height: 150)
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text("\(streakEntries.count)")
|
||||
.font(.system(size: 60, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("DAYS")
|
||||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(moodTint.color(forMood: selectedMood))
|
||||
.tracking(4)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// Right half — text left-aligned, vertically centered
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("LONGEST STREAK")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.foregroundColor(Color(hex: "6E6E80"))
|
||||
.tracking(4)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
selectedMood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(moodTint.color(forMood: selectedMood))
|
||||
|
||||
Text(selectedMood.strValue)
|
||||
.font(.system(size: 30, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(dateFormatter.string(from: streakEntries.first?.forDate ?? Date()))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
|
||||
Text("→")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(Color(hex: "6E6E80"))
|
||||
|
||||
Text(dateFormatter.string(from: streakEntries.last?.forDate ?? Date()))
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
|
||||
VStack(spacing: 6) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 72, height: 72)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 22, weight: .medium))
|
||||
.foregroundColor(Color(hex: "3A3A4A"))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
}
|
||||
.frame(width: 650, height: 400)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//
|
||||
// MonthTotalVariations.swift
|
||||
// Feels
|
||||
//
|
||||
// 3 design variations for the Month Total sharing template.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - V1 "Clean Calendar" — Calendar-style grid, Health-app feel
|
||||
|
||||
struct MonthTotalV1: View {
|
||||
let moodMetrics: [MoodMetrics]
|
||||
let moodEntries: [MoodEntryModel]
|
||||
let month: Int
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
let columns = Array(repeating: GridItem(.flexible(), spacing: 8), count: 7)
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
ZStack {
|
||||
Color.white
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer().frame(height: 70)
|
||||
|
||||
// Header
|
||||
VStack(spacing: 8) {
|
||||
Text(Random.monthName(fromMonthInt: month))
|
||||
.font(.system(size: 36, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "1C1C1E"))
|
||||
|
||||
Text("\(moodEntries.count) DAYS TRACKED")
|
||||
.font(.system(size: 14, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "8E8E93"))
|
||||
.tracking(2)
|
||||
}
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
// Calendar grid
|
||||
LazyVGrid(columns: columns, spacing: 8) {
|
||||
ForEach(moodEntries) { entry in
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(moodTint.color(forMood: entry.mood))
|
||||
.frame(height: 70)
|
||||
.overlay(
|
||||
VStack(spacing: 4) {
|
||||
entry.mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text(DateFormattingCache.shared.string(for: entry.forDate ?? Date(), format: .day))
|
||||
.font(.system(size: 12, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer().frame(height: 40)
|
||||
|
||||
// Stats row
|
||||
HStack(spacing: 0) {
|
||||
ForEach(moodMetrics.sorted(by: { $0.mood.rawValue > $1.mood.rawValue }), id: \.mood) { metric in
|
||||
VStack(spacing: 8) {
|
||||
Capsule()
|
||||
.fill(moodTint.color(forMood: metric.mood))
|
||||
.frame(width: 40, height: 6)
|
||||
|
||||
Text("\(metric.percent, specifier: "%.0f")%")
|
||||
.font(.system(size: 18, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color(hex: "1C1C1E"))
|
||||
|
||||
Text(metric.mood.strValue)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundColor(Color(hex: "8E8E93"))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 22))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 28, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color(hex: "C7C7CC"))
|
||||
}
|
||||
|
||||
Spacer().frame(height: 50)
|
||||
}
|
||||
}
|
||||
.frame(width: 666, height: 1190)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
|
||||
// MARK: - V5 "Stacked Bars" — Full-width mood color bars, white overlays
|
||||
|
||||
struct MonthTotalV5: View {
|
||||
let moodMetrics: [MoodMetrics]
|
||||
let moodEntries: [MoodEntryModel]
|
||||
let month: Int
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults)
|
||||
private var moodTint: MoodTints = .Default
|
||||
|
||||
let columns = Array(repeating: GridItem(.flexible(), spacing: 3), count: 7)
|
||||
|
||||
var image: UIImage {
|
||||
shareView.asImage(size: CGSize(width: 666, height: 1190))
|
||||
}
|
||||
|
||||
var shareView: some View {
|
||||
let sorted = moodMetrics.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })
|
||||
|
||||
return VStack(spacing: 0) {
|
||||
// Header — dark band
|
||||
ZStack {
|
||||
Color(hex: "1C1C1E")
|
||||
|
||||
HStack {
|
||||
Text(Random.monthName(fromMonthInt: month).uppercased())
|
||||
.font(.system(size: 18, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
.tracking(4)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(moodEntries.count)")
|
||||
.font(.system(size: 28, weight: .heavy, design: .rounded))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.frame(height: 80)
|
||||
|
||||
// Mini grid of all days
|
||||
ZStack {
|
||||
Color(hex: "1C1C1E")
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 3) {
|
||||
ForEach(moodEntries) { entry in
|
||||
Rectangle()
|
||||
.fill(moodTint.color(forMood: entry.mood))
|
||||
.frame(height: 50)
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
|
||||
// Color percentage blocks
|
||||
ForEach(sorted, id: \.mood) { metric in
|
||||
let barHeight: CGFloat = max(CGFloat(metric.percent / 100) * 400, 50)
|
||||
|
||||
ZStack {
|
||||
moodTint.color(forMood: metric.mood)
|
||||
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(metric.mood.strValue.uppercased())
|
||||
.font(.system(size: 13, weight: .bold, design: .rounded))
|
||||
.tracking(2)
|
||||
Text("\(metric.total) days")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.opacity(0.7)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(metric.percent, specifier: "%.0f")%")
|
||||
.font(.system(size: 32, weight: .heavy, design: .rounded))
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
.frame(height: barHeight)
|
||||
}
|
||||
|
||||
// Footer — dark band fills remaining space
|
||||
ZStack {
|
||||
Color(hex: "1C1C1E")
|
||||
|
||||
VStack(spacing: 10) {
|
||||
Image("FeelsAppIcon")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 96, height: 96)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 22))
|
||||
|
||||
Text("Feels")
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.white.opacity(0.3))
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.background(Color(hex: "1C1C1E"))
|
||||
.frame(width: 666, height: 1190)
|
||||
}
|
||||
|
||||
var body: some View { shareView }
|
||||
}
|
||||
Reference in New Issue
Block a user