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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user