/** * L'anteprima di un import: cosa c'è nel file e cosa succederebbe a ognuna * delle sue voci, prima di scrivere. Ci si arriva da Impostazioni → Importa. * * Legge senza scrivere. Il piano lo calcola src/io/piano.ts, lo stesso * confronto che l'import poi esegue: qui si mostra e basta. Il testo del * tasto e il segno di ogni riga li decide scambio.ts, che i test caricano. * * Un file che non si riesce a leggere e uno che non è nostro sono lo stesso * rifiuto, detto nella pagina e non in un Alert: chi lo legge può tornare * indietro con calma. */ import { useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { File } from 'expo-file-system'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { categorieDaEsportare } from '../../data/categorie.ts'; import { ricetteDaEsportare } from '../../data/ricette.ts'; import type { RicettarioLetto } from '../../io/formato.ts'; import { importaLetto, leggiFile } from '../../io/importa.ts'; import { pianoImport } from '../../io/piano.ts'; import type { Piano } from '../../io/piano.ts'; import Barra from '../componenti/Barra.tsx'; import { useApp } from '../contesto.ts'; import type { PropsSchermata } from '../navigazione.ts'; import { righeAnteprima, righeImport, tastoImporta } from '../scambio.ts'; import type { RigaAnteprima } from '../scambio.ts'; import { creaScrittoreUnico } from '../scritturaUnica.ts'; import type { Colori } from '../tema.ts'; import { RAGGIO, SPAZIO, TESTO, useColori } from '../tema.ts'; type Letto = { file: RicettarioLetto; foto: Map }; type Stato = | { nome: 'lettura' } | { nome: 'rifiutato' } | { nome: 'pronto'; letto: Letto; piano: Piano }; export default function Anteprima({ navigation, route }: PropsSchermata<'Anteprima'>) { const { db, lingua, testo } = useApp(); const colori = useColori(); const stili = useMemo(() => creaStili(colori), [colori]); const sotto = useSafeAreaInsets().bottom; const [stato, setStato] = useState({ nome: 'lettura' }); const [inCorso, setInCorso] = useState(false); const scrittoreUnico = useRef(creaScrittoreUnico()).current; useEffect(() => { let vivo = true; // Un secondo file mentre il primo è a schermo: via il piano vecchio col suo // tasto attivo, finché il nuovo non è letto. setStato({ nome: 'lettura' }); (async () => { const uri = route.params.uri; let letto: Letto | null = null; try { letto = await leggiFile(await new File(uri).bytes()); // iOS copia in Documents/Inbox i file aperti da altre app e non la svuota mai. if (uri.includes('/Inbox/')) { try { new File(uri).delete(); } catch {} } } catch { letto = null; } if (!vivo) return; if (letto === null) { setStato({ nome: 'rifiutato' }); return; } let ricette; let categorie; try { [ricette, categorie] = await Promise.all([ricetteDaEsportare(db), categorieDaEsportare(db)]); } catch { // Senza la fotografia locale il piano non si può fare: si dice come un // rifiuto, che è l'unico stato senza tasto. if (vivo) setStato({ nome: 'rifiutato' }); return; } if (!vivo) return; setStato({ nome: 'pronto', letto, piano: pianoImport(letto.file, ricette, categorie) }); })(); return () => { vivo = false; }; }, [db, route.params.uri]); const importa = () => scrittoreUnico(async () => { if (stato.nome !== 'pronto') return; setInCorso(true); try { const esito = await importaLetto(db, stato.letto); const righe = righeImport(esito, lingua) .map((r) => (r.etichetta === null ? '' : `${testo(r.etichetta)}: `) + testo(r.chiave, r.valori)) .join('\n'); Alert.alert( testo('anteprima.titolo'), righe, [{ text: 'OK', onPress: () => navigation.goBack() }], { cancelable: false }, ); } finally { setInCorso(false); } }); const riga = (r: RigaAnteprima) => ( {r.nome} {testo(r.segno)} {r.foto ? ` · ${testo('anteprima.foto')}` : ''} ); const rifiuto = righeImport({ ok: false, motivo: 'file-non-valido' }, lingua)[0]; const tasto = stato.nome === 'pronto' ? tastoImporta(stato.piano, lingua) : null; const attivo = tasto !== null && tasto.chiave !== 'anteprima.importa.zero' && !inCorso; return ( {stato.nome === 'lettura' ? ( ) : stato.nome === 'rifiutato' ? ( {testo(rifiuto.chiave, rifiuto.valori)} ) : ( <> {righeAnteprima(stato.piano.ricette).map(riga)} {stato.piano.categorie.length > 0 ? ( <> {testo('anteprima.categorie')} {righeAnteprima(stato.piano.categorie).map(riga)} ) : null} void importa()} disabled={!attivo} style={[stili.tasto, attivo ? null : stili.tastoSpento]} accessibilityRole="button" accessibilityState={{ disabled: !attivo }} > {inCorso ? ( ) : ( {tasto === null ? '' : testo(tasto.chiave, tasto.valori)} )} )} ); } const creaStili = (colori: Colori) => StyleSheet.create({ pagina: { flex: 1, backgroundColor: colori.fondo }, centro: { flex: 1 }, contenuto: { paddingHorizontal: SPAZIO.l, paddingBottom: SPAZIO.xl }, riga: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'space-between', gap: SPAZIO.m, minHeight: 44, paddingVertical: SPAZIO.s, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colori.bordo, }, nome: { ...TESTO.corpo, color: colori.testo, flexShrink: 1 }, segno: { ...TESTO.piccolo, color: colori.accento }, segnoFermo: { color: colori.testoTenue }, // Come le intestazioni di Impostazioni: piccole, maiuscole, tenui. sezione: { ...TESTO.piccolo, textTransform: 'uppercase', color: colori.testoTenue, marginTop: SPAZIO.xl, marginBottom: SPAZIO.s, }, rifiuto: { ...TESTO.corpo, color: colori.testo, padding: SPAZIO.xl, textAlign: 'center' }, azioni: { padding: SPAZIO.l }, // Il tasto è quello del foglio di GestioneCategorie, largo quanto la pagina. tasto: { backgroundColor: colori.accento, borderRadius: RAGGIO.campo, paddingVertical: SPAZIO.m, paddingHorizontal: SPAZIO.xl, alignItems: 'center', }, tastoSpento: { backgroundColor: colori.bordo }, tastoTesto: { ...TESTO.corpo, fontWeight: '600', color: colori.accentoSopra }, tastoTestoSpento: { color: colori.testoTenue }, });