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
+20
View File
@@ -10,6 +10,7 @@ final class DishViewModel: ObservableObject {
@Published var ingredients: [String] = []
@Published var photoData: Data?
@Published var isGeneratingIngredients: Bool = false
@Published var isParsingIngredients: Bool = false
@Published var showTagSelector: Bool = false
@Published var showDeleteAlert: Bool = false
@Published var showAssignedDeleteAlert: Bool = false
@@ -60,6 +61,25 @@ final class DishViewModel: ObservableObject {
}
}
/// Splits a dictated phrase into individual ingredient lines and appends
/// them to the current draft, dropping any empty placeholder rows first.
/// No-op on empty input or while a parse is already running.
func addDictatedIngredients(_ text: String, language: AppLanguage) {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, !isParsingIngredients else { return }
isParsingIngredients = true
Task {
let lines = await IngredientParser.parse(trimmed, language: language)
await MainActor.run {
self.isParsingIngredients = false
guard !lines.isEmpty else { return }
self.ingredients.removeAll { $0.trimmingCharacters(in: .whitespaces).isEmpty }
self.ingredients.append(contentsOf: lines)
AnalyticsService.logIngredientsGenerated(lineCount: lines.count, source: "dictation")
}
}
}
func save(context: ModelContext) {
let trimmedName = name.trimmingCharacters(in: .whitespaces)
guard !trimmedName.isEmpty else { return }