1.0.2: Add multilingual support, redesign share view, and iCloud sync improvements

- Add localisations for German, French, Italian, and Brazilian Portuguese
- Redesign WeekPlanShareView with richer layout and sharing options
- Improve HomeView with various UI enhancements
- Update AppSettings and Tag models for new features
- Minor iCloud sync and Settings fixes
- Add archive & upload script for App Store submissions

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-03-07 19:02:33 +01:00
parent 63c9ba57de
commit c66132f494
15 changed files with 1946 additions and 109 deletions
+236 -11
View File
@@ -1,5 +1,6 @@
import SwiftUI
import SwiftData
import UIKit
struct HomeView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@@ -24,6 +25,10 @@ struct HomeView: View {
@State private var showPostOnboardingAutoAssignPrompt: Bool = false
@State private var showPostOnboardingPremiumPrompt: Bool = false
@State private var hasEvaluatedPostOnboardingPrompts: Bool = false
@State private var showExportStylePicker: Bool = false
@State private var pendingExportPlan: WeekPlan?
@State private var shareImageURL: URL?
@State private var showShareSheet: Bool = false
private var settings: AppSettings? { allSettings.first }
@@ -383,6 +388,26 @@ struct HomeView: View {
PremiumView(settings: settings)
}
}
.sheet(isPresented: $showExportStylePicker) {
WeekExportStylePickerSheet(
selectedStyle: settings.weekExportStyleEnum
) { selectedStyle in
settings.weekExportStyleEnum = selectedStyle
try? context.save()
showExportStylePicker = false
if let planToShare = pendingExportPlan {
prepareShareImage(plan: planToShare, settings: settings)
}
pendingExportPlan = nil
}
.presentationDetents([.medium, .large])
}
.sheet(isPresented: $showShareSheet) {
if let shareImageURL {
ShareSheet(activityItems: [shareImageURL])
}
}
.sheet(isPresented: $showMonthlyHistory) {
MonthlyHistoryView(
weekPlans: weekPlans,
@@ -584,13 +609,10 @@ struct HomeView: View {
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
if settings.isPremium,
let image = renderWeekShareImage(plan: plan, settings: settings),
let shareURL = persistShareImage(image) {
ShareLink(
item: shareURL,
preview: SharePreview(String(localized: "share_week_title"), image: Image(uiImage: image))
) {
if settings.isPremium {
Button {
startShareFlow(plan: plan, settings: settings)
} label: {
Label("share_week_button", systemImage: "square.and.arrow.up")
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
@@ -599,6 +621,7 @@ struct HomeView: View {
.background(Color.white.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.plain)
} else {
Button {
showPremiumFromExport = true
@@ -637,19 +660,42 @@ struct HomeView: View {
)
}
private func startShareFlow(plan: WeekPlan, settings: AppSettings) {
if settings.weekExportStyle == nil {
pendingExportPlan = plan
showExportStylePicker = true
return
}
prepareShareImage(plan: plan, settings: settings)
}
private func prepareShareImage(plan: WeekPlan, settings: AppSettings) {
showShareSheet = false
shareImageURL = nil
guard let image = renderWeekShareImage(plan: plan, settings: settings),
let url = persistShareImage(image, style: settings.weekExportStyleEnum) else {
return
}
shareImageURL = url
showShareSheet = true
}
private func renderWeekShareImage(plan: WeekPlan, settings: AppSettings) -> UIImage? {
let renderer = ImageRenderer(content: WeekPlanShareView(plan: plan, settings: settings, dishes: dishes, tags: tags))
let canvasSize = WeekPlanShareView.canvasSize(for: settings.weekExportStyleEnum)
renderer.proposedSize = ProposedViewSize(
width: WeekPlanShareView.a4LandscapeWidth,
height: WeekPlanShareView.a4LandscapeHeight
width: canvasSize.width,
height: canvasSize.height
)
renderer.scale = 1
return renderer.uiImage
}
private func persistShareImage(_ image: UIImage) -> URL? {
private func persistShareImage(_ image: UIImage, style: WeekExportStyle) -> URL? {
guard let data = image.pngData() else { return nil }
let url = FileManager.default.temporaryDirectory.appendingPathComponent("mealmood-week-plan.png")
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
let filename = "mealmood-week-plan-\(style.rawValue)-\(timestamp).png"
let url = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
try? data.write(to: url, options: .atomic)
return url
}
@@ -731,6 +777,185 @@ struct HomeView: View {
}
}
private struct ShareSheet: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
private struct WeekExportStylePickerSheet: View {
let selectedStyle: WeekExportStyle
let onConfirm: (WeekExportStyle) -> Void
@State private var temporarySelection: WeekExportStyle
init(selectedStyle: WeekExportStyle, onConfirm: @escaping (WeekExportStyle) -> Void) {
self.selectedStyle = selectedStyle
self.onConfirm = onConfirm
_temporarySelection = State(initialValue: selectedStyle)
}
var body: some View {
NavigationStack {
VStack(spacing: 14) {
Text("share_export_style_picker_title")
.font(.mealMoodH2)
.foregroundColor(.mealMoodTextPrimary)
.padding(.top, 8)
Text("share_export_style_picker_subtitle")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
ScrollView {
VStack(spacing: 12) {
ForEach(WeekExportStyle.allCases, id: \.self) { style in
Button {
temporarySelection = style
} label: {
VStack(alignment: .leading, spacing: 10) {
WeekExportStyleThumbnail(style: style)
.frame(height: 110)
.clipShape(RoundedRectangle(cornerRadius: 10))
HStack {
Text(LocalizedStringKey(style.localizedKey))
.font(.mealMoodBodyBold)
.foregroundColor(.mealMoodTextPrimary)
Spacer()
Image(systemName: temporarySelection == style ? "checkmark.circle.fill" : "circle")
.foregroundColor(temporarySelection == style ? .mealMoodCoral : .mealMoodTextSecondary)
}
}
.padding(10)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(
temporarySelection == style ? Color.mealMoodCoral : Color.mealMoodMint.opacity(0.55),
lineWidth: temporarySelection == style ? 2 : 1
)
)
}
.buttonStyle(.plain)
}
}
.padding(.vertical, 6)
}
Button("share_export_style_picker_apply") {
onConfirm(temporarySelection)
}
.buttonStyle(.borderedProminent)
.tint(.mealMoodCoral)
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
.background(Color.mealMoodBackground.ignoresSafeArea())
.navigationBarTitleDisplayMode(.inline)
}
}
}
private struct WeekExportStyleThumbnail: View {
let style: WeekExportStyle
var body: some View {
ZStack {
switch style {
case .defaultStyle:
defaultPreview
case .schoolTimetable:
schoolPreview
case .vertical:
verticalPreview
}
}
}
private var defaultPreview: some View {
ZStack {
LinearGradient(colors: [Color(hex: "#FFEFE6"), Color(hex: "#F1FBF5")], startPoint: .topLeading, endPoint: .bottomTrailing)
VStack(spacing: 6) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white.opacity(0.9))
.frame(height: 20)
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.8))
.frame(height: 62)
.overlay(
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodCoral.opacity(0.35)).frame(height: 8)
Rectangle().fill(Color.mealMoodMint.opacity(0.35)).frame(height: 8)
Rectangle().fill(Color(hex: "#E9EDF7")).frame(height: 8)
}
.padding(8)
)
}
.padding(8)
}
}
private var schoolPreview: some View {
ZStack {
Color(hex: "#FFF9F5")
VStack(spacing: 5) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white)
.frame(height: 16)
HStack(spacing: 4) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.mealMoodCoral.opacity(0.5))
.frame(width: 42)
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodMint.opacity(0.4)).frame(height: 12)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
}
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodMint.opacity(0.4)).frame(height: 12)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
Rectangle().fill(Color(hex: "#FFFDFB")).frame(height: 20)
}
}
}
.padding(8)
}
}
private var verticalPreview: some View {
ZStack {
Color(hex: "#FFFDF8")
VStack(spacing: 6) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white)
.frame(height: 18)
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: 6) {
Capsule()
.fill(Color.mealMoodCoral.opacity(0.45))
.frame(width: 46, height: 14)
VStack(spacing: 4) {
Rectangle().fill(Color.mealMoodMint.opacity(0.35)).frame(height: 7)
Rectangle().fill(Color(hex: "#EDE8E1")).frame(height: 7)
}
}
.padding(.horizontal, 6)
.padding(.vertical, 4)
.background(Color.white)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
.padding(8)
}
}
}
private struct MonthlyHistoryView: View {
let weekPlans: [WeekPlan]
let currentWeekStart: Date
+333 -90
View File
@@ -6,14 +6,40 @@ struct WeekPlanShareView: View {
let settings: AppSettings
let dishes: [Dish]
let tags: [Tag]
let exportStyleOverride: WeekExportStyle?
static let a4LandscapeWidth: CGFloat = 3508
static let a4LandscapeHeight: CGFloat = 2480
static let a4PortraitWidth: CGFloat = 2480
static let a4PortraitHeight: CGFloat = 3508
private let shareURL = "https://mealmood.app"
private let qrContext = CIContext()
private let qrFilter = CIFilter.qrCodeGenerator()
init(
plan: WeekPlan,
settings: AppSettings,
dishes: [Dish],
tags: [Tag],
exportStyleOverride: WeekExportStyle? = nil
) {
self.plan = plan
self.settings = settings
self.dishes = dishes
self.tags = tags
self.exportStyleOverride = exportStyleOverride
}
static func canvasSize(for style: WeekExportStyle) -> CGSize {
switch style {
case .vertical:
return CGSize(width: a4PortraitWidth, height: a4PortraitHeight)
case .defaultStyle, .schoolTimetable:
return CGSize(width: a4LandscapeWidth, height: a4LandscapeHeight)
}
}
private var dayRange: ClosedRange<Int> {
settings.includeWeekends ? 0...6 : 0...4
}
@@ -26,6 +52,18 @@ struct WeekPlanShareView: View {
}
}
private var exportStyle: WeekExportStyle {
exportStyleOverride ?? settings.weekExportStyleEnum
}
private var canvasSize: CGSize {
Self.canvasSize(for: exportStyle)
}
private var locale: Locale {
Locale(identifier: settings.languageEnum.resolved().localeIdentifier)
}
private var exportDateString: String {
Date.now.formatted(date: .abbreviated, time: .omitted)
}
@@ -41,77 +79,42 @@ struct WeekPlanShareView: View {
var body: some View {
ZStack {
backgroundLayer
switch exportStyle {
case .defaultStyle:
defaultBackground
case .schoolTimetable:
schoolBackground
case .vertical:
verticalBackground
}
VStack(alignment: .leading, spacing: 34) {
VStack(spacing: 24) {
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)
}
}
}
Group {
switch exportStyle {
case .defaultStyle:
defaultGridLayout
case .schoolTimetable:
schoolTimetableLayout
case .vertical:
verticalLayout
}
.padding(18)
}
.shadow(color: Color(hex: "#DDAA99").opacity(0.18), radius: 26, x: 0, y: 16)
Spacer()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
footerBlock
}
.padding(.horizontal, 84)
.padding(.vertical, 72)
.padding(.horizontal, exportStyle == .vertical ? 72 : 84)
.padding(.vertical, exportStyle == .vertical ? 58 : 70)
}
.frame(width: Self.a4LandscapeWidth, height: Self.a4LandscapeHeight)
.frame(width: canvasSize.width, height: canvasSize.height)
}
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 {
private var defaultBackground: some View {
ZStack {
LinearGradient(
colors: [
Color(hex: "#FFEFE6"),
Color(hex: "#F1FBF5")
],
colors: [Color(hex: "#FFEFE6"), Color(hex: "#F1FBF5")],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
@@ -127,57 +130,262 @@ struct WeekPlanShareView: View {
.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 schoolBackground: some View {
ZStack {
Color(hex: "#FFF9F5")
RoundedRectangle(cornerRadius: 220)
.fill(Color.mealMoodMint.opacity(0.22))
.frame(width: 1500, height: 520)
.offset(x: 920, y: -920)
RoundedRectangle(cornerRadius: 220)
.fill(Color.mealMoodCoral.opacity(0.18))
.frame(width: 1600, height: 560)
.offset(x: -920, y: 940)
}
}
private var verticalBackground: some View {
ZStack {
Color(hex: "#FFFDF8")
Ellipse()
.fill(Color.mealMoodMint.opacity(0.28))
.frame(width: 1100, height: 900)
.offset(x: 800, y: -1200)
Ellipse()
.fill(Color.mealMoodCoral.opacity(0.2))
.frame(width: 900, height: 680)
.offset(x: -800, y: 1200)
}
}
@ViewBuilder
private var headerBlock: some View {
switch exportStyle {
case .defaultStyle:
defaultHeader
case .schoolTimetable, .vertical:
elegantHeader
}
}
private var defaultHeader: 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))
VStack(alignment: .leading, spacing: 6) {
Text(exportHeaderTitle)
.font(.system(size: 52, weight: .bold, design: .rounded))
.foregroundColor(.mealMoodTextPrimary)
Text(String(localized: "share_week_title"))
Text(plan.weekStartDate.formattedWeekRange())
.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)
}
Text(exportDateString)
.font(.system(size: 24, weight: .medium, design: .rounded))
.foregroundColor(.mealMoodTextSecondary)
}
.padding(.horizontal, 30)
.padding(.vertical, 24)
.background(Color.white.opacity(0.76))
.background(Color.white.opacity(0.8))
.clipShape(RoundedRectangle(cornerRadius: 24))
.overlay(
RoundedRectangle(cornerRadius: 24)
.stroke(Color(hex: "#F3C4B5").opacity(0.7), lineWidth: 1)
.stroke(Color.mealMoodCoral.opacity(0.5), lineWidth: 1)
)
.shadow(color: Color(hex: "#DDAA99").opacity(0.15), radius: 20, x: 0, y: 12)
}
private var elegantHeader: some View {
VStack(spacing: 8) {
Text(plan.weekStartDate.formattedWeekRange())
.font(.custom("Didot", size: exportStyle == .vertical ? 126 : 108))
.foregroundColor(Color(hex: "#30473F"))
.multilineTextAlignment(.center)
.lineLimit(1)
.minimumScaleFactor(0.45)
.frame(maxWidth: .infinity)
Text(exportHeaderTitle)
.font(.system(size: exportStyle == .vertical ? 58 : 54, weight: .semibold, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
Text(exportDateString)
.font(.system(size: 34, weight: .medium, design: .serif))
.foregroundColor(.mealMoodTextSecondary)
}
.padding(.horizontal, 24)
.padding(.vertical, 24)
.background(Color.white.opacity(0.88))
.clipShape(RoundedRectangle(cornerRadius: 24))
.overlay(
RoundedRectangle(cornerRadius: 24)
.stroke(Color.mealMoodCoral.opacity(0.45), lineWidth: 1)
)
}
private var exportHeaderTitle: String {
switch exportStyle {
case .defaultStyle:
return String(localized: "share_week_title")
case .schoolTimetable:
return String(localized: "share_export_school_title")
case .vertical:
return String(localized: "share_export_vertical_title")
}
}
private var defaultGridLayout: some View {
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(dayShortTitle(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)
}
private var schoolTimetableLayout: some View {
ZStack {
RoundedRectangle(cornerRadius: 28)
.fill(Color.white)
.shadow(color: Color.black.opacity(0.06), radius: 20, x: 0, y: 10)
VStack(spacing: 18) {
Text(String(localized: "share_export_school_subtitle"))
.font(.custom("Didot", size: 48))
.foregroundColor(.mealMoodTextSecondary)
Grid(horizontalSpacing: 12, verticalSpacing: 12) {
GridRow {
Text(" ")
.frame(width: 280)
ForEach(mealTypes, id: \.self) { mealType in
Text(mealTypeLabel(mealType).uppercased(with: locale))
.font(.system(size: 34, weight: .bold, design: .serif))
.foregroundColor(Color(hex: "#355548"))
.frame(maxWidth: .infinity, minHeight: 88)
.background(Color.mealMoodMint.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
ForEach(Array(dayRange), id: \.self) { day in
GridRow {
Text(dayHeaderTitle(for: day).uppercased(with: locale))
.font(.system(size: 30, weight: .bold, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.center)
.frame(width: 280)
.frame(minHeight: 138)
.background(dayHeaderBackground(day: day))
.clipShape(RoundedRectangle(cornerRadius: 12))
ForEach(mealTypes, id: \.self) { mealType in
Text(dishName(day: day, mealType: mealType))
.font(.system(size: 31, weight: .medium, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.leading)
.lineLimit(3)
.frame(maxWidth: .infinity, minHeight: 138, alignment: .leading)
.padding(.horizontal, 16)
.background(Color(hex: "#FFFDFB"))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(hex: "#ECDDD5"), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
}
}
.padding(.horizontal, 24)
.padding(.vertical, 22)
}
}
private var verticalLayout: some View {
ZStack {
RoundedRectangle(cornerRadius: 30)
.fill(Color.white.opacity(0.95))
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.mealMoodMint.opacity(0.45), lineWidth: 2)
)
VStack(spacing: 16) {
ForEach(Array(dayRange), id: \.self) { day in
VStack(alignment: .leading, spacing: 10) {
Text(dayHeaderTitle(for: day).uppercased(with: locale))
.font(.custom("Didot", size: 50))
.foregroundColor(.mealMoodTextPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(dayHeaderBackground(day: day))
.clipShape(RoundedRectangle(cornerRadius: 12))
VStack(alignment: .leading, spacing: 8) {
ForEach(mealTypes, id: \.self) { mealType in
HStack(alignment: .top, spacing: 10) {
Text("\(mealTypeLabel(mealType)):")
.font(.system(size: 34, weight: .bold, design: .serif))
.foregroundColor(Color(hex: "#385549"))
Text(dishName(day: day, mealType: mealType))
.font(.system(size: 35, weight: .regular, design: .serif))
.foregroundColor(.mealMoodTextPrimary)
.lineLimit(2)
.minimumScaleFactor(0.7)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 6)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color(hex: "#EEE4DE"), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
.padding(.horizontal, 22)
.padding(.vertical, 20)
}
}
private var footerBlock: some View {
@@ -211,13 +419,48 @@ struct WeekPlanShareView: View {
}
.padding(.horizontal, 26)
.padding(.vertical, 18)
.background(Color.white.opacity(0.8))
.background(Color.white.opacity(0.82))
.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 dayShortTitle(for day: Int) -> String {
let dayDate = plan.weekStartDate.addingDays(day)
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEE d")
return formatter.string(from: dayDate).capitalized(with: locale)
}
private func dayLongTitle(for day: Int) -> String {
let dayDate = plan.weekStartDate.addingDays(day)
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEEE")
return formatter.string(from: dayDate).capitalized(with: locale)
}
private func dayHeaderTitle(for day: Int) -> String {
let dayDate = plan.weekStartDate.addingDays(day)
let formatter = DateFormatter()
formatter.locale = locale
formatter.setLocalizedDateFormatFromTemplate("EEEE d")
return formatter.string(from: dayDate).capitalized(with: locale)
}
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 func gridHeaderCell(_ title: String, day: Int?) -> some View {
@@ -45,6 +45,15 @@ struct SettingsView: View {
set: { settings.includeWeekends = $0 }
))
.tint(.mealMoodCoral)
Picker("settings_export_style", selection: Binding(
get: { settings.weekExportStyleEnum },
set: { settings.weekExportStyleEnum = $0 }
)) {
ForEach(WeekExportStyle.allCases, id: \.self) { style in
Text(LocalizedStringKey(style.localizedKey)).tag(style)
}
}
} header: {
Label("settings_planning", systemImage: "fork.knife")
}