mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-04-10 16:30:18 +02:00
Fix photos + API hooks + cleaning
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -137,3 +137,6 @@ yarn.lock
|
||||
keys/
|
||||
temp.*
|
||||
temp/
|
||||
.env
|
||||
.env.development
|
||||
.env.production
|
||||
|
||||
@@ -10,7 +10,8 @@ Problèmes
|
||||
|
||||
[ ] Une équipe perdait sans arrêt la connection avec le serveur
|
||||
[ ] La position en arrière plan, téléphone éteint par exemple, n'avait pas l'air de fonctionner (une équipe n'avait pas de notif)
|
||||
[ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe.
|
||||
[x] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe. (Potentiellement un problème
|
||||
de conversion : 046512 -> 46512)
|
||||
[x] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus
|
||||
automatique.
|
||||
[x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les
|
||||
|
||||
@@ -7,12 +7,30 @@ lang: en-GB
|
||||
This tutorial will help you to set up your development environment, use a dev build and create an APK.
|
||||
|
||||
## Table of Contents
|
||||
* [Environment](#environment) : Dependencies, packages, app key and device
|
||||
* [Environment](#environment) : Dependencies, packages, app key, device and .env
|
||||
* [Dev build](#dev-build) : Create, install and use
|
||||
* [APK](#apk) : Create and install
|
||||
|
||||
## Environment
|
||||
|
||||
### .env files
|
||||
|
||||
Some infos like API keys or IP addresses cannot be pushed on the public repository, therefore you have to create the .env files that will store those values. Go in the `traque-app` folder, create those two files and replace the FILL_HERE with the correct values (you can ask someone already working on the project) :
|
||||
|
||||
* `.env.development` :
|
||||
|
||||
```.env
|
||||
EXPO_PUBLIC_SERVER_URL=FILL_HERE
|
||||
EXPO_PUBLIC_SOCKET_URL=FILL_HERE
|
||||
```
|
||||
|
||||
* `.env.production` :
|
||||
|
||||
```.env
|
||||
EXPO_PUBLIC_SERVER_URL=FILL_HERE
|
||||
EXPO_PUBLIC_SOCKET_URL=FILL_HERE
|
||||
```
|
||||
|
||||
### Installing dependencies and preparing the device
|
||||
|
||||
You will need to install Android Studio, some SDKs and prepare your device if you want to use a physical one. Follow this [tutorial](https://reactnative.dev/docs/set-up-your-environment?os=linux&platform=android).
|
||||
|
||||
@@ -8,22 +8,22 @@ import { CustomButton } from '../components/button';
|
||||
import { CustomImage } from '../components/image';
|
||||
import { CustomTextInput } from '../components/input';
|
||||
// Contexts
|
||||
import { useSocket } from "../context/socketContext";
|
||||
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';
|
||||
|
||||
const Index = () => {
|
||||
const router = useRouter();
|
||||
const {SERVER_URL} = useSocket();
|
||||
const {login, loggedIn, loading} = useTeamConnexion();
|
||||
const {login, loggedIn} = useTeamConnexion();
|
||||
const {getLocationAuthorization, stopLocationTracking} = useTeamContext();
|
||||
const {image, pickImage, sendImage} = usePickImage();
|
||||
const [teamID, setTeamID] = useState("");
|
||||
const {image, pickImage} = usePickImage();
|
||||
const [teamId, setTeamId] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { uploadTeamImage } = useImageApi();
|
||||
|
||||
// Disbaling location tracking
|
||||
useEffect(() => {
|
||||
@@ -32,32 +32,37 @@ const Index = () => {
|
||||
|
||||
// Routeur
|
||||
useEffect(() => {
|
||||
if (!loading && loggedIn) {
|
||||
if (loggedIn) {
|
||||
uploadTeamImage(image?.uri);
|
||||
router.replace("/interface");
|
||||
}
|
||||
}, [router, loggedIn, loading]);
|
||||
}, [router, loggedIn, uploadTeamImage, image]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isSubmitting || !getLocationAuthorization()) return;
|
||||
|
||||
function handleSubmit() {
|
||||
if (!isSubmitting && !loading) {
|
||||
setIsSubmitting(true);
|
||||
if (getLocationAuthorization()) {
|
||||
login(parseInt(teamID))
|
||||
.then((response) => {
|
||||
|
||||
const regex = /^\d{6}$/;
|
||||
if (!regex.test(teamId)) {
|
||||
setTimeout(() => Alert.alert("Erreur", "Veuillez entrer un ID d'équipe valide."), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await login(teamId);
|
||||
|
||||
if (response.isLoggedIn) {
|
||||
sendImage(`${SERVER_URL}/upload?team=${teamID}`);
|
||||
setTeamId("");
|
||||
} else {
|
||||
Alert.alert("Échec", "L'ID d'équipe est inconnu.");
|
||||
setTimeout(() => Alert.alert("Échec", "L'ID d'équipe est inconnu."), 100);
|
||||
}
|
||||
} catch (error) {
|
||||
setTimeout(() => Alert.alert("Échec", "La connexion au serveur a échoué."), 100);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch(() => {
|
||||
Alert.alert("Échec", "La connection au serveur a échoué.");
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
}
|
||||
setTeamID("");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
@@ -67,7 +72,7 @@ const Index = () => {
|
||||
<Text style={styles.logoText}>LA TRAQUE</Text>
|
||||
</View>
|
||||
<View style={styles.subContainer}>
|
||||
<CustomTextInput value={teamID} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamID}/>
|
||||
<CustomTextInput value={teamId} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamId}/>
|
||||
</View>
|
||||
<View style={styles.subContainer}>
|
||||
<Text style={{fontSize: 15}}>Appuyer pour changer la photo d'équipe</Text>
|
||||
@@ -75,7 +80,7 @@ const Index = () => {
|
||||
<CustomImage source={image ? {uri: image.uri} : require('../assets/images/missing_image.jpg')} onPress={pickImage}/>
|
||||
</View>
|
||||
<View style={styles.subContainer}>
|
||||
<CustomButton label={(isSubmitting || loading) ? "..." : "Valider"} onPress={handleSubmit}/>
|
||||
<CustomButton label={isSubmitting ? "..." : "Valider"} onPress={handleSubmit}/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -20,9 +20,10 @@ import { Colors } from '../util/colors';
|
||||
|
||||
const Interface = () => {
|
||||
const router = useRouter();
|
||||
const {messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
|
||||
const {loggedIn, logout, loading} = useTeamConnexion();
|
||||
const {name, ready, captured, locationSendDeadline, sendCurrentPosition, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame();
|
||||
const {teamInfos, messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
|
||||
const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos;
|
||||
const {loggedIn, logout} = useTeamConnexion();
|
||||
const {sendCurrentPosition} = useGame();
|
||||
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
|
||||
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
|
||||
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
|
||||
@@ -48,12 +49,10 @@ const Interface = () => {
|
||||
|
||||
// Router
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!loggedIn) {
|
||||
router.replace("/");
|
||||
}
|
||||
}
|
||||
}, [router, loggedIn, loading]);
|
||||
}, [router, loggedIn]);
|
||||
|
||||
// Activating geolocation tracking
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { CustomTextInput } from './input';
|
||||
import { Stat } from './stat';
|
||||
// Contexts
|
||||
import { useTeamContext } from '../context/teamContext';
|
||||
import { useSocket } from '../context/socketContext';
|
||||
// Hooks
|
||||
import { useTimeDifference } from '../hook/useTimeDifference';
|
||||
import { useGame } from '../hook/useGame';
|
||||
@@ -17,17 +16,18 @@ import { useGame } from '../hook/useGame';
|
||||
import { GameState } from '../util/gameState';
|
||||
import { Colors } from '../util/colors';
|
||||
import { secondsToHHMMSS } from '../util/functions';
|
||||
import { useImageApi } from '../hook/useImageApi';
|
||||
|
||||
export const Drawer = ({ height }) => {
|
||||
const [collapsibleState, setCollapsibleState] = useState(true);
|
||||
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
|
||||
const {SERVER_URL} = useSocket();
|
||||
const {gameState, startDate} = useTeamContext();
|
||||
const {capture, enemyName, captureCode, name, teamId, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = useGame();
|
||||
const {teamInfos, gameState, startDate} = useTeamContext();
|
||||
const {enemyName, captureCode, name, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = teamInfos;
|
||||
const {capture} = useGame();
|
||||
const [timeSinceStart] = useTimeDifference(startDate, 1000);
|
||||
const [enemyImageURI, setEnemyImageURI] = useState("../assets/images/missing_image.jpg");
|
||||
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;
|
||||
@@ -48,11 +48,6 @@ export const Drawer = ({ height }) => {
|
||||
}
|
||||
}, [captureStatus]);
|
||||
|
||||
// Refresh the image
|
||||
useEffect(() => {
|
||||
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
|
||||
}, [SERVER_URL, enemyName, teamId]);
|
||||
|
||||
const handleCapture = () => {
|
||||
if (captureStatus != 1) {
|
||||
setCaptureStatus(1);
|
||||
@@ -86,8 +81,8 @@ export const Drawer = ({ height }) => {
|
||||
}
|
||||
{ gameState == GameState.PLAYING && !hasHandicap && <Fragment>
|
||||
<View style={styles.imageContainer}>
|
||||
{<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>}
|
||||
{<CustomImage source={{ uri : enemyImageURI }} canZoom/>}
|
||||
<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>
|
||||
<CustomImage source={enemyImage} canZoom/>
|
||||
</View>
|
||||
<View style={styles.actionsContainer}>
|
||||
<View style={styles.actionsLeftContainer}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// React
|
||||
import { Fragment } from 'react';
|
||||
import { Polygon } from 'react-native-maps';
|
||||
import { circleToPolygon } from '../util/functions';
|
||||
|
||||
@@ -7,15 +7,13 @@ import LinearGradient from 'react-native-linear-gradient';
|
||||
import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer';
|
||||
// Contexts
|
||||
import { useTeamContext } from '../context/teamContext';
|
||||
// Hooks
|
||||
import { useGame } from '../hook/useGame';
|
||||
// Util
|
||||
import { GameState } from '../util/gameState';
|
||||
import { ZoneTypes, InitialRegions } from '../util/constants';
|
||||
|
||||
export const CustomMap = () => {
|
||||
const {zoneType, zoneExtremities, location, gameState} = useTeamContext();
|
||||
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = useGame();
|
||||
const {teamInfos, zoneType, zoneExtremities, location, gameState} = useTeamContext();
|
||||
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos;
|
||||
const [centerMap, setCenterMap] = useState(true);
|
||||
const mapRef = useRef(null);
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
// React
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
// IO
|
||||
import { io } from "socket.io-client";
|
||||
// Util
|
||||
import { SOCKET_URL } from "../util/constants";
|
||||
|
||||
const IP = "172.16.1.180";
|
||||
const SOCKET_URL = `ws://${IP}/player`;
|
||||
const SERVER_URL = `http://${IP}/back`;
|
||||
const SocketContext = createContext();
|
||||
|
||||
export const teamSocket = io(SOCKET_URL, {
|
||||
path: "/back/socket.io",
|
||||
});
|
||||
|
||||
export const SocketContext = createContext();
|
||||
const teamSocket = io(SOCKET_URL, {path: "/back/socket.io"});
|
||||
|
||||
export const SocketProvider = ({ children }) => {
|
||||
const value = useMemo(() => ({ teamSocket, SERVER_URL }), []);
|
||||
const value = useMemo(() => ({ teamSocket }), []);
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
// React
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { useSocket } from "./socketContext";
|
||||
// Hook
|
||||
import { useSocketAuth } from "../hook/useSocketAuth";
|
||||
|
||||
const teamConnexionContext = createContext();
|
||||
const TeamConnexionContext = createContext();
|
||||
|
||||
export const TeamConnexionProvider = ({ children }) => {
|
||||
const { teamSocket } = useSocket();
|
||||
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
|
||||
const { login, password: teamId, loggedIn, logout } = useSocketAuth();
|
||||
|
||||
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading}), [teamId, login, logout, loggedIn, loading]);
|
||||
const value = useMemo(() => ({ teamId, login, logout, loggedIn}), [teamId, login, logout, loggedIn]);
|
||||
|
||||
return (
|
||||
<teamConnexionContext.Provider value={value}>
|
||||
<TeamConnexionContext.Provider value={value}>
|
||||
{children}
|
||||
</teamConnexionContext.Provider>
|
||||
</TeamConnexionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTeamConnexion = () => {
|
||||
return useContext(teamConnexionContext);
|
||||
return useContext(TeamConnexionContext);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
// React
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
// Context
|
||||
import { useSocket } from "./socketContext";
|
||||
import { useTeamConnexion } from "./teamConnexionContext";
|
||||
import { GameState } from "../util/gameState";
|
||||
// Hook
|
||||
import { useSendDeviceInfo } from "../hook/useSendDeviceInfo";
|
||||
import { useLocation } from "../hook/useLocation";
|
||||
import { useSocketListener } from "../hook/useSocketListener";
|
||||
// Util
|
||||
import { GameState } from "../util/gameState";
|
||||
|
||||
const teamContext = createContext();
|
||||
const TeamContext = createContext();
|
||||
|
||||
export const TeamProvider = ({children}) => {
|
||||
const {teamSocket} = useSocket();
|
||||
@@ -51,18 +55,17 @@ export const TeamProvider = ({children}) => {
|
||||
|
||||
useSocketListener(teamSocket, "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]);
|
||||
|
||||
return (
|
||||
<teamContext.Provider value={value}>
|
||||
<TeamContext.Provider value={value}>
|
||||
{children}
|
||||
</teamContext.Provider>
|
||||
</TeamContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTeamContext = () => {
|
||||
return useContext(teamContext);
|
||||
return useContext(TeamContext);
|
||||
};
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import { useSocket } from "../context/socketContext";
|
||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||
import { useTeamContext } from "../context/teamContext";
|
||||
// React
|
||||
import { useCallback } from "react";
|
||||
// Hook
|
||||
import { useSocketCommands } from "./useSocketCommands";
|
||||
|
||||
export const useGame = () => {
|
||||
const { teamSocket } = useSocket();
|
||||
const { teamId } = useTeamConnexion();
|
||||
const { teamInfos } = useTeamContext();
|
||||
const { emitSendPosition, emitCapture } = useSocketCommands();
|
||||
|
||||
function sendCurrentPosition() {
|
||||
console.log("Reveal position.");
|
||||
teamSocket.emit("send_position");
|
||||
}
|
||||
|
||||
function capture(captureCode) {
|
||||
console.log("Try to capture :", captureCode);
|
||||
const sendCurrentPosition = useCallback(() => {
|
||||
emitSendPosition();
|
||||
}, [emitSendPosition]);
|
||||
|
||||
const capture = useCallback((captureCode) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn("Server did not respond to capture emit.");
|
||||
reject();
|
||||
console.warn("Server timeout: capture", captureCode);
|
||||
reject(new Error("Timeout"));
|
||||
}, 3000);
|
||||
|
||||
teamSocket.emit("capture", captureCode, (response) => {
|
||||
emitCapture(captureCode, (response) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(response.message);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [emitCapture]);
|
||||
|
||||
return {...teamInfos, sendCurrentPosition, capture, teamId};
|
||||
return { sendCurrentPosition, capture };
|
||||
};
|
||||
|
||||
45
mobile/traque-app/hook/useImageApi.jsx
Normal file
45
mobile/traque-app/hook/useImageApi.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 };
|
||||
};
|
||||
@@ -1,34 +1,43 @@
|
||||
// React
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(initialValue);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
let isMounted = true;
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const item = await AsyncStorage.getItem(key);
|
||||
setStoredValue(item ? JSON.parse(item) : initialValue);
|
||||
if (isMounted && item !== null) {
|
||||
setStoredValue(JSON.parse(item));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setLoading(false);
|
||||
console.error(`Error loading key "${key}":`, error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [initialValue, key]);
|
||||
return () => { isMounted = false; };
|
||||
}, [key]);
|
||||
|
||||
const setValue = async value => {
|
||||
const setValue = useCallback(async (value) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
setStoredValue((prevValue) => {
|
||||
const valueToStore = value instanceof Function ? value(prevValue) : value;
|
||||
|
||||
return [storedValue, setValue, loading];
|
||||
AsyncStorage.setItem(key, JSON.stringify(valueToStore)).catch(err =>
|
||||
console.error(`Error saving key "${key}":`, err)
|
||||
);
|
||||
|
||||
return valueToStore;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
return [storedValue, setValue];
|
||||
};
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useEffect, useState } from "react";
|
||||
// React
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { Alert } from "react-native";
|
||||
// Expo
|
||||
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
|
||||
import * as Location from 'expo-location';
|
||||
import { useSocket } from "../context/socketContext";
|
||||
// Hook
|
||||
import { useSocketCommands } from "./useSocketCommands";
|
||||
|
||||
export const useLocation = (timeInterval, distanceInterval) => {
|
||||
const { emitUpdatePosition } = useSocketCommands();
|
||||
const [location, setLocation] = useState(null); // [latitude, longitude]
|
||||
const { teamSocket } = useSocket();
|
||||
const LOCATION_TASK_NAME = "background-location-task";
|
||||
const locationUpdateParameters = {
|
||||
const locationUpdateParameters = useMemo(() => ({
|
||||
accuracy: Location.Accuracy.High,
|
||||
distanceInterval: distanceInterval, // Update every 10 meters
|
||||
timeInterval: timeInterval, // Minimum interval in ms
|
||||
@@ -19,7 +22,7 @@ export const useLocation = (timeInterval, distanceInterval) => {
|
||||
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) {
|
||||
@@ -37,19 +40,14 @@ export const useLocation = (timeInterval, distanceInterval) => {
|
||||
} catch (e) {
|
||||
console.warn("setLocation failed (probably in background):", e);
|
||||
}
|
||||
console.log("Sending position :", new_location);
|
||||
teamSocket.emit("update_position", new_location);
|
||||
emitUpdatePosition(new_location);
|
||||
} else {
|
||||
console.log("No location measured.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getLocationAuthorization();
|
||||
}, []);
|
||||
|
||||
async function getLocationAuthorization() {
|
||||
const getLocationAuthorization = useCallback(async () => {
|
||||
const { status : statusForeground } = await Location.requestForegroundPermissionsAsync();
|
||||
const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync();
|
||||
if (statusForeground !== "granted" || statusBackground !== "granted") {
|
||||
@@ -58,23 +56,27 @@ export const useLocation = (timeInterval, distanceInterval) => {
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function startLocationTracking() {
|
||||
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]);
|
||||
|
||||
async function stopLocationTracking() {
|
||||
const stopLocationTracking = useCallback(async () => {
|
||||
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
|
||||
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
|
||||
console.log("Location tracking stopped.");
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getLocationAuthorization();
|
||||
}, [getLocationAuthorization]);
|
||||
|
||||
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useState, } from 'react';
|
||||
// React
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
// Expo
|
||||
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
|
||||
|
||||
export const usePickImage = () => {
|
||||
const [image, setImage] = useState(null);
|
||||
|
||||
const pickImage = async () => {
|
||||
const pickImage = useCallback(async () => {
|
||||
try {
|
||||
const permissionResult = await requestMediaLibraryPermissionsAsync();
|
||||
|
||||
@@ -13,7 +15,6 @@ export const usePickImage = () => {
|
||||
Alert.alert("Permission refusée", "Activez l'accès au stockage ou à la gallerie dans les paramètres.");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = await launchImageLibraryAsync({
|
||||
mediaTypes: ['images'],
|
||||
allowsMultipleSelection: false,
|
||||
@@ -28,31 +29,11 @@ export const usePickImage = () => {
|
||||
else {
|
||||
console.log('Image picker cancelled.');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
console.error('Error picking image;', error);
|
||||
Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image.");
|
||||
}
|
||||
};
|
||||
|
||||
function sendImage(location) {
|
||||
if (image) {
|
||||
let data = new FormData();
|
||||
data.append('file', {
|
||||
uri: image.uri,
|
||||
name: 'photo.jpg',
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetch(location , {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {image, pickImage, sendImage};
|
||||
}, []);
|
||||
|
||||
return {image, pickImage};
|
||||
};
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
import { useEffect } from 'react';
|
||||
// React
|
||||
import { useEffect, useRef } from 'react';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import { useSocket } from "../context/socketContext";
|
||||
// Context
|
||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||
// Hook
|
||||
import { useSocketCommands } from "./useSocketCommands";
|
||||
|
||||
export const useSendDeviceInfo = () => {
|
||||
const batteryUpdateTimeout = 5*60*1000;
|
||||
const { teamSocket } = useSocket();
|
||||
const { emitBattery, emitDeviceInfo } = useSocketCommands();
|
||||
const { loggedIn } = useTeamConnexion();
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true;
|
||||
|
||||
if (!loggedIn) return;
|
||||
|
||||
const sendInfo = async () => {
|
||||
const brand = DeviceInfo.getBrand();
|
||||
const model = DeviceInfo.getModel();
|
||||
const name = await DeviceInfo.getDeviceName();
|
||||
teamSocket.emit('device_info', {model: brand + " " + model, name: name});
|
||||
const [brand, model, name] = await Promise.all([
|
||||
DeviceInfo.getBrand(),
|
||||
DeviceInfo.getModel(),
|
||||
DeviceInfo.getDeviceName()
|
||||
]);
|
||||
if (!isMounted) return;
|
||||
emitDeviceInfo({model: brand + " " + model, name: name});
|
||||
};
|
||||
|
||||
const sendBattery = async () => {
|
||||
const level = await DeviceInfo.getBatteryLevel();
|
||||
teamSocket.emit('battery_update', Math.round(level * 100));
|
||||
if (!isMounted) return;
|
||||
emitBattery(Math.round(level * 100));
|
||||
};
|
||||
|
||||
sendInfo();
|
||||
sendBattery();
|
||||
|
||||
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout);
|
||||
const batteryCheckInterval = setInterval(() => sendBattery(), 5*60*1000); // 5 minutes
|
||||
|
||||
return () => clearInterval(batteryCheckInterval);
|
||||
}, [batteryUpdateTimeout, loggedIn, teamSocket]);
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
clearInterval(batteryCheckInterval);
|
||||
};
|
||||
}, [emitBattery, emitDeviceInfo, loggedIn]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,72 +1,60 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
// React
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
// Hook
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
import { useSocketCommands } from "./useSocketCommands";
|
||||
|
||||
const LOGIN_MESSAGE = "login";
|
||||
const LOGOUT_MESSAGE = "logout";
|
||||
|
||||
export const useSocketAuth = (socket, passwordName) => {
|
||||
export const useSocketAuth = () => {
|
||||
const { emitLogin, emitLogout } = useSocketCommands();
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [waitingForResponse, setWaitingForResponse] = useState(false);
|
||||
const [hasTriedSavedPassword, setHasTriedSavedPassword] = useState(false);
|
||||
const [savedPassword, setSavedPassword, savedPasswordLoading] = useLocalStorage(passwordName, null);
|
||||
const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null);
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !hasTriedSavedPassword) {
|
||||
console.log("Try to log in with saved password :", savedPassword);
|
||||
setWaitingForResponse(true);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn("Server did not respond to login emit.");
|
||||
setWaitingForResponse(false);
|
||||
}, 3000);
|
||||
|
||||
socket.emit(LOGIN_MESSAGE, savedPassword, (response) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(response.message);
|
||||
setLoggedIn(response.isLoggedIn);
|
||||
setWaitingForResponse(false);
|
||||
});
|
||||
setHasTriedSavedPassword(true);
|
||||
}
|
||||
}, [hasTriedSavedPassword, loading, savedPassword, socket]);
|
||||
|
||||
function login(password) {
|
||||
console.log("Try to log in with :", password);
|
||||
setSavedPassword(password);
|
||||
setWaitingForResponse(true);
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = useCallback((password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (!isMounted.current) return;
|
||||
console.warn("Server did not respond to login emit.");
|
||||
setWaitingForResponse(false);
|
||||
reject();
|
||||
}, 3000);
|
||||
}, 2000);
|
||||
|
||||
socket.emit(LOGIN_MESSAGE, password, (response) => {
|
||||
emitLogin(password, (response) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(response.message);
|
||||
setLoggedIn(response.isLoggedIn);
|
||||
setWaitingForResponse(false);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
console.log("Logout");
|
||||
setSavedPassword(null);
|
||||
if (!isMounted.current) return;
|
||||
|
||||
if (response.isLoggedIn) {
|
||||
console.log("Log in accepted.");
|
||||
setLoggedIn(true);
|
||||
setSavedPassword(password);
|
||||
resolve(response);
|
||||
} else {
|
||||
console.log("Log in refused.");
|
||||
setLoggedIn(false);
|
||||
socket.emit(LOGOUT_MESSAGE);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [emitLogin, setSavedPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
if(!waitingForResponse && !savedPasswordLoading) {
|
||||
setLoading(false);
|
||||
} else {
|
||||
setLoading(true);
|
||||
if (!loggedIn && savedPassword) {
|
||||
login(savedPassword);
|
||||
}
|
||||
}, [waitingForResponse, savedPasswordLoading]);
|
||||
}, [loggedIn, login, savedPassword]);
|
||||
|
||||
return {login, logout, password: savedPassword, loggedIn, loading};
|
||||
const logout = useCallback(() => {
|
||||
setLoggedIn(false);
|
||||
setSavedPassword(null);
|
||||
emitLogout();
|
||||
}, [emitLogout, setSavedPassword]);
|
||||
|
||||
return {login, logout, password: savedPassword, loggedIn};
|
||||
};
|
||||
|
||||
52
mobile/traque-app/hook/useSocketCommands.jsx
Normal file
52
mobile/traque-app/hook/useSocketCommands.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
// 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 };
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
// React
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const useSocketListener = (socket, event, callback) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// React
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useTimeDifference = (refTime, timeout) => {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
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,
|
||||
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
getNewTeamId() {
|
||||
let id = randint(1_000_000);
|
||||
while (this.teams.find(t => t.id === id)) id = randint(1_000_000);
|
||||
return id;
|
||||
return id.toString().padStart(6, '0');
|
||||
},
|
||||
|
||||
checkEndGame() {
|
||||
|
||||
@@ -31,7 +31,7 @@ const upload = multer({
|
||||
fileFilter: function (req, file, callback) {
|
||||
if (ALLOWED_MIME.indexOf(file.mimetype) == -1) {
|
||||
callback(null, false);
|
||||
} else if (!game.getTeam(Number(req.query.team))) {
|
||||
} else if (!game.getTeam(req.query.team)) {
|
||||
callback(null, false);
|
||||
} else {
|
||||
callback(null, true);
|
||||
@@ -58,9 +58,9 @@ export function initPhotoUpload() {
|
||||
})
|
||||
//App handler for serving the photo of a team given its secret ID
|
||||
app.get("/photo/my", (req, res) => {
|
||||
let team = game.getTeam(Number(req.query.team));
|
||||
let team = game.getTeam(req.query.team);
|
||||
if (team) {
|
||||
const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.id.toString());
|
||||
const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.id);
|
||||
res.set("Content-Type", "image/png")
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg"));
|
||||
@@ -70,9 +70,9 @@ export function initPhotoUpload() {
|
||||
})
|
||||
//App handler for serving the photo of the team chased by the team given by its secret ID
|
||||
app.get("/photo/enemy", (req, res) => {
|
||||
let team = game.getTeam(Number(req.query.team));
|
||||
let team = game.getTeam(req.query.team);
|
||||
if (team) {
|
||||
const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.chasing.toString());
|
||||
const imagePath = path.join(process.cwd(), UPLOAD_DIR, team.chasing);
|
||||
res.set("Content-Type", "image/png")
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.sendFile(fs.existsSync(imagePath) ? imagePath : path.join(process.cwd(), "images", "missing_image.jpg"));
|
||||
|
||||
Reference in New Issue
Block a user