/** * Il foglio dove si scrive un ingrediente: quantità, unità, nome, e la sezione * quando la ricetta ne ha più di una. * * Due modi. «Nuovo»: «Aggiungi» mette la riga nella lista e il foglio resta * aperto e vuoto, col cursore sulla quantità, così si scrive il prossimo * senza chiudere niente; sotto la testata compare «Aggiunto: 80 g * Parmigiano», che è la risposta al gesto (la lista dietro è coperta). «Fine» * chiude, e se c'è un ingrediente scritto e non ancora aggiunto lo aggiunge: * è lavoro dell'utente, e buttarlo perché ha toccato il comando a sinistra * sarebbe il contrario di quello che si aspetta. «Modifica»: «Fatto» applica, * «Annulla» lascia com'era, e in fondo c'è «Rimuovi ingrediente». * * Il tasto della tastiera porta avanti, quantità, unità, nome, e sul nome * aggiunge (o conferma): con la tastiera aperta non serve toccare niente. * * L'unità si sceglie coi gettoni sotto il campo (le più usate) o si scrive a * mano; toccare il gettone già scelto la toglie. Chi scrive «200 g» nella * quantità si ritrova 200 e g nei due campi (dividiQuantita): prima quella riga * si salvava come q.b. senza dire niente. * * Le decisioni stanno in logica-modifica.ts, qui c'è solo il disegno. * * I colori non si scrivono qui: vengono da tema.ts, come nelle altre schermate. */ import { useMemo, useRef, useState } from 'react'; import type { TextInput } from 'react-native'; import { AccessibilityInfo, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import type { Lingua } from '../../domain/lingua/index.ts'; import { useApp } from '../contesto.ts'; import type { CampiRiga } from '../bozza.ts'; import { dividiQuantita, quantitaIllegibile, sipuoConfermareRiga, testoQuantitaRiga, unitaDopoTocco, unitaRapide, } from '../logica-modifica.ts'; import type { Colori } from '../tema.ts'; import { RAGGIO, SPAZIO, TESTO, useColori } from '../tema.ts'; import { ComandoLeggero } from './Bottoni.tsx'; import { CampoTesto } from './Campo.tsx'; import { Foglio } from './Foglio.tsx'; import Segno from './Segno.tsx'; import { TestaFoglio } from './TestaFoglio.tsx'; export type PropsFoglioIngrediente = { visibile: boolean; /** Cambia a ogni apertura: i campi ripartono da `iniziale`. */ apertura: number; modo: 'nuovo' | 'modifica'; /** I campi con cui si apre: vuoti per un ingrediente nuovo. */ iniziale: CampiRiga; /** La riga arriva dal parser e non l'ha capita: si spiega cosa fare. */ incerta: boolean; gruppoIniziale: string; /** Vuoto se la ricetta ha una sezione sola: allora la scelta non compare. */ sezioni: { id: string; nome: string }[]; lingua: Lingua; onAggiungi: (campi: CampiRiga, gruppoId: string) => void; onFatto: (campi: CampiRiga, gruppoId: string) => void; onRimuovi: () => void; onChiudi: () => void; }; const VUOTI: CampiRiga = { quantita: '', unita: '', nome: '' }; export function FoglioIngrediente({ visibile, apertura, modo, iniziale, incerta, gruppoIniziale, sezioni, lingua, onAggiungi, onFatto, onRimuovi, onChiudi, }: PropsFoglioIngrediente) { const { testo } = useApp(); const colori = useColori(); const stili = useMemo(() => creaStili(colori), [colori]); const sotto = useSafeAreaInsets().bottom; const [campi, setCampi] = useState(iniziale); const [gruppo, setGruppo] = useState(gruppoIniziale); const [aggiunto, setAggiunto] = useState(null); const quantitaRef = useRef(null); const unitaRef = useRef(null); const nomeRef = useRef(null); // Ogni apertura riparte da quello che dice chi apre, già nel primo disegno // (lo stato derivato di React: si aggiorna durante il render). Con un // effetto il foglio mostrava per un attimo la riga di prima; con una `key` // che lo rimontava già aperto, sull'iPhone la tastiera arrivava prima che il // foglio sapesse dov'era, e restava coperto (collaudo del 28/09). const [aperturaVista, setAperturaVista] = useState(apertura); if (apertura !== aperturaVista) { setAperturaVista(apertura); setCampi(iniziale); setGruppo(gruppoIniziale); setAggiunto(null); } const cambia = (c: Partial) => setCampi((prima) => ({ ...prima, ...c })); // «200 g» nella quantità: il numero resta lì e l'unità passa nel suo campo. const conUnitaSeparata = (c: CampiRiga): CampiRiga => ({ ...c, ...dividiQuantita(c.quantita, c.unita, lingua) }); const pulita = (c: CampiRiga): CampiRiga => ({ quantita: c.quantita.trim(), unita: c.unita.trim(), nome: c.nome.trim() }); // La quantità si guarda dopo la divisione: «200 g» è leggibile, perché // uscendo dal campo diventa 200 e g. const illegibile = quantitaIllegibile(conUnitaSeparata(campi).quantita, lingua); const confermabile = sipuoConfermareRiga(campi.nome) && !illegibile; const aggiungi = () => { if (!confermabile) return; const riga = pulita(conUnitaSeparata(campi)); onAggiungi(riga, gruppo); const q = testoQuantitaRiga(riga, lingua); setAggiunto(`${q.testo} ${riga.nome}`); // La lista dietro è coperta: chi usa VoiceOver deve sentire che è entrata. AccessibilityInfo.announceForAccessibility(testo('ingrediente.aggiunto', { riga: `${q.testo} ${riga.nome}` })); setCampi(VUOTI); quantitaRef.current?.focus(); }; const fatto = () => { if (!confermabile) return; onFatto(pulita(conUnitaSeparata(campi)), gruppo); onChiudi(); }; const fine = () => { if (confermabile) onAggiungi(pulita(conUnitaSeparata(campi)), gruppo); setCampi(VUOTI); onChiudi(); }; const nuovo = modo === 'nuovo'; return ( (incerta ? nomeRef : quantitaRef).current?.focus()} > {aggiunto !== null ? ( {testo('ingrediente.aggiunto', { riga: aggiunto })} ) : null} {incerta ? {testo('ingrediente.incerta')} : null} cambia({ quantita: v })} onBlur={() => setCampi((c) => conUnitaSeparata(c))} // Su iOS dà la tastiera coi numeri ma lascia scrivere 1/2 e 1,5; // su Android non esiste e si ricade sulla tastiera normale. keyboardType="numbers-and-punctuation" returnKeyType="next" submitBehavior="submit" onSubmitEditing={() => unitaRef.current?.focus()} numerico stile={stili.quantita} /> cambia({ unita: v })} autoCapitalize="none" autoCorrect={false} returnKeyType="next" submitBehavior="submit" onSubmitEditing={() => nomeRef.current?.focus()} stile={stili.unita} /> {unitaRapide(lingua).map((u) => { const scelta = campi.unita.trim() === u; return ( cambia({ unita: unitaDopoTocco(campi.unita, u) })} /> ); })} {illegibile ? ( {testo('ingrediente.quantita.illegibile')} ) : ( {testo('ingrediente.quantita.aiuto')} )} cambia({ nome: v })} returnKeyType="done" // Sul nome il tasto aggiunge e la tastiera resta: il prossimo si // scrive subito. Nel modo «modifica» conferma e chiude. submitBehavior={nuovo ? 'submit' : 'blurAndSubmit'} onSubmitEditing={nuovo ? aggiungi : fatto} /> {sezioni.length > 0 ? ( {testo('ingrediente.sezione')} {sezioni.map((s) => ( setGruppo(s.id)} /> ))} ) : null} {!nuovo ? ( { onRimuovi(); onChiudi(); }} stile={stili.rimuovi} /> ) : null} ); } function Gettone({ testo, scelto, onPress, stili, colori, }: { testo: string; scelto: boolean; onPress: () => void; stili: ReturnType; colori: Colori; }) { return ( [stili.gettone, scelto ? stili.gettoneScelto : null, pressed ? stili.premuto : null]} > {scelto ? : null} {testo} ); } const creaStili = (colori: Colori) => StyleSheet.create({ foglio: { backgroundColor: colori.fondo, borderTopLeftRadius: 22, borderTopRightRadius: 22, flexShrink: 1, }, scorre: { flexShrink: 1 }, dentro: { paddingHorizontal: SPAZIO.l, paddingTop: SPAZIO.s, gap: SPAZIO.m }, aggiunto: { flexDirection: 'row', alignItems: 'center', gap: 6 }, aggiuntoTesto: { flex: 1, ...TESTO.piccolo, color: colori.testoTenue }, incerta: { ...TESTO.piccolo, color: colori.testo, backgroundColor: colori.attenzioneTenue, borderRadius: RAGGIO.campo, padding: SPAZIO.m, overflow: 'hidden', }, coppia: { flexDirection: 'row', gap: SPAZIO.m }, quantita: { width: 120 }, unita: { flex: 1 }, gettoni: { flexDirection: 'row', gap: SPAZIO.s }, gettone: { minHeight: 36, borderRadius: RAGGIO.tondo, paddingHorizontal: 13, flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: colori.superficie, borderWidth: 1, borderColor: colori.bordo, }, // La scelta è in inchiostro, non in accento: l'accento pieno è il // bottone principale, e un gettone scelto non è un comando. gettoneScelto: { backgroundColor: colori.testo, borderColor: colori.testo }, gettoneTesto: { ...TESTO.etichetta, color: colori.testo }, gettoneTestoScelto: { color: colori.fondo, fontWeight: '600' }, premuto: { opacity: 0.6 }, aiuto: { ...TESTO.piccolo, color: colori.testoTenue, marginTop: -SPAZIO.xs }, illegibile: { ...TESTO.piccolo, color: colori.attenzione, marginTop: -SPAZIO.xs }, blocco: { gap: 6 }, etichetta: { ...TESTO.piccolo, fontWeight: '600', color: colori.testoTenue }, rimuovi: { marginTop: SPAZIO.xs }, });