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