Add social media share functionality to Month and Year views

- Add share button to MonthCard and YearCard headers
- Create social media-friendly "Monthly Mood Wrap" and "Year in Review" designs
- Show top mood, days tracked, and mood breakdown with colorful bar charts
- Add ImageOnlyShareSheet to share images without extra text
- Uses user's selected theme colors and icon pack

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-10 23:16:48 -06:00
parent f822927e98
commit 4713192d55
3 changed files with 306 additions and 37 deletions

View File

@@ -40,20 +40,35 @@ class ShareActivityItemSource: NSObject, UIActivityItemSource {
struct ShareSheet: UIViewControllerRepresentable {
let photo: UIImage
func makeUIViewController(context: Context) -> UIActivityViewController {
let text = "ifeel"
let itemSource = ShareActivityItemSource(shareText: text, shareImage: photo)
let activityItems: [Any] = [photo, text, itemSource]
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil)
return controller
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
}
}
struct ImageOnlyShareSheet: UIViewControllerRepresentable {
let photo: UIImage
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: [photo],
applicationActivities: nil)
return controller
}
func updateUIViewController(_ vc: UIActivityViewController, context: Context) {
}
}

View File

@@ -72,6 +72,10 @@ struct MonthView: View {
)
selectedDetail.fuckingWrapped = detailView
selectedDetail.showFuckingSheet = true
},
onShare: { image in
shareImage.fuckingWrappedShrable = image
shareImage.showFuckingSheet = true
}
)
}
@@ -141,7 +145,7 @@ struct MonthView: View {
}
.sheet(isPresented: self.$shareImage.showFuckingSheet) {
if let uiImage = self.shareImage.fuckingWrappedShrable {
ShareSheet(photo: uiImage)
ImageOnlyShareSheet(photo: uiImage)
}
}
.onPreferenceChange(ViewOffsetKey.self) { value in
@@ -169,6 +173,7 @@ struct MonthCard: View {
let theme: Theme
let filteredDays: [Int]
let onTap: () -> Void
let onShare: (UIImage) -> Void
@State private var showStats = true
@@ -181,25 +186,146 @@ struct MonthCard: View {
return Random.createTotalPerc(fromEntries: monthEntries)
}
private var topMood: Mood? {
metrics.filter { $0.total > 0 }.max(by: { $0.total < $1.total })?.mood
}
private var totalTrackedDays: Int {
entries.filter { ![.missing, .placeholder].contains($0.mood) }.count
}
private var shareableView: some View {
VStack(spacing: 0) {
// Header with month/year
Text("\(Random.monthName(fromMonthInt: month).uppercased()) \(String(year))")
.font(.system(size: 32, weight: .heavy, design: .rounded))
.foregroundColor(textColor)
.padding(.top, 40)
.padding(.bottom, 8)
Text("Monthly Mood Wrap")
.font(.system(size: 16, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.6))
.padding(.bottom, 30)
// Top mood highlight
if let topMood = topMood {
VStack(spacing: 12) {
Circle()
.fill(moodTint.color(forMood: topMood))
.frame(width: 100, height: 100)
.overlay(
imagePack.icon(forMood: topMood)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
.padding(24)
)
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 20, x: 0, y: 10)
Text("Top Mood")
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.5))
Text(topMood.strValue.uppercased())
.font(.system(size: 20, weight: .bold, design: .rounded))
.foregroundColor(moodTint.color(forMood: topMood))
}
.padding(.bottom, 30)
}
// Stats row
HStack(spacing: 0) {
VStack(spacing: 4) {
Text("\(totalTrackedDays)")
.font(.system(size: 36, weight: .bold, design: .rounded))
.foregroundColor(textColor)
Text("Days Tracked")
.font(.system(size: 12, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.5))
}
.frame(maxWidth: .infinity)
}
.padding(.bottom, 30)
// Mood breakdown with bars
VStack(spacing: 12) {
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
HStack(spacing: 12) {
Circle()
.fill(moodTint.color(forMood: metric.mood))
.frame(width: 32, height: 32)
.overlay(
imagePack.icon(forMood: metric.mood)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
.padding(7)
)
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 6)
.fill(Color.gray.opacity(0.2))
RoundedRectangle(cornerRadius: 6)
.fill(moodTint.color(forMood: metric.mood))
.frame(width: max(8, geo.size.width * CGFloat(metric.percent / 100)))
}
}
.frame(height: 12)
Text("\(Int(metric.percent))%")
.font(.system(size: 14, weight: .semibold, design: .rounded))
.foregroundColor(textColor)
.frame(width: 40, alignment: .trailing)
}
}
}
.padding(.horizontal, 32)
.padding(.bottom, 40)
// App branding
Text("ifeel")
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.3))
.padding(.bottom, 20)
}
.frame(width: 400)
.background(theme.currentTheme.secondaryBGColor)
}
var body: some View {
VStack(spacing: 0) {
// Month Header
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
HStack {
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
.font(.title3.bold())
.foregroundColor(textColor)
HStack {
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
HStack {
Text("\(Random.monthName(fromMonthInt: month)) \(String(year))")
.font(.title3.bold())
.foregroundColor(textColor)
Spacer()
Image(systemName: showStats ? "chevron.up" : "chevron.down")
.font(.caption.weight(.semibold))
.foregroundColor(textColor.opacity(0.5))
Image(systemName: showStats ? "chevron.up" : "chevron.down")
.font(.caption.weight(.semibold))
.foregroundColor(textColor.opacity(0.5))
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.buttonStyle(.plain)
Spacer()
Button(action: {
let image = shareableView.asImage(size: CGSize(width: 400, height: 700))
onShare(image)
}) {
Image(systemName: "square.and.arrow.up")
.font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.6))
}
.buttonStyle(.plain)
}
.buttonStyle(.plain)
.padding(.horizontal, 16)
.padding(.vertical, 12)
// Weekday Labels
HStack(spacing: 2) {

View File

@@ -24,6 +24,7 @@ struct YearView: View {
@EnvironmentObject var iapManager: IAPManager
@StateObject public var viewModel: YearViewModel
@StateObject private var filteredDays = DaysFilterClass.shared
@StateObject private var shareImage = StupidAssShareObservableObject()
@State private var trialWarningHidden = false
@State private var showSubscriptionStore = false
@@ -46,7 +47,11 @@ struct YearView: View {
imagePack: imagePack,
textColor: textColor,
theme: theme,
filteredDays: filteredDays.currentFilters
filteredDays: filteredDays.currentFilters,
onShare: { image in
shareImage.fuckingWrappedShrable = image
shareImage.showFuckingSheet = true
}
)
}
}
@@ -95,6 +100,11 @@ struct YearView: View {
.sheet(isPresented: $showSubscriptionStore) {
FeelsSubscriptionStoreView()
}
.sheet(isPresented: $shareImage.showFuckingSheet) {
if let uiImage = shareImage.fuckingWrappedShrable {
ImageOnlyShareSheet(photo: uiImage)
}
}
.onAppear(perform: {
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
})
@@ -120,6 +130,7 @@ struct YearCard: View {
let textColor: Color
let theme: Theme
let filteredDays: [Int]
let onShare: (UIImage) -> Void
@State private var showStats = true
@@ -140,30 +151,147 @@ struct YearCard: View {
yearEntries.filter { ![Mood.missing, Mood.placeholder].contains($0.mood) }.count
}
private var topMood: Mood? {
metrics.filter { $0.total > 0 }.max(by: { $0.total < $1.total })?.mood
}
private var shareableView: some View {
VStack(spacing: 0) {
// Header with year
Text(String(year))
.font(.system(size: 48, weight: .heavy, design: .rounded))
.foregroundColor(textColor)
.padding(.top, 40)
.padding(.bottom, 8)
Text("Year in Review")
.font(.system(size: 18, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.6))
.padding(.bottom, 30)
// Top mood highlight
if let topMood = topMood {
VStack(spacing: 12) {
Circle()
.fill(moodTint.color(forMood: topMood))
.frame(width: 120, height: 120)
.overlay(
imagePack.icon(forMood: topMood)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
.padding(28)
)
.shadow(color: moodTint.color(forMood: topMood).opacity(0.5), radius: 25, x: 0, y: 12)
Text("Top Mood")
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.5))
Text(topMood.strValue.uppercased())
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundColor(moodTint.color(forMood: topMood))
}
.padding(.bottom, 30)
}
// Stats row
HStack(spacing: 0) {
VStack(spacing: 4) {
Text("\(totalEntries)")
.font(.system(size: 42, weight: .bold, design: .rounded))
.foregroundColor(textColor)
Text("Days Tracked")
.font(.system(size: 13, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.5))
}
.frame(maxWidth: .infinity)
}
.padding(.bottom, 30)
// Mood breakdown with bars
VStack(spacing: 14) {
ForEach(metrics.filter { $0.total > 0 }.sorted(by: { $0.mood.rawValue > $1.mood.rawValue })) { metric in
HStack(spacing: 14) {
Circle()
.fill(moodTint.color(forMood: metric.mood))
.frame(width: 36, height: 36)
.overlay(
imagePack.icon(forMood: metric.mood)
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
.padding(8)
)
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.2))
RoundedRectangle(cornerRadius: 8)
.fill(moodTint.color(forMood: metric.mood))
.frame(width: max(8, geo.size.width * CGFloat(metric.percent / 100)))
}
}
.frame(height: 16)
Text("\(Int(metric.percent))%")
.font(.system(size: 16, weight: .semibold, design: .rounded))
.foregroundColor(textColor)
.frame(width: 45, alignment: .trailing)
}
}
}
.padding(.horizontal, 32)
.padding(.bottom, 40)
// App branding
Text("ifeel")
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundColor(textColor.opacity(0.3))
.padding(.bottom, 20)
}
.frame(width: 400)
.background(theme.currentTheme.secondaryBGColor)
}
var body: some View {
VStack(spacing: 0) {
// Year Header
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
HStack {
Text(String(year))
.font(.title2.bold())
.foregroundColor(textColor)
HStack {
Button(action: { withAnimation(.easeInOut(duration: 0.2)) { showStats.toggle() } }) {
HStack {
Text(String(year))
.font(.title2.bold())
.foregroundColor(textColor)
Spacer()
Text("\(totalEntries) days")
.font(.subheadline)
.foregroundColor(textColor.opacity(0.6))
Text("\(totalEntries) days")
.font(.subheadline)
.foregroundColor(textColor.opacity(0.6))
Image(systemName: showStats ? "chevron.up" : "chevron.down")
.font(.caption.weight(.semibold))
.foregroundColor(textColor.opacity(0.5))
.padding(.leading, 4)
Image(systemName: showStats ? "chevron.up" : "chevron.down")
.font(.caption.weight(.semibold))
.foregroundColor(textColor.opacity(0.5))
.padding(.leading, 4)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.buttonStyle(.plain)
Spacer()
Button(action: {
let image = shareableView.asImage(size: CGSize(width: 400, height: 750))
onShare(image)
}) {
Image(systemName: "square.and.arrow.up")
.font(.subheadline.weight(.medium))
.foregroundColor(textColor.opacity(0.6))
}
.buttonStyle(.plain)
}
.buttonStyle(.plain)
.padding(.horizontal, 16)
.padding(.vertical, 12)
// Stats Section (collapsible)
if showStats {