Implement system bars management and Edge-to-Edge layout
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import * as NavigationBar from 'expo-navigation-bar';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { SessionProvider } from './src/auth/session';
|
||||
import { getDb } from './src/db/database';
|
||||
@@ -9,13 +11,20 @@ export default function App() {
|
||||
// Abre y migra la BD local al arrancar.
|
||||
useEffect(() => {
|
||||
void getDb();
|
||||
|
||||
// Configuración global de la barra de navegación en Android (Edge-to-Edge)
|
||||
if (Platform.OS === 'android') {
|
||||
void NavigationBar.setPositionAsync('absolute');
|
||||
void NavigationBar.setBackgroundColorAsync('#ffffff00'); // Transparente
|
||||
void NavigationBar.setButtonStyleAsync('dark'); // Iconos oscuros por defecto
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<SessionProvider>
|
||||
<RootNavigator />
|
||||
<StatusBar style="light" />
|
||||
<StatusBar style="auto" />
|
||||
</SessionProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
|
||||
Generated
+15
@@ -18,6 +18,7 @@
|
||||
"expo-file-system": "~56.0.8",
|
||||
"expo-image-picker": "~56.0.18",
|
||||
"expo-location": "~56.0.18",
|
||||
"expo-navigation-bar": "^57.0.2",
|
||||
"expo-screen-orientation": "^57.0.1",
|
||||
"expo-secure-store": "~56.0.4",
|
||||
"expo-sqlite": "~56.0.5",
|
||||
@@ -5911,6 +5912,20 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-navigation-bar": {
|
||||
"version": "57.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-navigation-bar/-/expo-navigation-bar-57.0.2.tgz",
|
||||
"integrity": "sha512-Ufe49dsTZjarA7gMMwwwnNJWpXo+oTyl5Ud0g3lsGVrj2HXjyw9cb9098N8+CoWpjfIa5PghuRYYXrcCrGQIHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"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",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"expo-file-system": "~56.0.8",
|
||||
"expo-image-picker": "~56.0.18",
|
||||
"expo-location": "~56.0.18",
|
||||
"expo-navigation-bar": "^57.0.2",
|
||||
"expo-screen-orientation": "^57.0.1",
|
||||
"expo-secure-store": "~56.0.4",
|
||||
"expo-sqlite": "~56.0.5",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import * as Location from 'expo-location';
|
||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||
import * as NavigationBar from 'expo-navigation-bar';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
View,
|
||||
ActivityIndicator,
|
||||
useWindowDimensions,
|
||||
Platform,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
@@ -36,6 +39,13 @@ export function CameraScreen({ route, navigation }: Props) {
|
||||
useCallback(() => {
|
||||
let isMounted = true;
|
||||
|
||||
// Ocultar barras del sistema (Modo Inmersivo)
|
||||
if (Platform.OS === 'android') {
|
||||
void NavigationBar.setVisibilityAsync('hidden');
|
||||
void NavigationBar.setBehaviorAsync('inset-touch');
|
||||
}
|
||||
StatusBar.setHidden(true);
|
||||
|
||||
const load = async () => {
|
||||
const [cfg, projectId] = await Promise.all([
|
||||
loadFooterConfig(),
|
||||
@@ -95,6 +105,11 @@ export function CameraScreen({ route, navigation }: Props) {
|
||||
return () => {
|
||||
isMounted = false;
|
||||
ScreenOrientation.removeOrientationChangeListener(sub);
|
||||
// Restaurar barras del sistema al salir
|
||||
if (Platform.OS === 'android') {
|
||||
void NavigationBar.setVisibilityAsync('visible');
|
||||
}
|
||||
StatusBar.setHidden(false);
|
||||
};
|
||||
}, [])
|
||||
);
|
||||
|
||||
@@ -14,16 +14,9 @@ import { getTemplates } from '../db/repositories';
|
||||
import { createInspection } from '../sync/mutations';
|
||||
import { getDb } from '../db/database';
|
||||
import { newUuid, nextTempId } from '../sync/uuid';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { MediaStrip } from '../ui/MediaStrip';
|
||||
import {
|
||||
Card,
|
||||
ChipSelect,
|
||||
COLORS,
|
||||
Field,
|
||||
PrimaryButton,
|
||||
SectionTitle,
|
||||
} from '../ui/components';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'InspectionForm'>;
|
||||
|
||||
@@ -78,6 +71,7 @@ function groupFields(fields: NormField[]): { group: string; items: NormField[] }
|
||||
const RESULTS = ['pass', 'fail', 'na'] as const;
|
||||
|
||||
export function InspectionFormScreen({ route, navigation }: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { featureId, featureName, templateId: suggestedId } = route.params;
|
||||
|
||||
// Paso 1: elegir plantilla (las asignadas al proyecto llegan en el bundle).
|
||||
@@ -204,7 +198,7 @@ export function InspectionFormScreen({ route, navigation }: Props) {
|
||||
// ── Paso 1: selector de plantilla ──
|
||||
if (!chosen) {
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.body}>
|
||||
<ScrollView contentContainerStyle={[styles.body, { paddingBottom: insets.bottom + 20 }]}>
|
||||
<Text style={styles.subtitle}>{featureName}</Text>
|
||||
<Text style={styles.tplName}>Elige una plantilla</Text>
|
||||
|
||||
@@ -241,7 +235,7 @@ export function InspectionFormScreen({ route, navigation }: Props) {
|
||||
|
||||
// ── Paso 2: formulario ──
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.body}>
|
||||
<ScrollView contentContainerStyle={[styles.body, { paddingBottom: insets.bottom + 20 }]}>
|
||||
<Text style={styles.subtitle}>{featureName}</Text>
|
||||
<Text style={styles.tplName}>{template?.name ?? 'Inspección libre'}</Text>
|
||||
<TouchableOpacity onPress={backToPicker}>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getOutboxCounts, OutboxCounts } from '../db/outbox';
|
||||
import { isOnline } from '../net/connectivity';
|
||||
import { runSync } from '../sync/engine';
|
||||
import { useAutoSync } from '../sync/useAutoSync';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { COLORS, PrimaryButton } from '../ui/components';
|
||||
import { FeaturesSection } from './sections/FeaturesSection';
|
||||
@@ -19,6 +20,7 @@ const TABS = ['Features', 'Incidencias'] as const;
|
||||
type Tab = (typeof TABS)[number];
|
||||
|
||||
export function ProjectDetailScreen({ route, navigation }: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { projectId } = route.params;
|
||||
const [tab, setTab] = useState<Tab>('Features');
|
||||
const [counts, setCounts] = useState<OutboxCounts>(EMPTY);
|
||||
@@ -107,7 +109,7 @@ export function ProjectDetailScreen({ route, navigation }: Props) {
|
||||
{tab === 'Incidencias' && <IssuesSection projectId={projectId} />}
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.footer, { paddingBottom: Math.max(12, insets.bottom) }]}>
|
||||
<PrimaryButton
|
||||
title={syncing ? 'Sincronizando…' : 'Sincronizar'}
|
||||
onPress={() => void onSync()}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { runSync } from '../sync/engine';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { COLORS } from '../ui/components';
|
||||
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type Props = NativeStackScreenProps<RootStackParamList, 'Projects'>;
|
||||
|
||||
/** Tolerante con la forma de la respuesta: `{projects}` (actual), `{data}` o array. */
|
||||
@@ -28,6 +30,7 @@ function normalize(res: unknown): Project[] {
|
||||
}
|
||||
|
||||
export function ProjectsScreen({ navigation }: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { user, signOut } = useSession();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -105,7 +108,7 @@ export function ProjectsScreen({ navigation }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, { paddingBottom: insets.bottom }]}>
|
||||
{/* Menu flotante (se posiciona relativo al contenedor principal) */}
|
||||
{showMenu && (
|
||||
<View style={styles.floatingMenu}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { RootStackParamList } from '../../navigation/types';
|
||||
import {
|
||||
Badge,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
type Nav = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
export function FeatureDetailContent({ featureId }: { featureId: number }) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const navigation = useNavigation<Nav>();
|
||||
const { user } = useSession();
|
||||
const canProgress = hasPermission(user, 'update progress');
|
||||
@@ -81,7 +83,7 @@ export function FeatureDetailContent({ featureId }: { featureId: number }) {
|
||||
const isActive = feature.is_active == null || Number(feature.is_active) !== 0;
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.body}>
|
||||
<ScrollView contentContainerStyle={[styles.body, { paddingBottom: insets.bottom + 20 }]}>
|
||||
<View style={styles.badges}>
|
||||
{featureType && (
|
||||
<Badge label={featureType.name} color={featureType.color ?? COLORS.muted} />
|
||||
|
||||
Reference in New Issue
Block a user