diff --git a/mobile/traque-app/app/_layout.jsx b/mobile/traque-app/app/_layout.jsx index cbe85b9..2ec2090 100644 --- a/mobile/traque-app/app/_layout.jsx +++ b/mobile/traque-app/app/_layout.jsx @@ -1,19 +1,16 @@ // Expo import { Slot } from 'expo-router'; // Contexts -import { SocketProvider } from "../context/socketContext"; import { TeamConnexionProvider } from "../context/teamConnexionContext"; import { TeamProvider } from "../context/teamContext"; const Layout = () => { return ( - - - - - - - + + + + + ); }; diff --git a/mobile/traque-app/app/index.jsx b/mobile/traque-app/app/index.jsx index 4580d4a..a9a1c3a 100644 --- a/mobile/traque-app/app/index.jsx +++ b/mobile/traque-app/app/index.jsx @@ -9,34 +9,33 @@ import { CustomImage } from '../components/image'; import { CustomTextInput } from '../components/input'; // Contexts import { useTeamConnexion } from "../context/teamConnexionContext"; -import { useTeamContext } from "../context/teamContext"; // Hooks import { usePickImage } from '../hook/usePickImage'; -import { useImageApi } from '../hook/useImageApi'; -// Util -import { Colors } from '../util/colors'; +// Services +import { uploadTeamImage } from '../services/imageService'; +import { getLocationAuthorization, stopLocationTracking } from '../services/backgroundLocationTask'; +// Constants +import { COLORS } from '../constants'; const Index = () => { const router = useRouter(); - const {login, loggedIn} = useTeamConnexion(); - const {getLocationAuthorization, stopLocationTracking} = useTeamContext(); + const { login, loggedIn } = useTeamConnexion(); const {image, pickImage} = usePickImage(); const [teamId, setTeamId] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - const { uploadTeamImage } = useImageApi(); - // Disbaling location tracking + // Disbaling location tracking and asking permissions useEffect(() => { stopLocationTracking(); - }, [stopLocationTracking]); + getLocationAuthorization(); + }, []); // Routeur useEffect(() => { if (loggedIn) { - uploadTeamImage(image?.uri); router.replace("/interface"); } - }, [router, loggedIn, uploadTeamImage, image]); + }, [router, loggedIn, image]); const handleSubmit = async () => { if (isSubmitting || !getLocationAuthorization()) return; @@ -53,6 +52,7 @@ const Index = () => { const response = await login(teamId); if (response.isLoggedIn) { + uploadTeamImage(teamId, image?.uri); setTeamId(""); } else { setTimeout(() => Alert.alert("Échec", "L'ID d'équipe est inconnu."), 100); @@ -94,7 +94,7 @@ const styles = StyleSheet.create({ flexGrow: 1, alignItems: 'center', paddingVertical: 20, - backgroundColor: Colors.background + backgroundColor: COLORS.background }, transitionContainer: { flexGrow: 1, diff --git a/mobile/traque-app/app/interface.jsx b/mobile/traque-app/app/interface.jsx index 9a831a1..6cb3622 100644 --- a/mobile/traque-app/app/interface.jsx +++ b/mobile/traque-app/app/interface.jsx @@ -6,21 +6,23 @@ import { useRouter } from 'expo-router'; // Components import { CustomMap } from '../components/map'; import { Drawer } from '../components/drawer'; +import { TimerMMSS } from '../components/timer'; // Contexts import { useTeamConnexion } from '../context/teamConnexionContext'; import { useTeamContext } from '../context/teamContext'; // Hooks import { useGame } from '../hook/useGame'; import { useTimeDifference } from '../hook/useTimeDifference'; +// Services +import { startLocationTracking } from '../services/backgroundLocationTask'; // Util -import { GameState } from '../util/gameState'; -import { TimerMMSS } from '../components/timer'; import { secondsToMMSS } from '../util/functions'; -import { Colors } from '../util/colors'; +// Constants +import { GAME_STATE, COLORS } from '../constants'; const Interface = () => { const router = useRouter(); - const {teamInfos, messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext(); + const {teamInfos, messages, nextZoneDate, isShrinking, gameState} = useTeamContext(); const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos; const {loggedIn, logout} = useTeamConnexion(); const {sendCurrentPosition} = useGame(); @@ -31,16 +33,16 @@ const Interface = () => { const statusMessage = useMemo(() => { switch (gameState) { - case GameState.SETUP: + case GAME_STATE.SETUP: return messages?.waiting || "Préparation de la partie"; - case GameState.PLACEMENT: + case GAME_STATE.PLACEMENT: return "Phase de placement"; - case GameState.PLAYING: + case GAME_STATE.PLAYING: if (captured) return messages?.captured || "Vous avez été éliminé..."; if (!outOfZone) return "La partie est en cours"; if (!hasHandicap) return `Veuillez retourner dans la zone\nHandicap dans ${secondsToMMSS(-timeLeftOutOfZone)}`; else return `Veuillez retourner dans la zone\nVotre position est révélée en continue`; - case GameState.FINISHED: + case GAME_STATE.FINISHED: return `Vous avez ${captured ? (messages?.loser || "perdu...") : (messages?.winner || "gagné !")}`; default: return "Inconnue"; @@ -56,12 +58,8 @@ const Interface = () => { // Activating geolocation tracking useEffect(() => { - if (loggedIn) { - startLocationTracking(); - } else { - stopLocationTracking(); - } - }, [startLocationTracking, stopLocationTracking, loggedIn]); + startLocationTracking(); + }, []); return ( @@ -83,12 +81,12 @@ const Interface = () => { - { gameState == GameState.PLACEMENT && + { gameState == GAME_STATE.PLACEMENT && {ready ? "Placé" : "Non placé"} } - { gameState == GameState.PLAYING && !captured && + { gameState == GAME_STATE.PLAYING && !captured && } @@ -99,12 +97,12 @@ const Interface = () => { setBottomContainerHeight(event.nativeEvent.layout.height)}> - { gameState == GameState.PLAYING && !captured && !hasHandicap && + { gameState == GAME_STATE.PLAYING && !captured && !hasHandicap && } - { gameState == GameState.PLAYING && !captured && + { gameState == GAME_STATE.PLAYING && !captured && } @@ -116,7 +114,7 @@ export default Interface; const styles = StyleSheet.create({ globalContainer: { - backgroundColor: Colors.background, + backgroundColor: COLORS.background, flex: 1, }, topContainer: { diff --git a/mobile/traque-app/components/drawer.jsx b/mobile/traque-app/components/drawer.jsx index a40b1c6..96cd974 100644 --- a/mobile/traque-app/components/drawer.jsx +++ b/mobile/traque-app/components/drawer.jsx @@ -8,17 +8,20 @@ import { CustomImage } from './image'; import { CustomTextInput } from './input'; import { Stat } from './stat'; // Contexts +import { useTeamConnexion } from '../context/teamConnexionContext'; import { useTeamContext } from '../context/teamContext'; // Hooks import { useTimeDifference } from '../hook/useTimeDifference'; import { useGame } from '../hook/useGame'; +// Services +import { enemyImage } from '../services/imageService'; // Util -import { GameState } from '../util/gameState'; -import { Colors } from '../util/colors'; import { secondsToHHMMSS } from '../util/functions'; -import { useImageApi } from '../hook/useImageApi'; +// Constants +import { GAME_STATE, COLORS } from '../constants'; export const Drawer = ({ height }) => { + const { teamId } = useTeamConnexion(); const [collapsibleState, setCollapsibleState] = useState(true); const [enemyCaptureCode, setEnemyCaptureCode] = useState(""); const {teamInfos, gameState, startDate} = useTeamContext(); @@ -27,7 +30,6 @@ export const Drawer = ({ height }) => { const [timeSinceStart] = useTimeDifference(startDate, 1000); const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"}; - const { enemyImage } = useImageApi(); const avgSpeed = useMemo(() => { const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600; @@ -76,13 +78,13 @@ export const Drawer = ({ height }) => { - { gameState == GameState.PLAYING && + { gameState == GAME_STATE.PLAYING && Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")} } - { gameState == GameState.PLAYING && !hasHandicap && + { gameState == GAME_STATE.PLAYING && !hasHandicap && {"Cible (" + (enemyName ?? "Indisponible") + ")"} - + @@ -122,7 +124,7 @@ const styles = StyleSheet.create({ }, innerDrawerContainer: { width: "100%", - backgroundColor: Colors.background, + backgroundColor: COLORS.background, borderTopLeftRadius: 30, borderTopRightRadius: 30, overflow: 'hidden', @@ -136,7 +138,7 @@ const styles = StyleSheet.create({ collapsibleWindow: { width: "100%", justifyContent: 'center', - backgroundColor: Colors.background, + backgroundColor: COLORS.background, }, collapsibleContent: { paddingHorizontal: 15, diff --git a/mobile/traque-app/components/map.jsx b/mobile/traque-app/components/map.jsx index 8375f25..4711223 100644 --- a/mobile/traque-app/components/map.jsx +++ b/mobile/traque-app/components/map.jsx @@ -7,20 +7,22 @@ import LinearGradient from 'react-native-linear-gradient'; import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer'; // Contexts import { useTeamContext } from '../context/teamContext'; +// Hook +import { useLocation } from '../hook/useLocation'; // Util -import { GameState } from '../util/gameState'; -import { ZoneTypes, InitialRegions } from '../util/constants'; +import { ZONE_TYPES, INITIAL_REGIONS, GAME_STATE } from '../constants'; export const CustomMap = () => { - const {teamInfos, zoneType, zoneExtremities, location, gameState} = useTeamContext(); + const { location } = useLocation(); + const {teamInfos, zoneType, zoneExtremities, gameState} = useTeamContext(); const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos; const [centerMap, setCenterMap] = useState(true); const mapRef = useRef(null); // Center the map on user position useEffect(() => { - if (centerMap && mapRef.current && location) { - mapRef.current.animateToRegion({latitude: location[0], longitude: location[1], latitudeDelta: 0, longitudeDelta: 0.02}, 1000); + if (centerMap && location && mapRef.current) { + mapRef.current.animateToRegion({...location, latitudeDelta: 0, longitudeDelta: 0.02}, 1000); } }, [centerMap, location]); @@ -30,7 +32,7 @@ export const CustomMap = () => { const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng}); const startZone = useMemo(() => { - if (gameState != GameState.PLACEMENT || !startingArea) return null; + if (gameState != GAME_STATE.PLACEMENT || !startingArea) return null; return ( @@ -38,7 +40,7 @@ export const CustomMap = () => { }, [gameState, startingArea]); const gameZone = useMemo(() => { - if (gameState !== GameState.PLAYING || !zoneExtremities) return null; + if (gameState !== GAME_STATE.PLAYING || !zoneExtremities) return null; const items = []; @@ -47,7 +49,7 @@ export const CustomMap = () => { const strokeWidth = 3; const lineDashPattern = [30, 10]; - if (zoneType === ZoneTypes.circle) { + if (zoneType === ZONE_TYPES.CIRCLE) { if (zoneExtremities.begin) items.push( { lineDashPattern={lineDashPattern} /> ); - } else if (zoneType === ZoneTypes.polygon) { + } else if (zoneType === ZONE_TYPES.POLYGON) { if (zoneExtremities.begin) items.push( { if (!location) return null; return ( - Alert.alert("Position actuelle", "Ceci est votre position")}> + Alert.alert("Position actuelle", "Ceci est votre position")}> ); }, [location]); const lastPositionMarker = useMemo(() => { - if (gameState != GameState.PLAYING || !lastSentLocation || hasHandicap) return null; + if (gameState != GAME_STATE.PLAYING || !lastSentLocation || hasHandicap) return null; return ( Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}> @@ -112,7 +114,7 @@ export const CustomMap = () => { }, [gameState, hasHandicap, lastSentLocation]); const enemyPositionMarker = useMemo(() => { - if (gameState != GameState.PLAYING || !enemyLocation || hasHandicap) return null; + if (gameState != GAME_STATE.PLAYING || !enemyLocation || hasHandicap) return null; return ( Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}> @@ -124,7 +126,7 @@ export const CustomMap = () => { return ( - setCenterMap(false)} toolbarEnabled={false}> + setCenterMap(false)} toolbarEnabled={false}> {startZone} {gameZone} {currentPositionMarker} diff --git a/mobile/traque-app/util/colors.js b/mobile/traque-app/constants/colors.js similarity index 54% rename from mobile/traque-app/util/colors.js rename to mobile/traque-app/constants/colors.js index bad3068..35f1e88 100644 --- a/mobile/traque-app/util/colors.js +++ b/mobile/traque-app/constants/colors.js @@ -1,3 +1,3 @@ -export const Colors = { +export const COLORS = { background: '#f5f5f5' }; diff --git a/mobile/traque-app/constants/config.js b/mobile/traque-app/constants/config.js new file mode 100644 index 0000000..940be22 --- /dev/null +++ b/mobile/traque-app/constants/config.js @@ -0,0 +1,2 @@ +export const SERVER_URL = process.env.EXPO_PUBLIC_SERVER_URL; +export const SOCKET_URL = process.env.EXPO_PUBLIC_SOCKET_URL; diff --git a/mobile/traque-app/constants/game.js b/mobile/traque-app/constants/game.js new file mode 100644 index 0000000..2815623 --- /dev/null +++ b/mobile/traque-app/constants/game.js @@ -0,0 +1,11 @@ +export const GAME_STATE = { + SETUP: "setup", + PLACEMENT: "placement", + PLAYING: "playing", + FINISHED: "finished" +}; + +export const ZONE_TYPES = { + CIRCLE: "circle", + POLYGON: "polygon" +}; diff --git a/mobile/traque-app/constants/index.js b/mobile/traque-app/constants/index.js new file mode 100644 index 0000000..f0e4d6e --- /dev/null +++ b/mobile/traque-app/constants/index.js @@ -0,0 +1,4 @@ +export * from './config'; +export * from './game'; +export * from './map'; +export * from './colors'; diff --git a/mobile/traque-app/constants/map.js b/mobile/traque-app/constants/map.js new file mode 100644 index 0000000..967ce80 --- /dev/null +++ b/mobile/traque-app/constants/map.js @@ -0,0 +1,32 @@ +export const INITIAL_REGIONS = { + PARIS : { + latitude: 48.864, + longitude: 2.342, + latitudeDelta: 0, + longitudeDelta: 50 + } +}; + +export const LOCATION_PARAMETERS = { + LOCAL: { + accuracy: 4, // High + distanceInterval: 3, // meters + timeInterval: 1000, // ms + }, + SERVER: { + accuracy: 4, // High + distanceInterval: 5, // meters + timeInterval: 5000, // ms + showsBackgroundLocationIndicator: true, // iOS only + pausesUpdatesAutomatically: false, // (iOS) Prevents auto-pausing of location updates + foregroundService: { + notificationTitle: "Enregistrement de votre position.", + notificationBody: "L'application utilise votre position en arrière plan.", + notificationColor: "#FF0000", // (Android) Notification icon color + }, + } +}; + +export const TASKS = { + BACKGROUND_LOCATION: "background-location-task" +}; diff --git a/mobile/traque-app/context/socketContext.jsx b/mobile/traque-app/context/socketContext.jsx deleted file mode 100644 index bb84505..0000000 --- a/mobile/traque-app/context/socketContext.jsx +++ /dev/null @@ -1,22 +0,0 @@ -// React -import { createContext, useContext, useMemo } from "react"; -// IO -import { io } from "socket.io-client"; -// Util -import { SOCKET_URL } from "../util/constants"; - -const SocketContext = createContext(); - -const teamSocket = io(SOCKET_URL, {path: "/back/socket.io"}); - -export const SocketProvider = ({ children }) => { - const value = useMemo(() => ({ teamSocket }), []); - - return ( - {children} - ); -}; - -export const useSocket = () => { - return useContext(SocketContext); -}; diff --git a/mobile/traque-app/context/teamContext.jsx b/mobile/traque-app/context/teamContext.jsx index 65c3ed3..d0cab3b 100644 --- a/mobile/traque-app/context/teamContext.jsx +++ b/mobile/traque-app/context/teamContext.jsx @@ -1,24 +1,30 @@ // React -import { createContext, useContext, useMemo, useState } from "react"; +import { createContext, useContext, useMemo, useState, useEffect } from "react"; // Context -import { useSocket } from "./socketContext"; import { useTeamConnexion } from "./teamConnexionContext"; // Hook import { useSendDeviceInfo } from "../hook/useSendDeviceInfo"; -import { useLocation } from "../hook/useLocation"; -import { useSocketListener } from "../hook/useSocketListener"; -// Util -import { GameState } from "../util/gameState"; +// Services +import { socket } from "../services/socket"; +// Constants +import { GAME_STATE } from "../constants"; const TeamContext = createContext(); +const useSocketListener = (event, callback) => { + useEffect(() => { + socket.on(event, callback); + return () => { + socket.off(event, callback); + }; + }, [callback, event]); +}; + export const TeamProvider = ({children}) => { - const {teamSocket} = useSocket(); - const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10); // update_team const [teamInfos, setTeamInfos] = useState({}); // game_state - const [gameState, setGameState] = useState(GameState.SETUP); + const [gameState, setGAME_STATE] = useState(GAME_STATE.SETUP); const [startDate, setStartDate] = useState(null); // current_zone const [zoneExtremities, setZoneExtremities] = useState(null); @@ -31,16 +37,16 @@ export const TeamProvider = ({children}) => { useSendDeviceInfo(); - useSocketListener(teamSocket, "update_team", (data) => { + useSocketListener("update_team", (data) => { setTeamInfos(teamInfos => ({...teamInfos, ...data})); }); - useSocketListener(teamSocket, "game_state", (data) => { - setGameState(data.state); + useSocketListener("game_state", (data) => { + setGAME_STATE(data.state); setStartDate(data.date); }); - useSocketListener(teamSocket, "settings", (data) => { + useSocketListener("settings", (data) => { setMessages(data.messages); setZoneType(data.zone.type); //TODO @@ -48,16 +54,16 @@ export const TeamProvider = ({children}) => { //setOutOfZoneDelay(data.outOfZoneDelay); }); - useSocketListener(teamSocket, "current_zone", (data) => { + useSocketListener("current_zone", (data) => { setZoneExtremities({begin: data.begin, end: data.end}); setNextZoneDate(data.endDate); }); - useSocketListener(teamSocket, "logout", logout); + useSocketListener("logout", logout); const value = useMemo(() => ( - {teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking} - ), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking]); + {teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages} + ), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages]); return ( diff --git a/mobile/traque-app/hook/useGame.jsx b/mobile/traque-app/hook/useGame.jsx index ba13cba..91624d4 100644 --- a/mobile/traque-app/hook/useGame.jsx +++ b/mobile/traque-app/hook/useGame.jsx @@ -1,14 +1,13 @@ // React import { useCallback } from "react"; -// Hook -import { useSocketCommands } from "./useSocketCommands"; +// Services +import { emitSendPosition, emitCapture } from "../services/socketEmitter"; export const useGame = () => { - const { emitSendPosition, emitCapture } = useSocketCommands(); const sendCurrentPosition = useCallback(() => { emitSendPosition(); - }, [emitSendPosition]); + }, []); const capture = useCallback((captureCode) => { return new Promise((resolve, reject) => { @@ -22,7 +21,7 @@ export const useGame = () => { resolve(response); }); }); - }, [emitCapture]); + }, []); return { sendCurrentPosition, capture }; }; diff --git a/mobile/traque-app/hook/useImageApi.jsx b/mobile/traque-app/hook/useImageApi.jsx deleted file mode 100644 index 7149c45..0000000 --- a/mobile/traque-app/hook/useImageApi.jsx +++ /dev/null @@ -1,45 +0,0 @@ -// Rect -import { useCallback, useMemo } from "react"; -// Contexts -import { useTeamConnexion } from "../context/teamConnexionContext"; -import { useTeamContext } from '../context/teamContext'; -// Util -import { SERVER_URL } from "../util/constants"; - -export const useImageApi = () => { - const { teamId } = useTeamConnexion(); - const { teamInfos } = useTeamContext(); - const { enemyName } = teamInfos; - - const uploadTeamImage = useCallback(async (imageUri) => { - if (!imageUri || !teamId) return; - - const data = new FormData(); - data.append('file', { - uri: imageUri, - name: 'photo.jpg', - type: 'image/jpeg', - }); - - try { - const response = await fetch(`${SERVER_URL}/upload?team=${teamId}`, { - method: 'POST', - body: data, - }); - - if (!response.ok) throw new Error("Échec de l'upload"); - return await response.blob(); - } catch (error) { - console.error("Erreur uploadImage :", error); - throw error; - } - }, [teamId]); - - const enemyImage = useMemo(() => { - if (!teamId || !enemyName) return require('../assets/images/missing_image.jpg'); - - return {uri: `${SERVER_URL}/photo/enemy?team=${teamId}`}; - }, [teamId, enemyName]); - - return { enemyImage, uploadTeamImage }; -}; diff --git a/mobile/traque-app/hook/useLocation.jsx b/mobile/traque-app/hook/useLocation.jsx index b616984..bdba8ef 100644 --- a/mobile/traque-app/hook/useLocation.jsx +++ b/mobile/traque-app/hook/useLocation.jsx @@ -1,82 +1,33 @@ // React -import { useEffect, useState, useCallback, useMemo } from "react"; -import { Alert } from "react-native"; +import { useState, useEffect } from 'react'; // Expo -import { defineTask, isTaskRegisteredAsync } from "expo-task-manager"; import * as Location from 'expo-location'; -// Hook -import { useSocketCommands } from "./useSocketCommands"; +// Constants +import { LOCATION_PARAMETERS } from '../constants'; -export const useLocation = (timeInterval, distanceInterval) => { - const { emitUpdatePosition } = useSocketCommands(); - const [location, setLocation] = useState(null); // [latitude, longitude] - const LOCATION_TASK_NAME = "background-location-task"; - const locationUpdateParameters = useMemo(() => ({ - accuracy: Location.Accuracy.High, - distanceInterval: distanceInterval, // Update every 10 meters - timeInterval: timeInterval, // Minimum interval in ms - showsBackgroundLocationIndicator: true, // iOS only - pausesUpdatesAutomatically: false, // (iOS) Prevents auto-pausing of location updates - foregroundService: { - notificationTitle: "Enregistrement de votre position.", - notificationBody: "L'application utilise votre position en arrière plan.", - notificationColor: "#FF0000", // (Android) Notification icon color - }, - }), [distanceInterval, timeInterval]); - - defineTask(LOCATION_TASK_NAME, async ({ data, error }) => { - if (error) { - console.error(error); - return; - } - if (data) { - const { locations } = data; - if (locations.length > 0) { - const firstLocation = locations[0]; - const { latitude, longitude } = firstLocation.coords; - const new_location = [latitude, longitude]; - try { - setLocation(new_location); - } catch (e) { - console.warn("setLocation failed (probably in background):", e); - } - emitUpdatePosition(new_location); - } else { - console.log("No location measured."); - } - } - }); - - const getLocationAuthorization = useCallback(async () => { - const { status : statusForeground } = await Location.requestForegroundPermissionsAsync(); - const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync(); - if (statusForeground !== "granted" || statusBackground !== "granted") { - Alert.alert("Échec", "Activez la localisation en arrière plan dans les paramètres."); - return false; - } else { - return true; - } - }, []); - - const startLocationTracking = useCallback(async () => { - if (await getLocationAuthorization()) { - if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) { - await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters); - console.log("Location tracking started."); - } - } - }, [getLocationAuthorization, locationUpdateParameters]); - - const stopLocationTracking = useCallback(async () => { - if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) { - await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME); - console.log("Location tracking stopped."); - } - }, []); +export const useLocation = () => { + const [location, setLocation] = useState(null); useEffect(() => { - getLocationAuthorization(); - }, [getLocationAuthorization]); + let subscription; - return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking]; + const startWatching = async () => { + const { status } = await Location.requestForegroundPermissionsAsync(); + + if (status !== 'granted') return; + + subscription = await Location.watchPositionAsync( + LOCATION_PARAMETERS, + (location) => setLocation(location.coords) + ); + }; + + startWatching(); + + return () => { + if (subscription) subscription.remove(); + }; + }, []); + + return { location }; }; diff --git a/mobile/traque-app/hook/useSendDeviceInfo.jsx b/mobile/traque-app/hook/useSendDeviceInfo.jsx index ab1be39..6d560a3 100644 --- a/mobile/traque-app/hook/useSendDeviceInfo.jsx +++ b/mobile/traque-app/hook/useSendDeviceInfo.jsx @@ -3,11 +3,10 @@ import { useEffect, useRef } from 'react'; import DeviceInfo from 'react-native-device-info'; // Context import { useTeamConnexion } from "../context/teamConnexionContext"; -// Hook -import { useSocketCommands } from "./useSocketCommands"; +// Services +import { emitBattery, emitDeviceInfo } from "../services/socketEmitter"; export const useSendDeviceInfo = () => { - const { emitBattery, emitDeviceInfo } = useSocketCommands(); const { loggedIn } = useTeamConnexion(); const isMounted = useRef(true); @@ -41,7 +40,7 @@ export const useSendDeviceInfo = () => { isMounted.current = false; clearInterval(batteryCheckInterval); }; - }, [emitBattery, emitDeviceInfo, loggedIn]); + }, [loggedIn]); return null; }; diff --git a/mobile/traque-app/hook/useSocketAuth.jsx b/mobile/traque-app/hook/useSocketAuth.jsx index 5efebaf..0b4fdfe 100644 --- a/mobile/traque-app/hook/useSocketAuth.jsx +++ b/mobile/traque-app/hook/useSocketAuth.jsx @@ -2,10 +2,10 @@ import { useState, useEffect, useCallback, useRef } from 'react'; // Hook import { useLocalStorage } from './useLocalStorage'; -import { useSocketCommands } from "./useSocketCommands"; +// Services +import { emitLogin, emitLogout } from "../services/socketEmitter"; export const useSocketAuth = () => { - const { emitLogin, emitLogout } = useSocketCommands(); const [loggedIn, setLoggedIn] = useState(false); const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null); const isMounted = useRef(true); @@ -42,7 +42,7 @@ export const useSocketAuth = () => { } }); }); - }, [emitLogin, setSavedPassword]); + }, [setSavedPassword]); useEffect(() => { if (!loggedIn && savedPassword) { @@ -54,7 +54,7 @@ export const useSocketAuth = () => { setLoggedIn(false); setSavedPassword(null); emitLogout(); - }, [emitLogout, setSavedPassword]); + }, [setSavedPassword]); return {login, logout, password: savedPassword, loggedIn}; }; diff --git a/mobile/traque-app/hook/useSocketCommands.jsx b/mobile/traque-app/hook/useSocketCommands.jsx deleted file mode 100644 index 0f9ae6d..0000000 --- a/mobile/traque-app/hook/useSocketCommands.jsx +++ /dev/null @@ -1,52 +0,0 @@ -// React -import { useCallback } from 'react'; -// Context -import { useSocket } from "../context/socketContext"; - -export const useSocketCommands = () => { - const { teamSocket } = useSocket(); - - const emitLogin = useCallback((password, callback) => { - if (!teamSocket?.connected) return; - console.log("Try to log in with :", password); - teamSocket.emit("login", password, callback); - }, [teamSocket]); - - const emitLogout = useCallback(() => { - if (!teamSocket?.connected) return; - console.log("Logout."); - teamSocket.emit("logout"); - }, [teamSocket]); - - const emitSendPosition = useCallback(() => { - if (!teamSocket?.connected) return; - console.log("Reveal position."); - teamSocket.emit("send_position"); - }, [teamSocket]); - - const emitUpdatePosition = useCallback((location) => { - if (!teamSocket?.connected) return; - console.log("Update position :", location); - teamSocket.emit("update_position", location); - }, [teamSocket]); - - const emitCapture = useCallback((captureCode, callback) => { - if (!teamSocket?.connected) return; - console.log("Try to capture :", captureCode); - teamSocket.emit("capture", captureCode, callback); - }, [teamSocket]); - - const emitBattery = useCallback((level) => { - if (!teamSocket?.connected) return; - console.log("Send battery level."); - teamSocket.emit('battery_update', level); - }, [teamSocket]); - - const emitDeviceInfo = useCallback((infos) => { - if (!teamSocket?.connected) return; - console.log("Send device infos."); - teamSocket.emit('device_info', infos); - }, [teamSocket]); - - return { emitLogin, emitLogout, emitSendPosition, emitUpdatePosition, emitCapture, emitBattery, emitDeviceInfo }; -}; diff --git a/mobile/traque-app/hook/useSocketListener.jsx b/mobile/traque-app/hook/useSocketListener.jsx deleted file mode 100644 index 55c86b6..0000000 --- a/mobile/traque-app/hook/useSocketListener.jsx +++ /dev/null @@ -1,11 +0,0 @@ -// React -import { useEffect } from "react"; - -export const useSocketListener = (socket, event, callback) => { - useEffect(() => { - socket.on(event, callback); - return () => { - socket.off(event, callback); - }; - }, [callback, event, socket]); -}; diff --git a/mobile/traque-app/services/backgroundLocationTask.js b/mobile/traque-app/services/backgroundLocationTask.js new file mode 100644 index 0000000..23b7bd2 --- /dev/null +++ b/mobile/traque-app/services/backgroundLocationTask.js @@ -0,0 +1,48 @@ +// Expo +import { defineTask, isTaskRegisteredAsync } from "expo-task-manager"; +import * as Location from 'expo-location'; +// Services +import { emitUpdatePosition } from "./socketEmitter"; +// Constants +import { TASKS, LOCATION_PARAMETERS } from "../constants"; + + +// Task + +defineTask(TASKS.BACKGROUND_LOCATION, async ({ data, error }) => { + if (error) { + console.error(error); + return; + } + if (data) { + const { locations } = data; + if (locations.length == 0) { + console.log("No location measured."); + return; + } + const { latitude, longitude } = locations[0].coords; + emitUpdatePosition([latitude, longitude]); + } +}); + + +// Functions + +export const getLocationAuthorization = async () => { + const { status : statusForeground } = await Location.requestForegroundPermissionsAsync(); + const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync(); + return statusForeground == "granted" && statusBackground == "granted"; +}; + +export const startLocationTracking = async () => { + if (!(await getLocationAuthorization())) return; + if (await isTaskRegisteredAsync(TASKS.BACKGROUND_LOCATION)) return; + console.log("Location tracking started."); + await Location.startLocationUpdatesAsync(TASKS.BACKGROUND_LOCATION, LOCATION_PARAMETERS.SERVER); +}; + +export const stopLocationTracking = async () => { + if (!await isTaskRegisteredAsync(TASKS.BACKGROUND_LOCATION)) return; + console.log("Location tracking stopped."); + await Location.stopLocationUpdatesAsync(TASKS.BACKGROUND_LOCATION); +}; diff --git a/mobile/traque-app/services/imageService.js b/mobile/traque-app/services/imageService.js new file mode 100644 index 0000000..0711fdb --- /dev/null +++ b/mobile/traque-app/services/imageService.js @@ -0,0 +1,32 @@ +// Constants +import { SERVER_URL } from "../constants"; + +export const uploadTeamImage = async (id, imageUri) => { + if (!imageUri || !id) return; + + const data = new FormData(); + data.append('file', { + uri: imageUri, + name: 'photo.jpg', + type: 'image/jpeg', + }); + + try { + const response = await fetch(`${SERVER_URL}/upload?team=${id}`, { + method: 'POST', + body: data, + }); + + if (!response.ok) throw new Error("Échec de l'upload"); + return await response.blob(); + } catch (error) { + console.error("Erreur uploadImage :", error); + throw error; + } +}; + +export const enemyImage = (id) => { + if (!id) return require('../assets/images/missing_image.jpg'); + + return {uri: `${SERVER_URL}/photo/enemy?team=${id}`}; +}; diff --git a/mobile/traque-app/services/socket.js b/mobile/traque-app/services/socket.js new file mode 100644 index 0000000..aa629fb --- /dev/null +++ b/mobile/traque-app/services/socket.js @@ -0,0 +1,8 @@ +// Socket +import { io } from "socket.io-client"; +// Constants +import { SOCKET_URL } from "../constants"; + +export const socket = io(SOCKET_URL, { + path: "/back/socket.io" +}); diff --git a/mobile/traque-app/services/socketEmitter.js b/mobile/traque-app/services/socketEmitter.js new file mode 100644 index 0000000..e788747 --- /dev/null +++ b/mobile/traque-app/services/socketEmitter.js @@ -0,0 +1,44 @@ +// Services +import { socket } from "./socket"; + +export const emitLogin = (password, callback) => { + if (!socket?.connected) return; + console.log("Try to log in with :", password); + socket.emit("login", password, callback); +}; + +export const emitLogout = () => { + if (!socket?.connected) return; + console.log("Logout."); + socket.emit("logout"); +}; + +export const emitSendPosition = () => { + if (!socket?.connected) return; + console.log("Reveal position."); + socket.emit("send_position"); +}; + +export const emitUpdatePosition = (location) => { + if (!socket?.connected) return; + console.log("Update position :", location); + socket.emit("update_position", location); +}; + +export const emitCapture = (captureCode, callback) => { + if (!socket?.connected) return; + console.log("Try to capture :", captureCode); + socket.emit("capture", captureCode, callback); +}; + +export const emitBattery = (level) => { + if (!socket?.connected) return; + console.log("Send battery level."); + socket.emit('battery_update', level); +}; + +export const emitDeviceInfo = (infos) => { + if (!socket?.connected) return; + console.log("Send device infos."); + socket.emit('device_info', infos); +}; diff --git a/mobile/traque-app/util/constants.js b/mobile/traque-app/util/constants.js deleted file mode 100644 index cabfae7..0000000 --- a/mobile/traque-app/util/constants.js +++ /dev/null @@ -1,16 +0,0 @@ -export const SERVER_URL = process.env.EXPO_PUBLIC_SERVER_URL; -export const SOCKET_URL = process.env.EXPO_PUBLIC_SOCKET_URL; - -export const InitialRegions = { - paris : { - latitude: 48.864, - longitude: 2.342, - latitudeDelta: 0, - longitudeDelta: 50 - } -}; - -export const ZoneTypes = { - circle: "circle", - polygon: "polygon" -}; diff --git a/mobile/traque-app/util/gameState.js b/mobile/traque-app/util/gameState.js deleted file mode 100644 index 3333b42..0000000 --- a/mobile/traque-app/util/gameState.js +++ /dev/null @@ -1,6 +0,0 @@ -export const GameState = { - SETUP: "setup", - PLACEMENT: "placement", - PLAYING: "playing", - FINISHED: "finished" -};