Version casi lista

This commit is contained in:
alexandrev-tibco
2026-02-17 13:34:02 +01:00
commit e593453abd
99 changed files with 10867 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import SwiftUI
import UIKit
#if canImport(GoogleMobileAds)
import GoogleMobileAds
struct AdBannerView: View {
var adUnitId: String = AdMobConfig.resolvedBannerHomeUnitId
@State private var state: BannerState = .loading
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.mealMoodTextSecondary.opacity(0.22))
.frame(height: 1)
ZStack {
Color.mealMoodSurface
BannerBridge(adUnitId: adUnitId, state: $state)
if state != .loaded {
HStack(spacing: 6) {
Image(systemName: "megaphone")
Text(state == .loading ? "ads_loading" : "ads_unavailable")
.font(.mealMoodCaption)
}
.foregroundColor(.mealMoodTextSecondary)
}
}
.frame(height: 56)
}
.frame(maxWidth: .infinity)
.background(Color.mealMoodSurface)
}
}
private enum BannerState {
case loading
case loaded
case failed
}
private struct BannerBridge: UIViewRepresentable {
let adUnitId: String
@Binding var state: BannerState
func makeCoordinator() -> Coordinator {
Coordinator(state: $state)
}
func makeUIView(context: Context) -> UIView {
let container = UIView()
container.backgroundColor = UIColor(Color.mealMoodSurface)
let width = max(UIScreen.main.bounds.width, 320)
let adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(width)
let banner = GADBannerView(adSize: adSize)
banner.adUnitID = adUnitId
banner.delegate = context.coordinator
banner.rootViewController = rootViewController()
banner.translatesAutoresizingMaskIntoConstraints = false
banner.backgroundColor = .clear
context.coordinator.bannerView = banner
container.addSubview(banner)
NSLayoutConstraint.activate([
banner.centerXAnchor.constraint(equalTo: container.centerXAnchor),
banner.centerYAnchor.constraint(equalTo: container.centerYAnchor),
banner.widthAnchor.constraint(equalToConstant: adSize.size.width),
banner.heightAnchor.constraint(equalToConstant: adSize.size.height)
])
context.coordinator.loadBannerIfPossible()
return container
}
func updateUIView(_ uiView: UIView, context: Context) {
context.coordinator.bannerView?.rootViewController = rootViewController()
context.coordinator.loadBannerIfPossible()
}
private func rootViewController() -> UIViewController? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap(\.windows)
.first(where: \.isKeyWindow)?
.rootViewController
}
final class Coordinator: NSObject, GADBannerViewDelegate {
@Binding var state: BannerState
weak var bannerView: GADBannerView?
private var didRequestLoad = false
private var retryCount = 0
init(state: Binding<BannerState>) {
self._state = state
}
func loadBannerIfPossible() {
guard let bannerView, bannerView.rootViewController != nil else { return }
guard !didRequestLoad else { return }
didRequestLoad = true
state = .loading
bannerView.load(GADRequest())
}
func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
state = .loaded
retryCount = 0
}
func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: any Error) {
state = .failed
didRequestLoad = false
if retryCount < 2 {
retryCount += 1
loadBannerIfPossible()
}
#if DEBUG
print("AdMob banner failed (\(retryCount)): \(error.localizedDescription)")
#endif
}
}
}
#else
struct AdBannerView: View {
var adUnitId: String = AdMobConfig.resolvedBannerHomeUnitId
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.mealMoodTextSecondary.opacity(0.22))
.frame(height: 1)
HStack(spacing: 6) {
Image(systemName: "megaphone")
Text("ads_unavailable")
.font(.mealMoodCaption)
}
.foregroundColor(.mealMoodTextSecondary)
.frame(maxWidth: .infinity, minHeight: 56)
.background(Color.mealMoodSurface)
}
.frame(maxWidth: .infinity)
.background(Color.mealMoodSurface)
}
}
#endif
@@ -0,0 +1,9 @@
import SwiftUI
struct AppIconPlaceholder: View {
var size: CGFloat = 120
var body: some View {
MealMoodLogoMark(size: size)
}
}
+45
View File
@@ -0,0 +1,45 @@
import SwiftUI
struct EmptySlotView: View {
let mealType: MealType
let isDropTarget: Bool
let isValidDrop: Bool?
init(mealType: MealType, isDropTarget: Bool = false, isValidDrop: Bool? = nil) {
self.mealType = mealType
self.isDropTarget = isDropTarget
self.isValidDrop = isValidDrop
}
var body: some View {
VStack(spacing: 6) {
Image(systemName: mealType.icon)
.font(.system(size: 20))
.foregroundColor(Color(hex: "#C4C4C4"))
Text(LocalizedStringKey(mealType.rawValue))
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
}
.frame(maxWidth: .infinity, minHeight: 80)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(hex: "#F9F9F9"))
.overlay(
RoundedRectangle(cornerRadius: 12)
.strokeBorder(
style: StrokeStyle(lineWidth: 2, dash: [5])
)
.foregroundColor(borderColor)
)
)
.animation(.easeInOut(duration: 0.2), value: isDropTarget)
}
private var borderColor: Color {
guard isDropTarget, let valid = isValidDrop else {
return Color(hex: "#E0E0E0")
}
return valid ? .mealMoodSuccess : .mealMoodError
}
}
+57
View File
@@ -0,0 +1,57 @@
import SwiftUI
struct FilledSlotView: View {
let dishName: String
let dishDescription: String?
let tags: [(name: String, color: String)]
let mealType: MealType
var showsRuleWarning: Bool = false
var onRemove: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: mealType.icon)
.font(.system(size: 12))
.foregroundColor(.mealMoodTextSecondary)
if showsRuleWarning {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 12))
.foregroundColor(.mealMoodWarning)
}
Spacer()
if onRemove != nil {
Button {
onRemove?()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 14))
.foregroundColor(.mealMoodTextSecondary.opacity(0.5))
}
}
}
Text(dishName)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.mealMoodTextPrimary)
.lineLimit(1)
.minimumScaleFactor(0.75)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 3) {
ForEach(Array(tags.prefix(2).enumerated()), id: \.offset) { _, tag in
TagDot(color: tag.color, size: 9)
}
if tags.count > 2 {
Text("+\(tags.count - 2)")
.font(.system(size: 10, weight: .medium))
.foregroundColor(.mealMoodTextSecondary)
}
}
}
.padding(10)
.frame(maxWidth: .infinity, minHeight: 80, alignment: .topLeading)
.mealCardStyle()
}
}
@@ -0,0 +1,16 @@
import SwiftUI
struct MealMoodLogoMark: View {
var size: CGFloat = 120
var body: some View {
Image("AppLogo")
.resizable()
.interpolation(.high)
.scaledToFill()
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: size * 0.22, style: .continuous))
.shadow(color: .black.opacity(0.12), radius: size * 0.08, y: size * 0.04)
.accessibilityHidden(true)
}
}
@@ -0,0 +1,23 @@
import SwiftUI
struct PremiumUpsellBanner: View {
let messageKey: LocalizedStringKey
let actionTitleKey: LocalizedStringKey
let action: () -> Void
var body: some View {
HStack(spacing: 8) {
Image(systemName: "star.circle")
Text(messageKey)
Spacer()
Button(actionTitleKey, action: action)
.font(.mealMoodCaption)
.fontWeight(.semibold)
}
.font(.mealMoodCaption)
.foregroundColor(.mealMoodCoral)
.padding(12)
.background(Color.mealMoodSurface)
.cornerRadius(12)
}
}
+43
View File
@@ -0,0 +1,43 @@
import SwiftUI
struct PrimaryButton: View {
let title: String
var icon: String? = nil
let action: () -> Void
var isEnabled: Bool = true
var localizeTitle: Bool = true
var body: some View {
Button(action: action) {
HStack(spacing: 8) {
Group {
if localizeTitle {
Text(LocalizedStringKey(title))
} else {
Text(verbatim: title)
}
}
.font(.mealMoodBodyBold)
if let icon = icon {
Text(icon)
}
}
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 14)
.frame(maxWidth: .infinity)
.background(
LinearGradient(
colors: isEnabled
? [Color.mealMoodCoral, Color.mealMoodMint]
: [Color.gray.opacity(0.3), Color.gray.opacity(0.3)],
startPoint: .leading,
endPoint: .trailing
)
)
.cornerRadius(14)
.shadow(color: isEnabled ? Color.mealMoodCoral.opacity(0.3) : .clear, radius: 8, y: 4)
}
.disabled(!isEnabled)
}
}
+36
View File
@@ -0,0 +1,36 @@
import SwiftUI
struct SecondaryButton: View {
let title: String
var icon: String? = nil
let action: () -> Void
var localizeTitle: Bool = true
var body: some View {
Button(action: action) {
HStack(spacing: 8) {
Group {
if localizeTitle {
Text(LocalizedStringKey(title))
} else {
Text(verbatim: title)
}
}
.font(.mealMoodBodyBold)
if let icon = icon {
Text(icon)
}
}
.foregroundColor(.mealMoodTextPrimary)
.padding(.horizontal, 24)
.padding(.vertical, 14)
.frame(maxWidth: .infinity)
.background(Color.mealMoodSurface)
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(Color.mealMoodTextSecondary.opacity(0.3), lineWidth: 1.5)
)
.cornerRadius(14)
}
}
}
+33
View File
@@ -0,0 +1,33 @@
import SwiftUI
struct TagPill: View {
let name: String
let color: String
var body: some View {
Text(name)
.font(.mealMoodCaption)
.foregroundColor(.white)
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(
Capsule()
.fill(Color(hex: color))
)
}
}
struct TagDot: View {
let color: String
var size: CGFloat = 10
var body: some View {
Circle()
.fill(Color(hex: color))
.frame(width: size, height: size)
.overlay(
Circle()
.stroke(Color.white.opacity(0.7), lineWidth: 1)
)
}
}
+55
View File
@@ -0,0 +1,55 @@
import SwiftUI
struct ToastView: View {
let message: String
@Binding var isShowing: Bool
var body: some View {
if isShowing {
VStack {
HStack(spacing: 8) {
Text(message)
.font(.mealMoodSmall)
.foregroundColor(.white)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background(
Capsule()
.fill(Color.mealMoodCoral.opacity(0.95))
.shadow(color: .black.opacity(0.1), radius: 8, y: 4)
)
Spacer()
}
.padding(.top, 8)
.transition(.move(edge: .top).combined(with: .opacity))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation(.easeOut) {
isShowing = false
}
}
}
}
}
}
struct ToastModifier: ViewModifier {
@Binding var isShowing: Bool
let message: String
func body(content: Content) -> some View {
ZStack {
content
ToastView(message: message, isShowing: $isShowing)
}
}
}
extension View {
func toast(isShowing: Binding<Bool>, message: String) -> some View {
modifier(ToastModifier(isShowing: isShowing, message: message))
}
}