Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BlurView } from 'expo-blur';
import * as Haptics from 'expo-haptics';
import { MaterialIcons } from '@expo/vector-icons';
import { ThemeProvider, useAppTheme } from './src/context/ThemeContext';
import { AirportProvider } from './src/context/AirportContext';
import HomeScreen from './src/screens/HomeScreen';
import TraveldocScreen from './src/screens/TraveldocScreen';
import FlightScreen from './src/screens/FlightScreen';
Expand Down Expand Up @@ -263,7 +264,9 @@ function AppInner() {
export default function App() {
return (
<ThemeProvider>
<AppInner />
<AirportProvider>
<AppInner />
</AirportProvider>
</ThemeProvider>
);
}
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-picker/picker": "2.11.1",
"@react-native-picker/picker": "2.11.4",
"@types/tesseract.js": "^0.0.2",
"expo": "~54.0.0",
"expo-blur": "~15.0.8",
Expand All @@ -25,18 +25,19 @@
"expo-linear-gradient": "~15.0.8",
"expo-location": "~19.0.8",
"expo-notifications": "~0.32.16",
"expo-secure-store": "~15.0.5",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-android-widget": "^0.20.1",
"react-native-calendars": "^1.1314.0",
"react-native-webview": "13.15.0",
"react-native-webview": "13.16.1",
"tesseract.js": "^7.0.0"
},
"devDependencies": {
"@react-native-community/cli": "^20.1.3",
"@types/react": "~19.1.10",
"pdfjs-dist": "^5.5.207",
"pdfjs-dist": "^5.6.205",
"typescript": "~5.9.2"
},
"private": true
Expand Down
21 changes: 10 additions & 11 deletions src/components/ShiftTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { useAppTheme } from '../context/ThemeContext';
import { getAirlineOps, getAirlineColor, ALLOWED_AIRLINES } from '../utils/airlineOps';
import { useAirport } from '../context/AirportContext';
import { getAirlineOps, getAirlineColor } from '../utils/airlineOps';
import { fetchAirportScheduleRaw } from '../utils/fr24api';

if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
Expand Down Expand Up @@ -53,6 +55,7 @@ function parseFlight(item: any): Flight | null {

export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd, inline }: Props) {
const { colors } = useAppTheme();
const { airportCode, isLoading: airportLoading } = useAirport();
const [flights, setFlights] = useState<Flight[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
Expand All @@ -65,17 +68,12 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd,
const SCREEN_H = Dimensions.get('window').height;

const fetchFlights = useCallback(async () => {
if (airportLoading) return;
setLoading(true);
setError(false);
try {
const res = await fetch(
'https://api.flightradar24.com/common/v1/airport.json?code=psa&plugin[]=schedule&page=1&limit=100',
{ headers: { 'User-Agent': 'Mozilla/5.0' } },
);
const json = await res.json();
const raw: any[] = json.result?.response?.airport?.pluginData?.schedule?.departures?.data || [];
const filtered = raw
.filter(i => ALLOWED_AIRLINES.some(k => (i.flight?.airline?.name || '').toLowerCase().includes(k)))
const { departures } = await fetchAirportScheduleRaw(airportCode);
const filtered = departures
.map(parseFlight)
.filter((f): f is Flight => f !== null && f.departureTs >= startSec && f.departureTs <= endSec)
.sort((a, b) => a.departureTs - b.departureTs);
Expand All @@ -85,18 +83,19 @@ export default function ShiftTimeline({ visible, onClose, shiftStart, shiftEnd,
} finally {
setLoading(false);
}
}, [startSec, endSec]);
}, [airportCode, airportLoading, startSec, endSec]);

// Inline: carica subito; Modal: carica quando visibile
useEffect(() => {
if (airportLoading) return;
if (inline || visible) {
fetchFlights();
setExpandedId(null);
setNowSec(Date.now() / 1000);
const interval = setInterval(() => setNowSec(Date.now() / 1000), 60000);
return () => clearInterval(interval);
}
}, [inline, visible, fetchFlights]);
}, [inline, visible, airportLoading, fetchFlights]);

const toggleExpand = (id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
Expand Down
52 changes: 52 additions & 0 deletions src/context/AirportContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
DEFAULT_AIRPORT_CODE,
getAirportInfo,
getStoredAirportCode,
setStoredAirportCode,
type AirportInfo,
} from '../utils/airportSettings';

type AirportContextValue = {
airportCode: string;
airport: AirportInfo;
setAirportCode: (code: string) => Promise<void>;
isLoading: boolean;
};

const defaultAirport = getAirportInfo(DEFAULT_AIRPORT_CODE);

const AirportContext = createContext<AirportContextValue>({
airportCode: defaultAirport.code,
airport: defaultAirport,
setAirportCode: async () => {},
isLoading: false,
});

export function AirportProvider({ children }: { children: React.ReactNode }) {
const [airportCode, setAirportCodeState] = useState(DEFAULT_AIRPORT_CODE);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
getStoredAirportCode()
.then(code => setAirportCodeState(code))
.finally(() => setIsLoading(false));
}, []);

const setAirportCode = useCallback(async (code: string) => {
const savedCode = await setStoredAirportCode(code);
setAirportCodeState(savedCode);
}, []);

