diff --git a/Shared/Models/SharingImageModels.swift b/Shared/Models/SharingImageModels.swift index f048c3e..e43a9f3 100644 --- a/Shared/Models/SharingImageModels.swift +++ b/Shared/Models/SharingImageModels.swift @@ -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) { } } diff --git a/Shared/Views/MonthView/MonthView.swift b/Shared/Views/MonthView/MonthView.swift index 8ee12bb..bc9d8e5 100644 --- a/Shared/Views/MonthView/MonthView.swift +++ b/Shared/Views/MonthView/MonthView.swift @@ -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) { diff --git a/Shared/Views/YearView/YearView.swift b/Shared/Views/YearView/YearView.swift index 7f89104..7818d4d 100644 --- a/Shared/Views/YearView/YearView.swift +++ b/Shared/Views/YearView/YearView.swift @@ -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 {