861 lines
24 KiB
TypeScript
861 lines
24 KiB
TypeScript
|
|
"use client"
|
||
|
|
|
||
|
|
import type React from "react"
|
||
|
|
import { useState, useEffect, useRef } from "react"
|
||
|
|
import {
|
||
|
|
StyleSheet,
|
||
|
|
TouchableOpacity,
|
||
|
|
View,
|
||
|
|
Text,
|
||
|
|
Vibration,
|
||
|
|
ActivityIndicator,
|
||
|
|
Dimensions,
|
||
|
|
Animated,
|
||
|
|
Linking,
|
||
|
|
} from "react-native"
|
||
|
|
import { Ionicons } from "@expo/vector-icons"
|
||
|
|
import * as Location from "expo-location"
|
||
|
|
import AsyncStorage from "@react-native-async-storage/async-storage"
|
||
|
|
import { PanGestureHandler, State } from "react-native-gesture-handler"
|
||
|
|
import { LinearGradient } from "expo-linear-gradient"
|
||
|
|
import { Modalize } from "react-native-modalize"
|
||
|
|
import { COLORS, SHADOWS } from "../src/constants/theme"
|
||
|
|
import { StatusBar } from "expo-status-bar"
|
||
|
|
|
||
|
|
const STORAGE_KEY = "panic_button_position"
|
||
|
|
const { width, height } = Dimensions.get("window")
|
||
|
|
const BUTTON_SIZE = 56
|
||
|
|
|
||
|
|
interface EnhancedPanicButtonProps {
|
||
|
|
onPanic?: (location?: { latitude: number; longitude: number } | null) => void
|
||
|
|
}
|
||
|
|
|
||
|
|
const EnhancedPanicButton: React.FC<EnhancedPanicButtonProps> = ({ onPanic }) => {
|
||
|
|
const [loading, setLoading] = useState(false)
|
||
|
|
const [ready, setReady] = useState(false)
|
||
|
|
const [position, setPosition] = useState({ x: width - BUTTON_SIZE - 24, y: height - BUTTON_SIZE - 120 })
|
||
|
|
const [currentModal, setCurrentModal] = useState<'main' | 'permission' | 'error' | null>(null)
|
||
|
|
|
||
|
|
// Refs para os modais
|
||
|
|
const mainModalRef = useRef<Modalize>(null)
|
||
|
|
const permissionModalRef = useRef<Modalize>(null)
|
||
|
|
const errorModalRef = useRef<Modalize>(null)
|
||
|
|
|
||
|
|
// Animated values
|
||
|
|
const translateX = useRef(new Animated.Value(position.x)).current
|
||
|
|
const translateY = useRef(new Animated.Value(position.y)).current
|
||
|
|
const lastOffset = useRef({ x: position.x, y: position.y })
|
||
|
|
const pulseAnim = useRef(new Animated.Value(1)).current
|
||
|
|
const shakeAnim = useRef(new Animated.Value(0)).current
|
||
|
|
const glowAnim = useRef(new Animated.Value(0)).current
|
||
|
|
|
||
|
|
// Animação de pulso e brilho do botão
|
||
|
|
useEffect(() => {
|
||
|
|
const pulse = Animated.loop(
|
||
|
|
Animated.sequence([
|
||
|
|
Animated.timing(pulseAnim, {
|
||
|
|
toValue: 1.12,
|
||
|
|
duration: 1200,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}),
|
||
|
|
Animated.timing(pulseAnim, {
|
||
|
|
toValue: 1,
|
||
|
|
duration: 1200,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
)
|
||
|
|
|
||
|
|
const glow = Animated.loop(
|
||
|
|
Animated.sequence([
|
||
|
|
Animated.timing(glowAnim, {
|
||
|
|
toValue: 1,
|
||
|
|
duration: 2000,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}),
|
||
|
|
Animated.timing(glowAnim, {
|
||
|
|
toValue: 0,
|
||
|
|
duration: 2000,
|
||
|
|
useNativeDriver: true,
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
)
|
||
|
|
|
||
|
|
pulse.start()
|
||
|
|
glow.start()
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
pulse.stop()
|
||
|
|
glow.stop()
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Carregar posição salva
|
||
|
|
useEffect(() => {
|
||
|
|
AsyncStorage.getItem(STORAGE_KEY).then((data) => {
|
||
|
|
if (data) {
|
||
|
|
const pos = JSON.parse(data)
|
||
|
|
setPosition(pos)
|
||
|
|
translateX.setValue(pos.x)
|
||
|
|
translateY.setValue(pos.y)
|
||
|
|
lastOffset.current = pos
|
||
|
|
}
|
||
|
|
setReady(true)
|
||
|
|
})
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Salvar posição ao mover
|
||
|
|
const savePosition = (x: number, y: number) => {
|
||
|
|
const newPos = {
|
||
|
|
x: Math.max(0, Math.min(x, width - BUTTON_SIZE)),
|
||
|
|
y: Math.max(0, Math.min(y, height - BUTTON_SIZE)),
|
||
|
|
}
|
||
|
|
setPosition(newPos)
|
||
|
|
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newPos))
|
||
|
|
lastOffset.current = newPos
|
||
|
|
}
|
||
|
|
|
||
|
|
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max))
|
||
|
|
|
||
|
|
const onHandlerStateChange = (event: any) => {
|
||
|
|
if (event.nativeEvent.oldState === State.ACTIVE) {
|
||
|
|
let newX = lastOffset.current.x + event.nativeEvent.translationX
|
||
|
|
let newY = lastOffset.current.y + event.nativeEvent.translationY
|
||
|
|
newX = clamp(newX, 0, width - BUTTON_SIZE)
|
||
|
|
newY = clamp(newY, 0, height - BUTTON_SIZE)
|
||
|
|
savePosition(newX, newY)
|
||
|
|
translateX.setValue(newX)
|
||
|
|
translateY.setValue(newY)
|
||
|
|
translateX.setOffset(0)
|
||
|
|
translateY.setOffset(0)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handlePress = () => {
|
||
|
|
Vibration.vibrate([50, 100, 50])
|
||
|
|
setCurrentModal('main')
|
||
|
|
mainModalRef.current?.open()
|
||
|
|
}
|
||
|
|
|
||
|
|
const shakeAnimation = () => {
|
||
|
|
Animated.sequence([
|
||
|
|
Animated.timing(shakeAnim, { toValue: 10, duration: 80, useNativeDriver: true }),
|
||
|
|
Animated.timing(shakeAnim, { toValue: -10, duration: 80, useNativeDriver: true }),
|
||
|
|
Animated.timing(shakeAnim, { toValue: 10, duration: 80, useNativeDriver: true }),
|
||
|
|
Animated.timing(shakeAnim, { toValue: -10, duration: 80, useNativeDriver: true }),
|
||
|
|
Animated.timing(shakeAnim, { toValue: 0, duration: 80, useNativeDriver: true }),
|
||
|
|
]).start()
|
||
|
|
}
|
||
|
|
|
||
|
|
const confirmPanic = async () => {
|
||
|
|
setLoading(true)
|
||
|
|
|
||
|
|
try {
|
||
|
|
const { status } = await Location.requestForegroundPermissionsAsync()
|
||
|
|
if (status !== "granted") {
|
||
|
|
setLoading(false)
|
||
|
|
mainModalRef.current?.close()
|
||
|
|
setTimeout(() => {
|
||
|
|
setCurrentModal('permission')
|
||
|
|
permissionModalRef.current?.open()
|
||
|
|
}, 300)
|
||
|
|
shakeAnimation()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const location = await Location.getCurrentPositionAsync({
|
||
|
|
accuracy: Location.Accuracy.High,
|
||
|
|
timeout: 10000,
|
||
|
|
})
|
||
|
|
|
||
|
|
setLoading(false)
|
||
|
|
mainModalRef.current?.close()
|
||
|
|
Vibration.vibrate([100, 200, 100, 200, 100])
|
||
|
|
|
||
|
|
if (onPanic) {
|
||
|
|
onPanic({
|
||
|
|
latitude: location.coords.latitude,
|
||
|
|
longitude: location.coords.longitude,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
setLoading(false)
|
||
|
|
mainModalRef.current?.close()
|
||
|
|
setTimeout(() => {
|
||
|
|
setCurrentModal('error')
|
||
|
|
errorModalRef.current?.open()
|
||
|
|
}, 300)
|
||
|
|
shakeAnimation()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handlePermissionDenied = () => {
|
||
|
|
permissionModalRef.current?.close()
|
||
|
|
if (onPanic) onPanic(null)
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleLocationError = () => {
|
||
|
|
errorModalRef.current?.close()
|
||
|
|
if (onPanic) onPanic(null)
|
||
|
|
}
|
||
|
|
|
||
|
|
const openSettings = () => {
|
||
|
|
Linking.openSettings()
|
||
|
|
permissionModalRef.current?.close()
|
||
|
|
}
|
||
|
|
|
||
|
|
const retryLocation = () => {
|
||
|
|
errorModalRef.current?.close()
|
||
|
|
setTimeout(() => {
|
||
|
|
setCurrentModal('main')
|
||
|
|
mainModalRef.current?.open()
|
||
|
|
}, 300)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!ready) return null
|
||
|
|
|
||
|
|
const renderMainModal = () => (
|
||
|
|
<View style={styles.modalContent}>
|
||
|
|
{/* Header com gradiente */}
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#DC2626", "#EF4444", "#F87171"]}
|
||
|
|
style={styles.modalHeader}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<View style={styles.headerContent}>
|
||
|
|
<View style={styles.iconContainer}>
|
||
|
|
<View style={styles.iconRing}>
|
||
|
|
<Ionicons name="alert" size={32} color="#fff" />
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
<Text style={styles.modalTitle}>Botão de Pânico</Text>
|
||
|
|
<Text style={styles.modalSubtitle}>Situação de emergência detectada</Text>
|
||
|
|
</View>
|
||
|
|
</LinearGradient>
|
||
|
|
|
||
|
|
{/* Corpo do modal */}
|
||
|
|
<View style={styles.modalBody}>
|
||
|
|
<View style={styles.warningSection}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#FEF3C7", "#FEF9E7"]}
|
||
|
|
style={styles.warningCard}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<View style={styles.warningIconContainer}>
|
||
|
|
<Ionicons name="warning" size={20} color="#D97706" />
|
||
|
|
</View>
|
||
|
|
<Text style={styles.warningText}>Use apenas em emergências reais</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View style={styles.featuresSection}>
|
||
|
|
<Text style={styles.featuresTitle}>O que acontecerá:</Text>
|
||
|
|
|
||
|
|
<View style={styles.featuresList}>
|
||
|
|
<View style={styles.featureItem}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={[COLORS.primary, "#3B82F6"]}
|
||
|
|
style={styles.featureIcon}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="location" size={16} color="white" />
|
||
|
|
</LinearGradient>
|
||
|
|
<View style={styles.featureContent}>
|
||
|
|
<Text style={styles.featureTitle}>Localização Enviada</Text>
|
||
|
|
<Text style={styles.featureDescription}>Sua posição GPS será compartilhada</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View style={styles.featureItem}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#10B981", "#059669"]}
|
||
|
|
style={styles.featureIcon}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="call" size={16} color="white" />
|
||
|
|
</LinearGradient>
|
||
|
|
<View style={styles.featureContent}>
|
||
|
|
<Text style={styles.featureTitle}>Equipe Notificada</Text>
|
||
|
|
<Text style={styles.featureDescription}>Emergência será comunicada imediatamente</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View style={styles.featureItem}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#8B5CF6", "#7C3AED"]}
|
||
|
|
style={styles.featureIcon}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="flash" size={16} color="white" />
|
||
|
|
</LinearGradient>
|
||
|
|
<View style={styles.featureContent}>
|
||
|
|
<Text style={styles.featureTitle}>Resposta Rápida</Text>
|
||
|
|
<Text style={styles.featureDescription}>Ação imediata será iniciada</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
{loading ? (
|
||
|
|
<View style={styles.loadingSection}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#FEF2F2", "#FFFFFF"]}
|
||
|
|
style={styles.loadingContainer}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 0, y: 1 }}
|
||
|
|
>
|
||
|
|
<ActivityIndicator size="large" color="#EF4444" />
|
||
|
|
<Text style={styles.loadingText}>Obtendo sua localização...</Text>
|
||
|
|
<Text style={styles.loadingSubtext}>Aguarde alguns segundos</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</View>
|
||
|
|
) : (
|
||
|
|
<View style={styles.actionsSection}>
|
||
|
|
<TouchableOpacity
|
||
|
|
style={styles.cancelButton}
|
||
|
|
onPress={() => mainModalRef.current?.close()}
|
||
|
|
>
|
||
|
|
<Text style={styles.cancelButtonText}>Cancelar</Text>
|
||
|
|
</TouchableOpacity>
|
||
|
|
|
||
|
|
<TouchableOpacity style={styles.confirmButton} onPress={confirmPanic}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#DC2626", "#EF4444"]}
|
||
|
|
style={styles.confirmButtonGradient}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="alert-circle" size={20} color="white" />
|
||
|
|
<Text style={styles.confirmButtonText}>Acionar Pânico</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</TouchableOpacity>
|
||
|
|
</View>
|
||
|
|
)}
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
)
|
||
|
|
|
||
|
|
const renderPermissionModal = () => (
|
||
|
|
<View style={styles.modalContent}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#D97706", "#F59E0B", "#FBBF24"]}
|
||
|
|
style={styles.modalHeader}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<View style={styles.headerContent}>
|
||
|
|
<View style={styles.iconContainer}>
|
||
|
|
<View style={styles.iconRing}>
|
||
|
|
<Ionicons name="location-off" size={32} color="#fff" />
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
<Text style={styles.modalTitle}>Permissão Necessária</Text>
|
||
|
|
<Text style={styles.modalSubtitle}>Acesso à localização requerido</Text>
|
||
|
|
</View>
|
||
|
|
</LinearGradient>
|
||
|
|
|
||
|
|
<View style={styles.modalBody}>
|
||
|
|
<View style={styles.permissionSection}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#FEF3C7", "#FFFBEB"]}
|
||
|
|
style={styles.permissionCard}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 0, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="shield-checkmark" size={24} color="#D97706" />
|
||
|
|
<Text style={styles.permissionTitle}>Por que precisamos?</Text>
|
||
|
|
<Text style={styles.permissionText}>
|
||
|
|
Para enviar sua localização exata à equipe de emergência e garantir que o socorro chegue rapidamente ao local correto.
|
||
|
|
</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View style={styles.actionsSection}>
|
||
|
|
<TouchableOpacity style={styles.cancelButton} onPress={handlePermissionDenied}>
|
||
|
|
<Text style={styles.cancelButtonText}>Continuar sem GPS</Text>
|
||
|
|
</TouchableOpacity>
|
||
|
|
|
||
|
|
<TouchableOpacity style={styles.settingsButton} onPress={openSettings}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#D97706", "#F59E0B"]}
|
||
|
|
style={styles.confirmButtonGradient}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="settings" size={20} color="white" />
|
||
|
|
<Text style={styles.confirmButtonText}>Abrir Configurações</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</TouchableOpacity>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
)
|
||
|
|
|
||
|
|
const renderErrorModal = () => (
|
||
|
|
<View style={styles.modalContent}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#DC2626", "#EF4444", "#F87171"]}
|
||
|
|
style={styles.modalHeader}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<View style={styles.headerContent}>
|
||
|
|
<View style={styles.iconContainer}>
|
||
|
|
<View style={styles.iconRing}>
|
||
|
|
<Ionicons name="alert-circle" size={32} color="#fff" />
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
<Text style={styles.modalTitle}>Erro de Localização</Text>
|
||
|
|
<Text style={styles.modalSubtitle}>Não foi possível obter GPS</Text>
|
||
|
|
</View>
|
||
|
|
</LinearGradient>
|
||
|
|
|
||
|
|
<View style={styles.modalBody}>
|
||
|
|
<View style={styles.errorSection}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#FEF2F2", "#FFFFFF"]}
|
||
|
|
style={styles.errorCard}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 0, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="information-circle" size={24} color="#EF4444" />
|
||
|
|
<Text style={styles.errorTitle}>O que fazer?</Text>
|
||
|
|
<Text style={styles.errorText}>
|
||
|
|
• Verifique se o GPS está ativado{'\n'}
|
||
|
|
• Certifique-se de estar em local aberto{'\n'}
|
||
|
|
• O pânico será acionado mesmo sem localização
|
||
|
|
</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View style={styles.actionsSection}>
|
||
|
|
<TouchableOpacity style={styles.cancelButton} onPress={handleLocationError}>
|
||
|
|
<Text style={styles.cancelButtonText}>Acionar sem GPS</Text>
|
||
|
|
</TouchableOpacity>
|
||
|
|
|
||
|
|
<TouchableOpacity style={styles.retryButton} onPress={retryLocation}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#059669", "#10B981"]}
|
||
|
|
style={styles.confirmButtonGradient}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<Ionicons name="refresh" size={20} color="white" />
|
||
|
|
<Text style={styles.confirmButtonText}>Tentar Novamente</Text>
|
||
|
|
</LinearGradient>
|
||
|
|
</TouchableOpacity>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
)
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<StatusBar style="dark" backgroundColor="transparent" translucent />
|
||
|
|
|
||
|
|
<PanGestureHandler
|
||
|
|
onGestureEvent={(event) => {
|
||
|
|
const { translationX, translationY } = event.nativeEvent
|
||
|
|
let newX = lastOffset.current.x + translationX
|
||
|
|
let newY = lastOffset.current.y + translationY
|
||
|
|
newX = clamp(newX, 0, width - BUTTON_SIZE)
|
||
|
|
newY = clamp(newY, 0, height - BUTTON_SIZE)
|
||
|
|
translateX.setValue(newX)
|
||
|
|
translateY.setValue(newY)
|
||
|
|
}}
|
||
|
|
onHandlerStateChange={onHandlerStateChange}
|
||
|
|
>
|
||
|
|
<Animated.View
|
||
|
|
style={[
|
||
|
|
styles.fab,
|
||
|
|
{
|
||
|
|
transform: [
|
||
|
|
{ translateX },
|
||
|
|
{ translateY },
|
||
|
|
{ scale: pulseAnim },
|
||
|
|
{ translateX: shakeAnim }
|
||
|
|
],
|
||
|
|
},
|
||
|
|
]}
|
||
|
|
>
|
||
|
|
{/* Efeito de brilho */}
|
||
|
|
<Animated.View
|
||
|
|
style={[
|
||
|
|
styles.glowEffect,
|
||
|
|
{
|
||
|
|
opacity: glowAnim.interpolate({
|
||
|
|
inputRange: [0, 1],
|
||
|
|
outputRange: [0.3, 0.8],
|
||
|
|
}),
|
||
|
|
transform: [
|
||
|
|
{
|
||
|
|
scale: glowAnim.interpolate({
|
||
|
|
inputRange: [0, 1],
|
||
|
|
outputRange: [1, 1.3],
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
]}
|
||
|
|
/>
|
||
|
|
|
||
|
|
<TouchableOpacity onPress={handlePress} activeOpacity={0.8} style={styles.fabButton}>
|
||
|
|
<LinearGradient
|
||
|
|
colors={["#DC2626", "#EF4444", "#F87171"]}
|
||
|
|
style={styles.fabGradient}
|
||
|
|
start={{ x: 0, y: 0 }}
|
||
|
|
end={{ x: 1, y: 1 }}
|
||
|
|
>
|
||
|
|
<View style={styles.fabIconContainer}>
|
||
|
|
<Ionicons name="alert" size={24} color="#fff" />
|
||
|
|
</View>
|
||
|
|
</LinearGradient>
|
||
|
|
</TouchableOpacity>
|
||
|
|
</Animated.View>
|
||
|
|
</PanGestureHandler>
|
||
|
|
|
||
|
|
{/* Modais sem Portal */}
|
||
|
|
<Modalize
|
||
|
|
ref={mainModalRef}
|
||
|
|
adjustToContentHeight
|
||
|
|
handlePosition="inside"
|
||
|
|
handleStyle={styles.modalHandle}
|
||
|
|
modalStyle={styles.modalStyle}
|
||
|
|
overlayStyle={styles.overlayStyle}
|
||
|
|
childrenStyle={styles.childrenStyle}
|
||
|
|
onClosed={() => setCurrentModal(null)}
|
||
|
|
>
|
||
|
|
{renderMainModal()}
|
||
|
|
</Modalize>
|
||
|
|
|
||
|
|
<Modalize
|
||
|
|
ref={permissionModalRef}
|
||
|
|
adjustToContentHeight
|
||
|
|
handlePosition="inside"
|
||
|
|
handleStyle={styles.modalHandle}
|
||
|
|
modalStyle={styles.modalStyle}
|
||
|
|
overlayStyle={styles.overlayStyle}
|
||
|
|
childrenStyle={styles.childrenStyle}
|
||
|
|
onClosed={() => setCurrentModal(null)}
|
||
|
|
>
|
||
|
|
{renderPermissionModal()}
|
||
|
|
</Modalize>
|
||
|
|
|
||
|
|
<Modalize
|
||
|
|
ref={errorModalRef}
|
||
|
|
adjustToContentHeight
|
||
|
|
handlePosition="inside"
|
||
|
|
handleStyle={styles.modalHandle}
|
||
|
|
modalStyle={styles.modalStyle}
|
||
|
|
overlayStyle={styles.overlayStyle}
|
||
|
|
childrenStyle={styles.childrenStyle}
|
||
|
|
onClosed={() => setCurrentModal(null)}
|
||
|
|
>
|
||
|
|
{renderErrorModal()}
|
||
|
|
</Modalize>
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
fab: {
|
||
|
|
position: "absolute",
|
||
|
|
width: BUTTON_SIZE,
|
||
|
|
height: BUTTON_SIZE,
|
||
|
|
borderRadius: BUTTON_SIZE / 2,
|
||
|
|
zIndex: 999,
|
||
|
|
},
|
||
|
|
glowEffect: {
|
||
|
|
position: "absolute",
|
||
|
|
width: BUTTON_SIZE + 16,
|
||
|
|
height: BUTTON_SIZE + 16,
|
||
|
|
borderRadius: (BUTTON_SIZE + 16) / 2,
|
||
|
|
backgroundColor: "#EF4444",
|
||
|
|
top: -8,
|
||
|
|
left: -8,
|
||
|
|
zIndex: -1,
|
||
|
|
},
|
||
|
|
fabButton: {
|
||
|
|
width: "100%",
|
||
|
|
height: "100%",
|
||
|
|
borderRadius: BUTTON_SIZE / 2,
|
||
|
|
overflow: "hidden",
|
||
|
|
...SHADOWS.large,
|
||
|
|
},
|
||
|
|
fabGradient: {
|
||
|
|
width: "100%",
|
||
|
|
height: "100%",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "center",
|
||
|
|
borderRadius: BUTTON_SIZE / 2,
|
||
|
|
},
|
||
|
|
fabIconContainer: {
|
||
|
|
width: 32,
|
||
|
|
height: 32,
|
||
|
|
borderRadius: 16,
|
||
|
|
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "center",
|
||
|
|
},
|
||
|
|
|
||
|
|
// Estilos do Modalize
|
||
|
|
modalStyle: {
|
||
|
|
backgroundColor: "transparent",
|
||
|
|
},
|
||
|
|
overlayStyle: {
|
||
|
|
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||
|
|
},
|
||
|
|
childrenStyle: {
|
||
|
|
backgroundColor: "transparent",
|
||
|
|
},
|
||
|
|
modalHandle: {
|
||
|
|
backgroundColor: "rgba(255, 255, 255, 0.3)",
|
||
|
|
width: 40,
|
||
|
|
height: 4,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Conteúdo dos modais
|
||
|
|
modalContent: {
|
||
|
|
backgroundColor: COLORS.card,
|
||
|
|
borderTopLeftRadius: 24,
|
||
|
|
borderTopRightRadius: 24,
|
||
|
|
overflow: "hidden",
|
||
|
|
marginBottom: 0,
|
||
|
|
},
|
||
|
|
modalHeader: {
|
||
|
|
paddingTop: 32,
|
||
|
|
paddingBottom: 24,
|
||
|
|
paddingHorizontal: 24,
|
||
|
|
},
|
||
|
|
headerContent: {
|
||
|
|
alignItems: "center",
|
||
|
|
},
|
||
|
|
iconContainer: {
|
||
|
|
marginBottom: 16,
|
||
|
|
},
|
||
|
|
iconRing: {
|
||
|
|
width: 72,
|
||
|
|
height: 72,
|
||
|
|
borderRadius: 36,
|
||
|
|
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "center",
|
||
|
|
borderWidth: 2,
|
||
|
|
borderColor: "rgba(255, 255, 255, 0.3)",
|
||
|
|
},
|
||
|
|
modalTitle: {
|
||
|
|
fontSize: 24,
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: "white",
|
||
|
|
marginBottom: 8,
|
||
|
|
textAlign: "center",
|
||
|
|
},
|
||
|
|
modalSubtitle: {
|
||
|
|
fontSize: 16,
|
||
|
|
color: "rgba(255, 255, 255, 0.9)",
|
||
|
|
textAlign: "center",
|
||
|
|
lineHeight: 22,
|
||
|
|
},
|
||
|
|
modalBody: {
|
||
|
|
padding: 24,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Seção de aviso
|
||
|
|
warningSection: {
|
||
|
|
marginBottom: 24,
|
||
|
|
},
|
||
|
|
warningCard: {
|
||
|
|
flexDirection: "row",
|
||
|
|
alignItems: "center",
|
||
|
|
padding: 16,
|
||
|
|
borderRadius: 16,
|
||
|
|
borderWidth: 1,
|
||
|
|
borderColor: "#F3E8FF",
|
||
|
|
},
|
||
|
|
warningIconContainer: {
|
||
|
|
width: 32,
|
||
|
|
height: 32,
|
||
|
|
borderRadius: 16,
|
||
|
|
backgroundColor: "rgba(217, 119, 6, 0.1)",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "center",
|
||
|
|
marginRight: 12,
|
||
|
|
},
|
||
|
|
warningText: {
|
||
|
|
fontSize: 14,
|
||
|
|
color: "#92400E",
|
||
|
|
fontWeight: "600",
|
||
|
|
flex: 1,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Seção de recursos
|
||
|
|
featuresSection: {
|
||
|
|
marginBottom: 24,
|
||
|
|
},
|
||
|
|
featuresTitle: {
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: COLORS.text,
|
||
|
|
marginBottom: 16,
|
||
|
|
},
|
||
|
|
featuresList: {
|
||
|
|
gap: 12,
|
||
|
|
},
|
||
|
|
featureItem: {
|
||
|
|
flexDirection: "row",
|
||
|
|
alignItems: "center",
|
||
|
|
},
|
||
|
|
featureIcon: {
|
||
|
|
width: 32,
|
||
|
|
height: 32,
|
||
|
|
borderRadius: 16,
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "center",
|
||
|
|
marginRight: 12,
|
||
|
|
},
|
||
|
|
featureContent: {
|
||
|
|
flex: 1,
|
||
|
|
},
|
||
|
|
featureTitle: {
|
||
|
|
fontSize: 14,
|
||
|
|
fontWeight: "600",
|
||
|
|
color: COLORS.text,
|
||
|
|
marginBottom: 2,
|
||
|
|
},
|
||
|
|
featureDescription: {
|
||
|
|
fontSize: 12,
|
||
|
|
color: COLORS.textLight,
|
||
|
|
lineHeight: 16,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Seção de loading
|
||
|
|
loadingSection: {
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
loadingContainer: {
|
||
|
|
alignItems: "center",
|
||
|
|
paddingVertical: 32,
|
||
|
|
paddingHorizontal: 24,
|
||
|
|
borderRadius: 16,
|
||
|
|
borderWidth: 1,
|
||
|
|
borderColor: "#FEE2E2",
|
||
|
|
},
|
||
|
|
loadingText: {
|
||
|
|
fontSize: 16,
|
||
|
|
color: COLORS.text,
|
||
|
|
marginTop: 16,
|
||
|
|
fontWeight: "600",
|
||
|
|
},
|
||
|
|
loadingSubtext: {
|
||
|
|
fontSize: 14,
|
||
|
|
color: COLORS.textLight,
|
||
|
|
marginTop: 4,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Seções específicas dos modais
|
||
|
|
permissionSection: {
|
||
|
|
marginBottom: 24,
|
||
|
|
},
|
||
|
|
permissionCard: {
|
||
|
|
alignItems: "center",
|
||
|
|
padding: 24,
|
||
|
|
borderRadius: 16,
|
||
|
|
borderWidth: 1,
|
||
|
|
borderColor: "#FEF3C7",
|
||
|
|
},
|
||
|
|
permissionTitle: {
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: "#92400E",
|
||
|
|
marginTop: 12,
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
permissionText: {
|
||
|
|
fontSize: 14,
|
||
|
|
color: "#92400E",
|
||
|
|
textAlign: "center",
|
||
|
|
lineHeight: 20,
|
||
|
|
},
|
||
|
|
|
||
|
|
errorSection: {
|
||
|
|
marginBottom: 24,
|
||
|
|
},
|
||
|
|
errorCard: {
|
||
|
|
alignItems: "center",
|
||
|
|
padding: 24,
|
||
|
|
borderRadius: 16,
|
||
|
|
borderWidth: 1,
|
||
|
|
borderColor: "#FEE2E2",
|
||
|
|
},
|
||
|
|
errorTitle: {
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: "bold",
|
||
|
|
color: "#DC2626",
|
||
|
|
marginTop: 12,
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
errorText: {
|
||
|
|
fontSize: 14,
|
||
|
|
color: "#DC2626",
|
||
|
|
textAlign: "center",
|
||
|
|
lineHeight: 20,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Ações
|
||
|
|
actionsSection: {
|
||
|
|
flexDirection: "row",
|
||
|
|
gap: 12,
|
||
|
|
marginBottom: 20,
|
||
|
|
},
|
||
|
|
cancelButton: {
|
||
|
|
flex: 1,
|
||
|
|
backgroundColor: COLORS.background,
|
||
|
|
borderRadius: 16,
|
||
|
|
paddingVertical: 16,
|
||
|
|
alignItems: "center",
|
||
|
|
borderWidth: 1.5,
|
||
|
|
borderColor: COLORS.border,
|
||
|
|
},
|
||
|
|
cancelButtonText: {
|
||
|
|
fontSize: 16,
|
||
|
|
color: COLORS.textLight,
|
||
|
|
fontWeight: "600",
|
||
|
|
},
|
||
|
|
confirmButton: {
|
||
|
|
flex: 1,
|
||
|
|
borderRadius: 16,
|
||
|
|
overflow: "hidden",
|
||
|
|
...SHADOWS.medium,
|
||
|
|
},
|
||
|
|
settingsButton: {
|
||
|
|
flex: 1,
|
||
|
|
borderRadius: 16,
|
||
|
|
overflow: "hidden",
|
||
|
|
...SHADOWS.medium,
|
||
|
|
},
|
||
|
|
retryButton: {
|
||
|
|
flex: 1,
|
||
|
|
borderRadius: 16,
|
||
|
|
overflow: "hidden",
|
||
|
|
...SHADOWS.medium,
|
||
|
|
},
|
||
|
|
confirmButtonGradient: {
|
||
|
|
flexDirection: "row",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "center",
|
||
|
|
paddingVertical: 16,
|
||
|
|
gap: 8,
|
||
|
|
},
|
||
|
|
confirmButtonText: {
|
||
|
|
fontSize: 16,
|
||
|
|
color: "white",
|
||
|
|
fontWeight: "bold",
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
export default EnhancedPanicButton
|