const airport = useMemo(() => getAirportInfo(airportCode), [airportCode]);

return (
<AirportContext.Provider value={{ airportCode, airport, setAirportCode, isLoading }}>
{children}
</AirportContext.Provider>
);
}

export function useAirport() {
return useContext(AirportContext);
}
130 changes: 63 additions & 67 deletions src/hooks/useDynamicTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,81 +70,77 @@ const themes: Record<string, Theme> = {
}
};

// Variabile globale per mantenere in memoria il tema anche cambiando scheda
// Global cache — shared across all consumers so the fetch runs only once.
// A Promise is stored while a fetch is in-flight so late subscribers can await
// the same result instead of triggering a duplicate request (fixes race condition).
let globalCachedTheme: Theme | null = null;
let isFetchingTheme = false;
let pendingFetch: Promise<Theme> | null = null;

async function resolveTheme(): Promise<Theme> {
if (globalCachedTheme) return globalCachedTheme;
if (pendingFetch) return pendingFetch;

pendingFetch = (async () => {
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return themes.default;

const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
const { latitude: lat, longitude: lon } = location.coords;

const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true`,
);
const data = await response.json();

const weatherCode: number = data.current_weather?.weathercode ?? 0;
const isDay: number = data.current_weather?.is_day ?? 1;
const hour = new Date().getHours();

let timePeriod = 'night';
if (hour >= 6 && hour < 12) timePeriod = 'morning';
else if (hour >= 12 && hour < 18) timePeriod = 'afternoon';
else if (hour >= 18 && hour < 20) timePeriod = 'evening';

let weatherType = 'clear';
if (weatherCode === 3) weatherType = 'cloudy';
else if (weatherCode >= 45 && weatherCode <= 67) weatherType = 'rain';
else if (weatherCode >= 80) weatherType = 'rain';

let selected = themes.default;
if (weatherType === 'cloudy') selected = themes.cloudy;
else if (weatherType === 'rain') selected = themes.rain;
else if (!isDay || timePeriod === 'night') selected = themes.night_clear;
else if (timePeriod === 'morning') selected = themes.morning_clear;
else if (timePeriod === 'afternoon') selected = themes.afternoon_clear;
else if (timePeriod === 'evening') selected = themes.evening_clear;

globalCachedTheme = selected;
return selected;
} catch (err) {
console.warn('Errore caricamento tema dinamico:', err);
return themes.default;
} finally {
pendingFetch = null;
}
})();

return pendingFetch;
}

export function useDynamicTheme() {
const [theme, setTheme] = useState<Theme>(globalCachedTheme || themes.default);
const [loading, setLoading] = useState(!globalCachedTheme);

useEffect(() => {
let isMounted = true;

async function fetchTheme() {
if (globalCachedTheme) return; // Se già calcolato, evita ricaricamenti nulli
if (isFetchingTheme) return;
isFetchingTheme = true;

try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
if (isMounted) setTheme(themes.default);
return;
}

// Reduced accuracy massively speeds up GPS locks indoors!
const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced });
const lat = location.coords.latitude;
const lon = location.coords.longitude;

// Fetch meteo mondiale gratuito e senza chiavi (Open-Meteo)
const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current_weather=true`);
const data = await response.json();

const weatherCode = data.current_weather.weathercode;
const isDay = data.current_weather.is_day; // 1 se giorno, 0 se notte

// Orario locale per distinguere mattina/pomeriggio/sera
const hour = new Date().getHours();

let timePeriod = 'night';
if (hour >= 6 && hour < 12) timePeriod = 'morning';
else if (hour >= 12 && hour < 18) timePeriod = 'afternoon';
else if (hour >= 18 && hour < 20) timePeriod = 'evening';

// Categorizzazione WMO meteorologica
let weatherType = 'clear';
if (weatherCode === 3) weatherType = 'cloudy';
else if (weatherCode >= 45 && weatherCode <= 67) weatherType = 'rain';
else if (weatherCode >= 80) weatherType = 'rain';

let selectedTheme = themes.default;

// Logica prioritaria di fusione (Il meteo "brutto" sovrascrive il sole, altrimenti decide l'ora)
if (weatherType === 'cloudy') {
selectedTheme = themes.cloudy;
} else if (weatherType === 'rain') {
selectedTheme = themes.rain;
} else {
if (!isDay || timePeriod === 'night') selectedTheme = themes.night_clear;
else if (timePeriod === 'morning') selectedTheme = themes.morning_clear;
else if (timePeriod === 'afternoon') selectedTheme = themes.afternoon_clear;
else if (timePeriod === 'evening') selectedTheme = themes.evening_clear;
}

globalCachedTheme = selectedTheme;
if (isMounted) setTheme(selectedTheme);
} catch (err) {
console.warn("Errore caricamento tema dinamico:", err);
} finally {
if (isMounted) setLoading(false);
isFetchingTheme = false;
let cancelled = false;
resolveTheme().then(t => {
if (!cancelled) {
setTheme(t);
setLoading(false);
}
}

fetchTheme();
return () => { isMounted = false; };
});
return () => { cancelled = true; };
}, []);

return { theme, loadingTheme: loading };
Expand Down
Loading
Loading