diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 26c05cb..f3982df 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -129,6 +129,8 @@ const it = { flightNotifMsg1: 'Programmate {count} notifiche per il turno di oggi.', flightNotifMsg0: 'Nessuna notifica futura trovata con i filtri attivi.', flightNotifAccessEnable: 'Attiva notifiche voli', flightNotifAccessDisable: 'Disattiva notifiche voli', + flightAccessibilityPin: 'Pinna volo', flightAccessibilityUnpin: 'Rimuovi pin', + flightFrom: 'da', flightTo: 'per', flightNotifSettingsTitle: 'Impostazioni notifiche', flightNotifSettingsSub: 'Decidi cosa ricevere, sticky e minutaggi.', flightNotifOnlyTracked: 'Solo compagnie monitorate', @@ -328,6 +330,8 @@ const en: typeof it = { flightNotifMsg1: '{count} notifications scheduled for today’s shift.', flightNotifMsg0: 'No upcoming notifications found with current filters.', flightNotifAccessEnable: 'Enable flight notifications', flightNotifAccessDisable: 'Disable flight notifications', + flightAccessibilityPin: 'Pin flight', flightAccessibilityUnpin: 'Unpin flight', + flightFrom: 'from', flightTo: 'to', flightNotifSettingsTitle: 'Notification settings', flightNotifSettingsSub: 'Choose what to receive, sticky behavior and timing.', flightNotifOnlyTracked: 'Only tracked airlines', diff --git a/src/screens/FlightScreen.tsx b/src/screens/FlightScreen.tsx index 09dbe5d..bc103ea 100644 --- a/src/screens/FlightScreen.tsx +++ b/src/screens/FlightScreen.tsx @@ -3,8 +3,10 @@ import { View, Text, StyleSheet, ActivityIndicator, Modal, ScrollView, FlatList, TouchableOpacity, RefreshControl, Image, Animated, PanResponder, NativeModules, Platform, Switch, + type ViewProps, type AccessibilityActionEvent, } from 'react-native'; import { Easing } from 'react-native'; +import * as Haptics from 'expo-haptics'; import * as Calendar from 'expo-calendar'; import * as Notifications from 'expo-notifications'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -302,15 +304,17 @@ const SWIPE_MAX_TRANSLATE = 96; const SWIPE_DRAG_RESISTANCE = 0.82; function SwipeableFlightCardComponent({ - children, isPinned, onToggle, + children, isPinned, onToggle, ...rest }: { children: React.ReactNode; isPinned: boolean; onToggle: () => void; -}) { +} & ViewProps) { const translateX = useRef(new Animated.Value(0)).current; const onToggleRef = useRef(onToggle); onToggleRef.current = onToggle; + + const hasTriggeredHaptic = useRef(false); const dragScale = useMemo(() => translateX.interpolate({ inputRange: [-SWIPE_MAX_TRANSLATE, 0], outputRange: [0.985, 1], @@ -336,9 +340,17 @@ function SwipeableFlightCardComponent({ ? Math.max(g.dx * SWIPE_DRAG_RESISTANCE, -SWIPE_MAX_TRANSLATE) : g.dx * 0.08; translateX.setValue(nextTranslate); + + if (nextTranslate <= -SWIPE_THRESHOLD && !hasTriggeredHaptic.current) { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + hasTriggeredHaptic.current = true; + } else if (nextTranslate > -SWIPE_THRESHOLD && hasTriggeredHaptic.current) { + hasTriggeredHaptic.current = false; + } }, onPanResponderRelease: (_, g) => { if (g.dx < -SWIPE_THRESHOLD || g.vx < -SWIPE_TRIGGER_VELOCITY) { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Animated.timing(translateX, { toValue: -SWIPE_MAX_TRANSLATE, duration: 170, @@ -351,6 +363,7 @@ function SwipeableFlightCardComponent({ } else { animateBack(g.vx); } + hasTriggeredHaptic.current = false; }, onPanResponderTerminate: () => { animateBack(); @@ -359,7 +372,11 @@ function SwipeableFlightCardComponent({ return ( - + {children} @@ -515,12 +532,29 @@ function FlightRowComponent({ item, activeTab, userShift, pinnedFlightId, onPin, return () => clearInterval(interval); }, []); + const aLabel = useMemo(() => { + const direction = activeTab === 'arrivals' ? t('flightFrom') : t('flightTo'); + const pinnedState = isPinned ? `${t('flightPinnedLabel')}. ` : ''; + return `${pinnedState}${flightNumber}, ${airline}, ${direction} ${originDest}, ${time}, ${statusText}`; + }, [activeTab, airline, flightNumber, isPinned, originDest, statusText, t, time]); + + const handleAccessibilityAction = useCallback((event: AccessibilityActionEvent) => { + if (event.nativeEvent.actionName === 'togglePin') { + if (isPinned) onUnpin(); + else onPin(item); + } + }, [isPinned, onPin, onUnpin, item]); + return ( isPinned ? onUnpin() : onPin(item)} + accessible + accessibilityLabel={aLabel} + accessibilityActions={[{ name: 'togglePin', label: isPinned ? t('flightAccessibilityUnpin') : t('flightAccessibilityPin') }]} + onAccessibilityAction={handleAccessibilityAction} > - + {isPinned && {t('flightPinned')}} {/* Header */}