331 lines
9.8 KiB
TypeScript
331 lines
9.8 KiB
TypeScript
/**
|
||||
|
|
* 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,
|
|||
|
|
pickAndSaveLogo,
|
|||
|
|
saveFooterConfig,
|
|||
|
|
} from '../photo/footerConfig';
|
|||
|
|
import { 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',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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>
|
|||
|
|
|
|||
|
|
{/* ── 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 },
|
|||
|
|
|
|||
|
|
/* 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 },
|
|||
|
|
});
|