import SwiftUI import CoreImage.CIFilterBuiltins struct WeekPlanShareView: View { let plan: WeekPlan let settings: AppSettings let dishes: [Dish] let tags: [Tag] static let a4LandscapeWidth: CGFloat = 3508 static let a4LandscapeHeight: CGFloat = 2480 private let shareURL = "https://mealmood.app" private let qrContext = CIContext() private let qrFilter = CIFilter.qrCodeGenerator() private var dayRange: ClosedRange { settings.includeWeekends ? 0...6 : 0...4 } private var mealTypes: [MealType] { switch settings.mealWindowsEnum { case .dinnerOnly: return [.dinner] case .lunchOnly: return [.lunch] case .both: return [.lunch, .dinner] } } private var exportDateString: String { Date.now.formatted(date: .abbreviated, time: .omitted) } private var qrImage: UIImage? { qrFilter.setValue(Data(shareURL.utf8), forKey: "inputMessage") qrFilter.setValue("M", forKey: "inputCorrectionLevel") guard let outputImage = qrFilter.outputImage else { return nil } let transformed = outputImage.transformed(by: CGAffineTransform(scaleX: 10, y: 10)) guard let cgImage = qrContext.createCGImage(transformed, from: transformed.extent) else { return nil } return UIImage(cgImage: cgImage) } var body: some View { ZStack { backgroundLayer VStack(alignment: .leading, spacing: 34) { headerBlock ZStack { RoundedRectangle(cornerRadius: 30) .fill(Color.white.opacity(0.62)) RoundedRectangle(cornerRadius: 30) .stroke(Color.white.opacity(0.75), lineWidth: 2) Grid(horizontalSpacing: 10, verticalSpacing: 10) { GridRow { gridHeaderCell("", day: nil) .frame(minWidth: 220) ForEach(Array(dayRange), id: \.self) { day in gridHeaderCell(dayTitle(for: day), day: day) } } ForEach(mealTypes, id: \.self) { mealType in GridRow { gridMealCell(mealTypeLabel(mealType), icon: mealType.icon, mealType: mealType) .frame(minWidth: 220) ForEach(Array(dayRange), id: \.self) { day in gridDishCell(dishName(day: day, mealType: mealType), day: day) } } } } .padding(18) } .shadow(color: Color(hex: "#DDAA99").opacity(0.18), radius: 26, x: 0, y: 16) Spacer() footerBlock } .padding(.horizontal, 84) .padding(.vertical, 72) } .frame(width: Self.a4LandscapeWidth, height: Self.a4LandscapeHeight) } private func dayTitle(for day: Int) -> String { let keys = ["day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat", "day_sun"] let localizedDay = day >= 0 && day < keys.count ? NSLocalizedString(keys[day], comment: "") : "" let dayDate = plan.weekStartDate.addingDays(day) let dayNumber = Calendar.current.component(.day, from: dayDate) return "\(localizedDay) \(dayNumber)" } private func mealTypeLabel(_ mealType: MealType) -> String { mealType == .lunch ? String(localized: "lunch") : String(localized: "dinner") } private func dishName(day: Int, mealType: MealType) -> String { let slot = plan.slots.first { $0.dayOfWeek == day && $0.mealType == mealType.rawValue } guard let slot, let dishId = slot.dishId, let dish = dishes.first(where: { $0.id == dishId }) else { return "—" } return dish.name } private var backgroundLayer: some View { ZStack { LinearGradient( colors: [ Color(hex: "#FFEFE6"), Color(hex: "#F1FBF5") ], startPoint: .topLeading, endPoint: .bottomTrailing ) Circle() .fill(Color(hex: "#FFD6E2").opacity(0.55)) .frame(width: 760, height: 760) .blur(radius: 20) .offset(x: -980, y: -760) Circle() .fill(Color(hex: "#CFF2E8").opacity(0.55)) .frame(width: 680, height: 680) .blur(radius: 20) .offset(x: 1050, y: 760) GeometryReader { geo in Canvas { context, size in let spacing: CGFloat = 80 var x: CGFloat = -size.height while x < size.width { var path = Path() path.move(to: CGPoint(x: x, y: 0)) path.addLine(to: CGPoint(x: x + size.height, y: size.height)) context.stroke(path, with: .color(Color(hex: "#FFFFFF").opacity(0.26)), lineWidth: 1.2) x += spacing } } .frame(width: geo.size.width, height: geo.size.height) } } } private var headerBlock: some View { HStack(alignment: .center, spacing: 24) { AppIconPlaceholder(size: 84) VStack(alignment: .leading, spacing: 8) { Text("MealMood Premium") .font(.system(size: 50, weight: .bold, design: .rounded)) .foregroundColor(.mealMoodTextPrimary) Text(String(localized: "share_week_title")) .font(.system(size: 28, weight: .semibold, design: .rounded)) .foregroundColor(Color(hex: "#5D645F")) } Spacer() VStack(alignment: .trailing, spacing: 8) { Text(plan.weekStartDate.formattedWeekRange()) .font(.system(size: 36, weight: .bold, design: .rounded)) .foregroundColor(Color(hex: "#4A4F4B")) Text(exportDateString) .font(.system(size: 24, weight: .medium, design: .rounded)) .foregroundColor(.mealMoodTextSecondary) } } .padding(.horizontal, 30) .padding(.vertical, 24) .background(Color.white.opacity(0.76)) .clipShape(RoundedRectangle(cornerRadius: 24)) .overlay( RoundedRectangle(cornerRadius: 24) .stroke(Color(hex: "#F3C4B5").opacity(0.7), lineWidth: 1) ) .shadow(color: Color(hex: "#DDAA99").opacity(0.15), radius: 20, x: 0, y: 12) } private var footerBlock: some View { HStack(alignment: .center, spacing: 20) { VStack(alignment: .leading, spacing: 6) { Text("Powered by MealMood") .font(.system(size: 30, weight: .bold, design: .rounded)) .foregroundColor(Color(hex: "#3A3A3A")) Text(shareURL) .font(.system(size: 24, weight: .semibold, design: .rounded)) .foregroundColor(Color(hex: "#588A77")) } Spacer() if let qrImage { VStack(spacing: 6) { Image(uiImage: qrImage) .interpolation(.none) .resizable() .scaledToFit() .frame(width: 162, height: 162) .padding(10) .background(Color.white) .clipShape(RoundedRectangle(cornerRadius: 18)) Text("Scan me") .font(.system(size: 18, weight: .semibold, design: .rounded)) .foregroundColor(.mealMoodTextSecondary) } } } .padding(.horizontal, 26) .padding(.vertical, 18) .background(Color.white.opacity(0.8)) .clipShape(RoundedRectangle(cornerRadius: 22)) .overlay( RoundedRectangle(cornerRadius: 22) .stroke(Color(hex: "#DDEEE6"), lineWidth: 1) ) .shadow(color: Color(hex: "#7F9E90").opacity(0.14), radius: 16, x: 0, y: 8) } private func gridHeaderCell(_ title: String, day: Int?) -> some View { let background = dayHeaderBackground(day: day) return Text(title) .font(.system(size: 25, weight: .bold, design: .rounded)) .foregroundColor(.mealMoodTextPrimary) .frame(maxWidth: .infinity, minHeight: 92) .padding(.horizontal, 10) .background(background) .clipShape(RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(Color.white.opacity(0.6), lineWidth: 1) ) } private func gridMealCell(_ title: String, icon: String, mealType: MealType) -> some View { let background = mealType == .lunch ? LinearGradient(colors: [Color(hex: "#FFE2C8"), Color(hex: "#FFD4B2")], startPoint: .topLeading, endPoint: .bottomTrailing) : LinearGradient(colors: [Color(hex: "#D7EFE7"), Color(hex: "#C2E7DB")], startPoint: .topLeading, endPoint: .bottomTrailing) return HStack(spacing: 8) { Image(systemName: icon) Text(title) } .font(.system(size: 25, weight: .bold, design: .rounded)) .foregroundColor(Color(hex: "#365548")) .frame(maxWidth: .infinity, minHeight: 130) .padding(.horizontal, 14) .background(background) .clipShape(RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(Color.white.opacity(0.6), lineWidth: 1) ) } private func gridDishCell(_ title: String, day: Int) -> some View { let tint = dayCellTint(day: day) return Text(title) .font(.system(size: 26, weight: .medium)) .foregroundColor(.mealMoodTextPrimary) .multilineTextAlignment(.center) .lineLimit(3) .minimumScaleFactor(0.6) .frame(maxWidth: .infinity, minHeight: 130) .padding(.horizontal, 10) .background( LinearGradient( colors: [Color.white.opacity(0.95), tint.opacity(0.32)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .clipShape(RoundedRectangle(cornerRadius: 14)) .overlay( RoundedRectangle(cornerRadius: 14) .stroke(Color(hex: "#F2E5DE"), lineWidth: 1) ) } private func dayHeaderBackground(day: Int?) -> LinearGradient { guard let day else { return LinearGradient( colors: [Color(hex: "#F6E7DE"), Color(hex: "#F2DED2")], startPoint: .topLeading, endPoint: .bottomTrailing ) } let palettes: [(String, String)] = [ ("#FFD5D9", "#FFC8D2"), ("#FFE1C8", "#FFD3AF"), ("#FFF0C8", "#FFE6A8"), ("#DFF2CC", "#CFEAB6"), ("#D3F0E7", "#BFE6DA"), ("#D9E8FF", "#C9DDFF"), ("#E6D9FF", "#D9C8FF") ] let index = max(0, min(day, palettes.count - 1)) return LinearGradient( colors: [Color(hex: palettes[index].0), Color(hex: palettes[index].1)], startPoint: .topLeading, endPoint: .bottomTrailing ) } private func dayCellTint(day: Int) -> Color { let colors = [ Color(hex: "#FFD0D8"), Color(hex: "#FFE1C6"), Color(hex: "#FFF0C3"), Color(hex: "#DFF1CA"), Color(hex: "#D5EFE6"), Color(hex: "#D7E7FF"), Color(hex: "#E7DCFF") ] return colors[max(0, min(day, colors.count - 1))] } }