Files
avante-movil/src/screens/PhotoSettingsScreen.tsx
T

382 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Configuración del pie de página en fotos.
* Permite activar/desactivar el footer, cambiar el logo, activar/desactivar
* cada campo (proyecto, fecha, coordenadas) y añadir campos personalizados
* con etiqueta y valor estático.
*/
import React, { useCallback, useEffect, useState } from 'react';
import {
Alert,
Image,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { newUuid } from '../sync/uuid';
import {
clearLogo,
FooterConfig,
FooterField,
loadFooterConfig,
OverlayPosition,
pickAndSaveLogo,
PhotoResolution,
saveFooterConfig,
} from '../photo/footerConfig';
import { ChipSelect, COLORS, PrimaryButton, SectionTitle } from '../ui/components';
const BUILT_IN_LABELS: Record<string, string> = {
project_name: 'Nombre del proyecto',
date: 'Fecha y hora',
coordinates: 'Coordenadas GPS',
};
const RESOLUTION_OPTIONS: PhotoResolution[] = ['low', 'medium', 'high'];
const QUALITY_OPTIONS = ['0.5', '0.7', '0.85', '1.0'];
const POSITION_OPTIONS: OverlayPosition[] = ['bottom-left', 'bottom-right', 'top-left', 'top-right'];
export function PhotoSettingsScreen() {
const [config, setConfig] = useState<FooterConfig | null>(null);
const [newLabel, setNewLabel] = useState('');
const [newValue, setNewValue] = useState('');
const [showAddForm, setShowAddForm] = useState(false);
const load = useCallback(async () => {
setConfig(await loadFooterConfig());
}, []);
useEffect(() => { void load(); }, [load]);
const save = useCallback(async (next: FooterConfig) => {
setConfig(next);
await saveFooterConfig(next);
}, []);
const toggleEnabled = useCallback((val: boolean) => {
if (!config) return;
void save({ ...config, enabled: val });
}, [config, save]);
const toggleField = useCallback((id: string, val: boolean) => {
if (!config) return;
void save({
...config,
fields: config.fields.map((f) => (f.id === id ? { ...f, enabled: val } : f)),
});
}, [config, save]);
const editFieldLabel = useCallback((id: string, label: string) => {
if (!config) return;
const next = {
...config,
fields: config.fields.map((f) => (f.id === id ? { ...f, label } : f)),
};
void save(next);
}, [config, save]);
const deleteCustomField = useCallback((id: string) => {
if (!config) return;
void save({ ...config, fields: config.fields.filter((f) => f.id !== id) });
}, [config, save]);
const addCustomField = useCallback(() => {
if (!config) return;
const label = newLabel.trim();
const value = newValue.trim();
if (!label) { Alert.alert('', 'La etiqueta no puede estar vacía.'); return; }
const field: FooterField = {
id: newUuid(),
key: 'custom',
label,
enabled: true,
customValue: value || label,
};
void save({ ...config, fields: [...config.fields, field] });
setNewLabel('');
setNewValue('');
setShowAddForm(false);
}, [config, newLabel, newValue, save]);
const onChangeLogo = useCallback(async () => {
if (!config) return;
const uri = await pickAndSaveLogo();
if (uri) void save({ ...config, logoUri: uri });
}, [config, save]);
const onRemoveLogo = useCallback(() => {
if (!config) return;
Alert.alert('Quitar logo', '¿Eliminar el logo del pie de página?', [
{
text: 'Quitar', style: 'destructive', onPress: () => {
void clearLogo();
void save({ ...config, logoUri: null });
},
},
{ text: 'Cancelar', style: 'cancel' },
]);
}, [config, save]);
if (!config) return null;
return (
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
{/* Master toggle */}
<View style={styles.row}>
<Text style={styles.masterLabel}>Añadir pie de página a las fotos</Text>
<Switch
value={config.enabled}
onValueChange={toggleEnabled}
trackColor={{ true: COLORS.primary }}
/>
</View>
{/* ── Logo ── */}
<SectionTitle>Logo</SectionTitle>
<View style={styles.logoRow}>
{config.logoUri ? (
<Image source={{ uri: config.logoUri }} style={styles.logoPreview} resizeMode="contain" />
) : (
<View style={styles.logoPlaceholder}>
<Text style={styles.logoPlaceholderText}>Sin logo</Text>
</View>
)}
<View style={styles.logoActions}>
<PrimaryButton
title={config.logoUri ? 'Cambiar' : 'Seleccionar'}
variant="ghost"
onPress={() => void onChangeLogo()}
/>
{config.logoUri ? (
<PrimaryButton title="Quitar" variant="danger" onPress={onRemoveLogo} />
) : null}
</View>
</View>
<Text style={styles.hint}>
Recorte cuadrado. Se recomienda logo con fondo blanco.
</Text>
{/* ── Resolución y Calidad ── */}
<SectionTitle>Calidad de imagen</SectionTitle>
<View style={styles.card}>
<ChipSelect
label="Resolución máxima"
value={config.resolution}
options={RESOLUTION_OPTIONS}
onChange={(v) => void save({ ...config, resolution: v })}
/>
<Text style={styles.hint}>
Low (~720p), Medium (~1080p), High (~4K).
</Text>
<View style={{ height: 12 }} />
<ChipSelect
label="Calidad de compresión"
value={String(config.quality)}
options={QUALITY_OPTIONS}
onChange={(v) => void save({ ...config, quality: parseFloat(v) })}
/>
<Text style={styles.hint}>
Valores bajos reducen el tamaño del archivo pero pueden verse peor.
</Text>
<View style={{ height: 12 }} />
<ChipSelect
label="Posición del sello"
value={config.overlayPosition}
options={POSITION_OPTIONS}
onChange={(v) => void save({ ...config, overlayPosition: v })}
/>
<Text style={styles.hint}>
Esquina de la imagen donde aparecerán los datos.
</Text>
</View>
{/* ── Campos ── */}
<SectionTitle>Campos</SectionTitle>
{config.fields.map((f) => {
const isBuiltIn = f.key !== 'custom';
return (
<View key={f.id} style={styles.fieldRow}>
<Switch
value={f.enabled}
onValueChange={(v) => toggleField(f.id, v)}
trackColor={{ true: COLORS.primary }}
/>
<View style={styles.fieldInfo}>
<Text style={styles.fieldKey}>
{isBuiltIn ? BUILT_IN_LABELS[f.key] ?? f.label : ''}
</Text>
<TextInput
style={styles.fieldLabel}
value={f.label}
onChangeText={(t) => editFieldLabel(f.id, t)}
placeholder="Etiqueta…"
returnKeyType="done"
/>
{f.key === 'custom' && (
<TextInput
style={[styles.fieldLabel, styles.fieldValue]}
value={f.customValue ?? ''}
onChangeText={(t) => {
if (!config) return;
void save({
...config,
fields: config.fields.map((x) =>
x.id === f.id ? { ...x, customValue: t } : x,
),
});
}}
placeholder="Valor…"
returnKeyType="done"
/>
)}
</View>
{!isBuiltIn && (
<TouchableOpacity
onPress={() => deleteCustomField(f.id)}
style={styles.deleteBtn}
>
<Text style={styles.deleteTxt}></Text>
</TouchableOpacity>
)}
</View>
);
})}
{/* Añadir campo personalizado */}
{showAddForm ? (
<View style={styles.addForm}>
<TextInput
style={styles.addInput}
placeholder="Etiqueta (p. ej. Empresa)"
value={newLabel}
onChangeText={setNewLabel}
returnKeyType="next"
/>
<TextInput
style={styles.addInput}
placeholder="Valor (p. ej. Constructora ABC)"
value={newValue}
onChangeText={setNewValue}
returnKeyType="done"
/>
<View style={styles.addFormBtns}>
<View style={{ flex: 1 }}>
<PrimaryButton title="Añadir" onPress={addCustomField} />
</View>
<View style={{ flex: 1 }}>
<PrimaryButton title="Cancelar" variant="ghost" onPress={() => setShowAddForm(false)} />
</View>
</View>
</View>
) : (
<TouchableOpacity style={styles.addFieldBtn} onPress={() => setShowAddForm(true)}>
<Text style={styles.addFieldTxt}> Añadir campo personalizado</Text>
</TouchableOpacity>
)}
<View style={{ height: 40 }} />
</ScrollView>
);
}
// ─── estilos ──────────────────────────────────────────────────────────────
const styles = StyleSheet.create({
body: { padding: 16 },
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.border,
marginBottom: 8,
},
masterLabel: { fontSize: 16, fontWeight: '600', flex: 1, marginRight: 12 },
/* Logo */
logoRow: { flexDirection: 'row', alignItems: 'center', gap: 16, marginBottom: 4 },
logoPreview: { width: 72, height: 72, borderRadius: 8, backgroundColor: COLORS.bg },
logoPlaceholder: {
width: 72,
height: 72,
borderRadius: 8,
backgroundColor: COLORS.bg,
justifyContent: 'center',
alignItems: 'center',
borderWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.border,
},
logoPlaceholderText: { fontSize: 11, color: COLORS.muted },
logoActions: { gap: 8 },
hint: { fontSize: 11, color: COLORS.muted, marginBottom: 8 },
card: {
backgroundColor: COLORS.bg,
borderRadius: 8,
padding: 12,
marginBottom: 8,
},
/* Campos */
fieldRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderColor: COLORS.border,
},
fieldInfo: { flex: 1 },
fieldKey: { fontSize: 11, color: COLORS.muted, marginBottom: 2 },
fieldLabel: {
fontSize: 14,
color: '#111',
borderBottomWidth: 1,
borderColor: COLORS.border,
paddingVertical: 2,
paddingHorizontal: 0,
},
fieldValue: { marginTop: 4, color: COLORS.muted },
deleteBtn: { padding: 8 },
deleteTxt: { color: COLORS.danger, fontSize: 16 },
/* Nuevo campo */
addFieldBtn: {
marginTop: 16,
paddingVertical: 12,
alignItems: 'center',
borderWidth: 1,
borderStyle: 'dashed',
borderColor: COLORS.primary,
borderRadius: 8,
},
addFieldTxt: { color: COLORS.primary, fontWeight: '600' },
addForm: {
marginTop: 12,
padding: 12,
backgroundColor: COLORS.bg,
borderRadius: 8,
gap: 10,
},
addInput: {
borderWidth: 1,
borderColor: COLORS.border,
borderRadius: 6,
paddingHorizontal: 10,
paddingVertical: 8,
fontSize: 14,
},
addFormBtns: { flexDirection: 'row', gap: 8 },
});