2.0: dictado de platos/ingredientes + calendario más alto en iPad/Mac

- Dictado por voz (Speech/SFSpeechRecognizer, on-device) en el nombre de
  plato y en ingredientes, con transcripción en vivo.
- Un solo texto dictado se separa en ingredientes individuales vía Apple
  Foundation Models on-device (IngredientParser), con fallback heurístico.
- Permisos de micrófono y reconocimiento de voz en Info.plist.
- Cadenas de dictado en los 6 idiomas.
- iPad/Mac: el calendario reclama ~50% de la altura disponible en lugar
  de quedar fijo a ~240px; iPhone (compact) sin cambios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3HaWmmtTQ1vdTERtSYU6p
This commit is contained in:
alexandrev-tibco
2026-07-22 18:16:51 +02:00
parent cbcccd66ed
commit 8e4d0c2d89
14 changed files with 442 additions and 52 deletions
+45 -33
View File
@@ -44,6 +44,16 @@ struct HomeView: View {
private var isRunningOnMac: Bool { ProcessInfo.processInfo.isiOSAppOnMac }
private var contentHorizontalPadding: CGFloat { (isRunningOnMac || horizontalSizeClass == .regular) ? 14 : 0 }
/// On iPad/Mac (regular width) let the calendar claim roughly half of the
/// available height so it feels like the centerpiece instead of a strip at
/// the top. On iPhone (compact) return nil to keep the intrinsic sizing.
private func calendarHeight(availableHeight: CGFloat) -> CGFloat? {
guard horizontalSizeClass == .regular, availableHeight > 0 else { return nil }
let target = availableHeight * 0.5
// Keep at least ~260pt for the dish drawer below.
return min(max(target, 300), availableHeight - 260)
}
var body: some View {
NavigationStack {
ZStack {
@@ -249,45 +259,47 @@ struct HomeView: View {
.padding(.bottom, 8)
}
VStack(spacing: 10) {
WeekCalendarView(
plan: plan,
settings: settings,
dishes: dishes,
tags: tags,
viewModel: viewModel,
onTapEmptySlot: { slot in
selectedEmptySlotId = slot.id
},
onTapFilledSlot: { slot in
selectedFilledSlotId = slot.id
GeometryReader { contentGeo in
VStack(spacing: 10) {
WeekCalendarView(
plan: plan,
settings: settings,
dishes: dishes,
tags: tags,
viewModel: viewModel,
onTapEmptySlot: { slot in
selectedEmptySlotId = slot.id
},
onTapFilledSlot: { slot in
selectedFilledSlotId = slot.id
}
)
.frame(height: calendarHeight(availableHeight: contentGeo.size.height))
let filledCount = plan.slotList.filter { $0.dishId != nil || $0.isEatingOut }.count
if filledCount > 0 {
exportCallout(plan: plan, settings: settings)
}
)
let filledCount = plan.slotList.filter { $0.dishId != nil || $0.isEatingOut }.count
if filledCount > 0 {
exportCallout(plan: plan, settings: settings)
}
if !viewModel.canEditCurrentWeek && filledCount > 0 && settings.isPremium {
weekRatingRow(plan: plan)
}
if !viewModel.canEditCurrentWeek && filledCount > 0 && settings.isPremium {
weekRatingRow(plan: plan)
}
let emptyCount = plan.slotList.filter { $0.dishId == nil && !$0.isEatingOut }.count
if viewModel.canEditCurrentWeek && !dishes.isEmpty && emptyCount > 0 {
autoAssignBanner(emptyCount: emptyCount, plan: plan, settings: settings)
}
let emptyCount = plan.slotList.filter { $0.dishId == nil && !$0.isEatingOut }.count
if viewModel.canEditCurrentWeek && !dishes.isEmpty && emptyCount > 0 {
autoAssignBanner(emptyCount: emptyCount, plan: plan, settings: settings)
ScrollView(showsIndicators: true) {
dishDrawer(plan: plan, settings: settings)
.padding(.bottom, 0)
}
.frame(maxHeight: .infinity)
}
ScrollView(showsIndicators: true) {
dishDrawer(plan: plan, settings: settings)
.padding(.bottom, 0)
}
.frame(maxHeight: .infinity)
.frame(maxWidth: .infinity, minHeight: contentGeo.size.height, alignment: .top)
.padding(.horizontal, contentHorizontalPadding)
.padding(.bottom, 0)
}
.frame(maxWidth: .infinity)
.frame(maxWidth: .infinity, alignment: .top)
.padding(.horizontal, contentHorizontalPadding)
.padding(.bottom, 0)
}
.frame(maxHeight: .infinity, alignment: .top)
.safeAreaInset(edge: .bottom, spacing: 0) {
+8 -9
View File
@@ -95,16 +95,16 @@ struct WeekCalendarView: View {
let spacing: CGFloat = 10
let rowLabelWidth: CGFloat = 86
let headerHeight: CGFloat = 26
let slotRowHeight: CGFloat = 96
let verticalInset: CGFloat = 2
let rowCount = CGFloat(max(mealTypes.count, 1))
let interRowDividers = CGFloat(max(0, mealTypes.count - 1))
let contentWidth = proxy.size.width - rowLabelWidth - (dayCount * spacing)
let dayWidth = max(92, contentWidth / max(dayCount, 1))
let totalHeight =
(verticalInset * 2) +
headerHeight +
1 +
(CGFloat(mealTypes.count) * slotRowHeight) +
CGFloat(max(0, mealTypes.count - 1))
// Distribute the available height across the meal rows so the calendar
// grows to fill its container on iPad/Mac instead of staying tiny.
let chromeHeight = (verticalInset * 2) + headerHeight + 1 + interRowDividers
let availableForRows = proxy.size.height - chromeHeight
let slotRowHeight = max(96, availableForRows / rowCount)
VStack(spacing: 0) {
HStack(spacing: 0) {
@@ -144,9 +144,8 @@ struct WeekCalendarView: View {
}
.padding(.horizontal, 16)
.padding(.vertical, verticalInset)
.frame(height: totalHeight, alignment: .top)
.frame(height: proxy.size.height, alignment: .top)
}
.frame(height: CGFloat(mealTypes.count) * 98 + 44)
}
private func calendarDivider(height: CGFloat? = nil) -> some View {