From a9ba966176bb97800d2fb8ca5c82d858281e660c Mon Sep 17 00:00:00 2001 From: javier Date: Wed, 29 Jul 2026 00:35:45 +0200 Subject: [PATCH] Fix photo overlay positioning and rotation detection --- package-lock.json | 11 +++++ package.json | 1 + src/photo/FooterOverlay.tsx | 69 +++++++++++++---------------- src/photo/footerConfig.ts | 3 ++ src/screens/CameraScreen.tsx | 55 +++++++++++++++++++---- src/screens/PhotoSettingsScreen.tsx | 14 ++++++ 6 files changed, 107 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 60ce28e..0c5af0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "expo-file-system": "~56.0.8", "expo-image-picker": "~56.0.18", "expo-location": "~56.0.18", + "expo-screen-orientation": "^57.0.1", "expo-secure-store": "~56.0.4", "expo-sqlite": "~56.0.5", "expo-status-bar": "~56.0.4", @@ -5910,6 +5911,16 @@ "react-native": "*" } }, + "node_modules/expo-screen-orientation": { + "version": "57.0.1", + "resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-57.0.1.tgz", + "integrity": "sha512-pvMaOf2N5mcJNiYijIj0+Anza6vrHvcnAfFeO2yjquvPbejiE8oEh9BP9+Hx8VRnzFMoxiO6bG9W/oOFhtFIDA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-secure-store": { "version": "56.0.4", "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-56.0.4.tgz", diff --git a/package.json b/package.json index c7096d4..524a458 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "expo-file-system": "~56.0.8", "expo-image-picker": "~56.0.18", "expo-location": "~56.0.18", + "expo-screen-orientation": "^57.0.1", "expo-secure-store": "~56.0.4", "expo-sqlite": "~56.0.5", "expo-status-bar": "~56.0.4", diff --git a/src/photo/FooterOverlay.tsx b/src/photo/FooterOverlay.tsx index 69387f1..01e9268 100644 --- a/src/photo/FooterOverlay.tsx +++ b/src/photo/FooterOverlay.tsx @@ -1,11 +1,10 @@ /** - * 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. + * Pie de página superpuesto sobre la foto. + * Se posiciona en una de las cuatro esquinas según `config.overlayPosition`. + * El diseño es compacto (recuadro con fondo semitransparente). */ import React from 'react'; -import { Image, StyleSheet, Text, View } from 'react-native'; +import { Image, StyleSheet, Text, View, ViewStyle } from 'react-native'; import { FooterConfig, resolveFieldValue, StampMeta } from './footerConfig'; interface Props { @@ -20,31 +19,31 @@ export function FooterOverlay({ config, meta, imageWidth }: Props) { 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); + const fontSize = Math.max(10, Math.round(imageWidth * 0.018)); + const logoDim = Math.max(40, Math.round(imageWidth * 0.08)); + const pad = Math.max(8, Math.round(imageWidth * 0.015)); + const lineH = Math.round(fontSize * 1.3); + + // Determinar posición + const posStyle: ViewStyle = {}; + if (config.overlayPosition.includes('top')) posStyle.top = 20; + else posStyle.bottom = 20; + + if (config.overlayPosition.includes('left')) posStyle.left = 20; + else posStyle.right = 20; return ( - + {/* Logo */} {config.logoUri ? ( - <> - - - - - + + + ) : null} {/* Campos */} @@ -60,7 +59,7 @@ export function FooterOverlay({ config, meta, imageWidth }: Props) { { fontSize, lineHeight: lineH }, i === 0 ? styles.bold : null, ]} - numberOfLines={1} + numberOfLines={2} > {f.key === 'custom' ? `${f.label}: ${val}` : val} @@ -74,23 +73,19 @@ export function FooterOverlay({ config, meta, imageWidth }: Props) { const styles = StyleSheet.create({ container: { position: 'absolute', - bottom: 0, - left: 0, - right: 0, - backgroundColor: 'rgba(0,0,0,0.75)', - flexDirection: 'row', - alignItems: 'center', + backgroundColor: 'rgba(0,0,0,0.65)', + borderRadius: 8, + maxWidth: '50%', // Para no ocupar toda la pantalla + alignItems: 'flex-start', }, 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 }, + logoImg: { width: '80%', height: '80%' }, + textBlock: { width: '100%' }, line: { color: '#fff' }, bold: { fontWeight: '700' }, }); diff --git a/src/photo/footerConfig.ts b/src/photo/footerConfig.ts index c79abcd..532116f 100644 --- a/src/photo/footerConfig.ts +++ b/src/photo/footerConfig.ts @@ -4,6 +4,7 @@ import { getMeta, setMeta } from '../db/repositories'; export type FieldKey = 'project_name' | 'date' | 'coordinates' | 'custom'; export type PhotoResolution = 'low' | 'medium' | 'high'; +export type OverlayPosition = 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right'; export interface FooterField { id: string; @@ -20,6 +21,7 @@ export interface FooterConfig { fields: FooterField[]; resolution: PhotoResolution; quality: number; // 0.1 to 1.0 + overlayPosition: OverlayPosition; } export interface StampMeta { @@ -42,6 +44,7 @@ function defaultConfig(): FooterConfig { ], resolution: 'medium', quality: 0.85, + overlayPosition: 'bottom-left', }; } diff --git a/src/screens/CameraScreen.tsx b/src/screens/CameraScreen.tsx index 0be8534..61a2649 100644 --- a/src/screens/CameraScreen.tsx +++ b/src/screens/CameraScreen.tsx @@ -1,33 +1,55 @@ import { CameraView, useCameraPermissions } from 'expo-camera'; import * as Location from 'expo-location'; +import * as ScreenOrientation from 'expo-screen-orientation'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { - Dimensions, StyleSheet, Text, TouchableOpacity, View, ActivityIndicator, + useWindowDimensions, } from 'react-native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { RootStackParamList } from '../navigation/types'; -import { FooterConfig, loadFooterConfig, resolveFieldValue, StampMeta } from '../photo/footerConfig'; +import { FooterConfig, loadFooterConfig, StampMeta } from '../photo/footerConfig'; import { getActiveProjectId, getProject } from '../db/repositories'; import { COLORS } from '../ui/components'; import { FooterOverlay } from '../photo/FooterOverlay'; type Props = NativeStackScreenProps; -const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window'); - export function CameraScreen({ route, navigation }: Props) { const { onCapture } = route.params; const [permission, requestPermission] = useCameraPermissions(); const cameraRef = useRef(null); + const { width: winW, height: winH } = useWindowDimensions(); const [config, setConfig] = useState(null); const [meta, setMeta] = useState(null); const [taking, setTaking] = useState(false); + const [rotation, setRotation] = useState(0); + + // Detectar orientación física para rotar el sello + useEffect(() => { + const updateOrientation = async () => { + const info = await ScreenOrientation.getOrientationAsync(); + handleOrientation(info); + }; + + const handleOrientation = (o: ScreenOrientation.Orientation) => { + if (o === ScreenOrientation.Orientation.LANDSCAPE_LEFT) setRotation(90); + else if (o === ScreenOrientation.Orientation.LANDSCAPE_RIGHT) setRotation(-90); + else if (o === ScreenOrientation.Orientation.PORTRAIT_UPSIDE_DOWN) setRotation(180); + else setRotation(0); + }; + + void updateOrientation(); + const sub = ScreenOrientation.addOrientationChangeListener((evt) => { + handleOrientation(evt.orientationInfo.orientation); + }); + return () => ScreenOrientation.removeOrientationChangeListener(sub); + }, []); const loadData = useCallback(async () => { const [cfg, projectId] = await Promise.all([ @@ -118,10 +140,17 @@ export function CameraScreen({ route, navigation }: Props) { - {/* Footer Preview */} + {/* Footer Preview con rotación */} {config && meta && ( - - + + winH ? winH : winW} + /> )} @@ -153,8 +182,16 @@ const styles = StyleSheet.create({ closeBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' }, closeTxt: { color: '#fff', fontSize: 24 }, footerWrapper: { - height: 100, // Espacio para el footer - justifyContent: 'flex-end', + position: 'absolute', + // Ocupamos toda la pantalla para que el transform rote sobre el centro + // pero el FooterOverlay se posicionará internamente en su esquina. + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + pointerEvents: 'none', }, controls: { paddingBottom: 40, diff --git a/src/screens/PhotoSettingsScreen.tsx b/src/screens/PhotoSettingsScreen.tsx index d04cd12..6d18def 100644 --- a/src/screens/PhotoSettingsScreen.tsx +++ b/src/screens/PhotoSettingsScreen.tsx @@ -22,6 +22,7 @@ import { FooterConfig, FooterField, loadFooterConfig, + OverlayPosition, pickAndSaveLogo, PhotoResolution, saveFooterConfig, @@ -36,6 +37,7 @@ const BUILT_IN_LABELS: Record = { 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(null); @@ -182,6 +184,18 @@ export function PhotoSettingsScreen() { Valores bajos reducen el tamaño del archivo pero pueden verse peor. + + + + void save({ ...config, overlayPosition: v })} + /> + + Esquina de la imagen donde aparecerán los datos. + {/* ── Campos ── */}