diff --git a/package-lock.json b/package-lock.json index 8a36ffb..5164296 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,8 @@ "react-native": "0.85.3", "react-native-maps": "1.27.2", "react-native-safe-area-context": "~5.7.0", - "react-native-screens": "4.25.2" + "react-native-screens": "4.25.2", + "react-native-view-shot": "^4.0.0" }, "devDependencies": { "@types/react": "~19.2.2", @@ -3000,7 +3001,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3446,6 +3447,15 @@ } } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4083,11 +4093,20 @@ "node": ">=8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/dateformat": { @@ -6922,6 +6941,19 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-call": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", @@ -9476,6 +9508,19 @@ "react-native": ">=0.82.0" } }, + "node_modules/react-native-view-shot": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz", + "integrity": "sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==", + "license": "MIT", + "dependencies": { + "html2canvas": "^1.4.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", @@ -10509,6 +10554,15 @@ "b4a": "^1.6.4" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -10744,7 +10798,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10909,6 +10963,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", diff --git a/package.json b/package.json index e3a97bb..770822f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "react": "19.2.3", "react-native": "0.85.3", "react-native-maps": "1.27.2", + "react-native-view-shot": "^4.0.0", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2" }, diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 23dcfb4..87b4a4f 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -274,6 +274,11 @@ export async function getProjects(): Promise { return db.getAllAsync('SELECT * FROM projects ORDER BY name'); } +export async function getProject(id: number): Promise { + const db = await getDb(); + return db.getFirstAsync('SELECT * FROM projects WHERE id = ?', id); +} + /** Persiste la lista que devuelve GET /projects (upsert sin tocar el resto). */ export async function saveProjectList(projects: Project[]): Promise { const db = await getDb(); diff --git a/src/navigation/index.tsx b/src/navigation/index.tsx index 0ffddd9..480765c 100644 --- a/src/navigation/index.tsx +++ b/src/navigation/index.tsx @@ -11,6 +11,7 @@ import { FeatureDetailScreen } from '../screens/FeatureDetailScreen'; import { InspectionFormScreen } from '../screens/InspectionFormScreen'; import { IssueCreateScreen } from '../screens/IssueCreateScreen'; import { OutboxScreen } from '../screens/OutboxScreen'; +import { PhotoSettingsScreen } from '../screens/PhotoSettingsScreen'; import { RootStackParamList } from './types'; const Stack = createNativeStackNavigator(); @@ -68,6 +69,11 @@ export function RootNavigator() { component={OutboxScreen} options={{ title: 'Cola de sincronización' }} /> + ); diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 6af630f..5ece508 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -6,4 +6,5 @@ export type RootStackParamList = { InspectionForm: { featureId: number; featureName: string; templateId?: number }; IssueCreate: { projectId: number; featureId?: number }; Outbox: undefined; + PhotoSettings: undefined; }; diff --git a/src/photo/FooterOverlay.tsx b/src/photo/FooterOverlay.tsx new file mode 100644 index 0000000..69387f1 --- /dev/null +++ b/src/photo/FooterOverlay.tsx @@ -0,0 +1,96 @@ +/** + * Pie de página superpuesto sobre la foto antes de capturarla con ViewShot. + * Se posiciona en la parte inferior de su contenedor con position:absolute. + * Todos los tamaños son proporcionales al ancho de la imagen para que el pie + * sea legible independientemente de la resolución del sensor. + */ +import React from 'react'; +import { Image, StyleSheet, Text, View } from 'react-native'; +import { FooterConfig, resolveFieldValue, StampMeta } from './footerConfig'; + +interface Props { + config: FooterConfig; + meta: StampMeta; + imageWidth: number; +} + +export function FooterOverlay({ config, meta, imageWidth }: Props) { + if (!config.enabled) return null; + + const enabled = config.fields.filter((f) => f.enabled); + if (enabled.length === 0 && !config.logoUri) return null; + + const fontSize = Math.max(12, Math.round(imageWidth * 0.016)); + const logoDim = Math.max(50, Math.round(imageWidth * 0.09)); + const pad = Math.max(8, Math.round(imageWidth * 0.012)); + const lineH = Math.round(fontSize * 1.4); + + return ( + + {/* Logo */} + {config.logoUri ? ( + <> + + + + + + ) : null} + + {/* Campos */} + + {enabled.map((f, i) => { + const val = resolveFieldValue(f, meta); + if (!val) return null; + return ( + + {f.key === 'custom' ? `${f.label}: ${val}` : val} + + ); + })} + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(0,0,0,0.75)', + flexDirection: 'row', + alignItems: 'center', + }, + logoWrap: { + backgroundColor: '#fff', + overflow: 'hidden', + justifyContent: 'center', + alignItems: 'center', + flexShrink: 0, + }, + logoImg: { width: '100%', height: '100%' }, + divider: { width: 1, backgroundColor: 'rgba(255,255,255,0.35)', flexShrink: 0 }, + textBlock: { flex: 1 }, + line: { color: '#fff' }, + bold: { fontWeight: '700' }, +}); diff --git a/src/photo/footerConfig.ts b/src/photo/footerConfig.ts new file mode 100644 index 0000000..f39dedd --- /dev/null +++ b/src/photo/footerConfig.ts @@ -0,0 +1,86 @@ +import * as FileSystem from 'expo-file-system/legacy'; +import * as ImagePicker from 'expo-image-picker'; +import { getMeta, setMeta } from '../db/repositories'; + +export type FieldKey = 'project_name' | 'date' | 'coordinates' | 'custom'; + +export interface FooterField { + id: string; + key: FieldKey; + label: string; + enabled: boolean; + /** Solo para key === 'custom': texto estático que aparece en el pie. */ + customValue?: string; +} + +export interface FooterConfig { + enabled: boolean; + logoUri: string | null; + fields: FooterField[]; +} + +export interface StampMeta { + projectName: string | null; + date: string; + coordinates: string | null; +} + +const META_KEY = 'photo_footer_config'; +const LOGO_DEST = (FileSystem.documentDirectory ?? '') + 'footer_logo.jpg'; + +function defaultConfig(): FooterConfig { + return { + enabled: true, + logoUri: null, + fields: [ + { id: 'project_name', key: 'project_name', label: 'Proyecto', enabled: true }, + { id: 'date', key: 'date', label: 'Fecha', enabled: true }, + { id: 'coordinates', key: 'coordinates', label: 'Coordenadas GPS', enabled: true }, + ], + }; +} + +export async function loadFooterConfig(): Promise { + const raw = await getMeta(META_KEY); + if (!raw) return defaultConfig(); + try { + return JSON.parse(raw) as FooterConfig; + } catch { + return defaultConfig(); + } +} + +export async function saveFooterConfig(config: FooterConfig): Promise { + await setMeta(META_KEY, JSON.stringify(config)); +} + +/** Abre la galería, copia la imagen al almacenamiento de la app y devuelve la ruta. */ +export async function pickAndSaveLogo(): Promise { + const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!perm.granted) return null; + const result = await ImagePicker.launchImageLibraryAsync({ + quality: 0.9, + mediaTypes: 'images', + allowsEditing: true, + aspect: [1, 1], + }); + if (result.canceled || !result.assets?.length) return null; + await FileSystem.copyAsync({ from: result.assets[0].uri, to: LOGO_DEST }); + // Añadir timestamp para que Image no use caché de la versión anterior. + return `${LOGO_DEST}?t=${Date.now()}`; +} + +export async function clearLogo(): Promise { + try { await FileSystem.deleteAsync(LOGO_DEST, { idempotent: true }); } catch { /* */ } +} + +/** Resuelve el texto que aparece en la línea de un campo. */ +export function resolveFieldValue(field: FooterField, meta: StampMeta): string { + switch (field.key) { + case 'project_name': return meta.projectName ?? ''; + case 'date': return meta.date; + case 'coordinates': return meta.coordinates ?? 'Sin GPS'; + case 'custom': return field.customValue ?? field.label; + default: return ''; + } +} diff --git a/src/screens/PhotoSettingsScreen.tsx b/src/screens/PhotoSettingsScreen.tsx new file mode 100644 index 0000000..d895544 --- /dev/null +++ b/src/screens/PhotoSettingsScreen.tsx @@ -0,0 +1,330 @@ +/** + * 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 = { + project_name: 'Nombre del proyecto', + date: 'Fecha y hora', + coordinates: 'Coordenadas GPS', +}; + +export function PhotoSettingsScreen() { + const [config, setConfig] = useState(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 ( + + + {/* Master toggle */} + + Añadir pie de página a las fotos + + + + {/* ── Logo ── */} + Logo + + {config.logoUri ? ( + + ) : ( + + Sin logo + + )} + + void onChangeLogo()} + /> + {config.logoUri ? ( + + ) : null} + + + + Recorte cuadrado. Se recomienda logo con fondo blanco. + + + {/* ── Campos ── */} + Campos + + {config.fields.map((f) => { + const isBuiltIn = f.key !== 'custom'; + return ( + + toggleField(f.id, v)} + trackColor={{ true: COLORS.primary }} + /> + + + {isBuiltIn ? BUILT_IN_LABELS[f.key] ?? f.label : ''} + + editFieldLabel(f.id, t)} + placeholder="Etiqueta…" + returnKeyType="done" + /> + {f.key === 'custom' && ( + { + if (!config) return; + void save({ + ...config, + fields: config.fields.map((x) => + x.id === f.id ? { ...x, customValue: t } : x, + ), + }); + }} + placeholder="Valor…" + returnKeyType="done" + /> + )} + + {!isBuiltIn && ( + deleteCustomField(f.id)} + style={styles.deleteBtn} + > + + + )} + + ); + })} + + {/* Añadir campo personalizado */} + {showAddForm ? ( + + + + + + + + + setShowAddForm(false)} /> + + + + ) : ( + setShowAddForm(true)}> + + Añadir campo personalizado + + )} + + + + ); +} + +// ─── 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 }, +}); diff --git a/src/ui/MediaStrip.tsx b/src/ui/MediaStrip.tsx index 2af41f5..e86d2ec 100644 --- a/src/ui/MediaStrip.tsx +++ b/src/ui/MediaStrip.tsx @@ -1,17 +1,77 @@ /** - * Tira de fotos de un registro (feature/issue/issue_task/issue_comment). - * Muestra las ya sincronizadas (tabla `media`, con url) y las locales en cola - * (`media_outbox`), y permite añadir nuevas desde cámara o galería (offline). + * Tira de fotos de un registro. Muestra las ya sincronizadas (tabla `media`) + * y las locales en cola (`media_outbox`). Permite añadir nuevas desde cámara + * o galería; antes de encolarlas les estampa un pie de página georreferenciado + * (logo + proyecto + fecha + coordenadas) capturado con react-native-view-shot. + * + * El pie de página es configurable desde PhotoSettingsScreen (icono ⚙). */ import * as ImagePicker from 'expo-image-picker'; -import React, { useCallback, useEffect, useState } from 'react'; -import { Alert, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import * as Location from 'expo-location'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { + Alert, + Image, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { captureRef } from 'react-native-view-shot'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Media, MediaParentEntity } from '../api/types'; import { absoluteUrl } from '../config'; import { enqueueMedia, getMediaOutboxFor, MediaOutboxRow } from '../db/outbox'; -import { getMediaFor } from '../db/repositories'; +import { getActiveProjectId, getMediaFor, getProject } from '../db/repositories'; +import { RootStackParamList } from '../navigation/types'; +import { + FooterConfig, + loadFooterConfig, + resolveFieldValue, + StampMeta, +} from '../photo/footerConfig'; +import { FooterOverlay } from '../photo/FooterOverlay'; import { COLORS } from './components'; +// ─── tipos internos ──────────────────────────────────────────────────────── + +interface ComposingTask { + uri: string; + renderW: number; + renderH: number; + meta: StampMeta; + config: FooterConfig; +} + +type Nav = NativeStackNavigationProp; + +const MAX_STAMP_W = 2048; + +// ─── helpers ─────────────────────────────────────────────────────────────── + +async function getCoords(): Promise<{ latitude: number; longitude: number } | null> { + try { + const { granted } = await Location.requestForegroundPermissionsAsync(); + if (!granted) return null; + const loc = await Location.getCurrentPositionAsync({ + accuracy: Location.Accuracy.Balanced, + }); + return loc.coords; + } catch { + return null; + } +} + +function formatCoords(lat: number, lng: number): string { + const la = lat >= 0 ? 'N' : 'S'; + const lo = lng >= 0 ? 'E' : 'O'; + return `${Math.abs(lat).toFixed(5)}°${la}, ${Math.abs(lng).toFixed(5)}°${lo}`; +} + +// ─── componente ──────────────────────────────────────────────────────────── + export function MediaStrip({ parentEntity, parentId, @@ -21,8 +81,16 @@ export function MediaStrip({ parentId: number; canUpload: boolean; }) { - const [synced, setSynced] = useState([]); + const navigation = useNavigation