Ajout traque-app
5
.gitignore
vendored
@@ -130,3 +130,8 @@ dist
|
||||
|
||||
# Other
|
||||
.vscode/
|
||||
.expo/
|
||||
android/
|
||||
ios/
|
||||
yarn.lock
|
||||
keys/
|
||||
|
||||
46
doc/TODO.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## TODO
|
||||
|
||||
### Général
|
||||
|
||||
- [x] Tester avec 2+ équipes : vérifier que l'on voit la position des autres équipes et qu'on reçoit la leur quand on envoi notre position.
|
||||
|
||||
### Team (Application)
|
||||
|
||||
- [x] Rendre plus claire le code de capture dans l'interface utilisateur (bien préciser que c'est l'id de capture).
|
||||
- [x] Ajouter timer du rétrécissement des zones.
|
||||
- [x] Afficher dernière position envoyée par la team.
|
||||
- [x] Rendre la position de l'ennemi visible dès le départ.
|
||||
- [x] Préciser que l'équipe doit fournir une photo d'eux où l'on voit leur tête et au moins leur buste.
|
||||
- [x] Utiliser les messages de victoire/défaite/etc définis par le serveur.
|
||||
- [x] Centrer la map sur la position à l'ouverture + bouton centrage
|
||||
- [x] Indiquer que l'équipe est hors zone.
|
||||
- [x] Mettre les stats dans le tiroir (distance, temps, vitesse moy, nb captures, nb envoi)
|
||||
- [ ] Implémenter des notifs lors du background (hors zone, position envoyée, update zone)
|
||||
- [ ] Ajouter les logs de la partie
|
||||
- [ ] Créer le menu paramètre (idées de section : langue, photo équipe, notifs, mode sombre, unitées)
|
||||
- [ ] Afficher la trajectoire passée sur la carte (désactivable)
|
||||
- [ ] Afficher les évènements passés sur la carte (captures, envois, départ) (désactivable)
|
||||
- [ ] Permettre le changement du style de la carte (schéma, satellite, relief etc)
|
||||
- [ ] Ajouter imprécision de la position au besoin (comme sur google maps)
|
||||
- [ ] Synchroniser les horloges sur l'interface
|
||||
|
||||
### Admin (Pageweb)
|
||||
|
||||
- [x] Clarifier qui est qui sur l'interface.
|
||||
- [x] Clarifier qui chasse qui sur l'interface.
|
||||
- [x] Ajouter timer du rétrécissement des zones.
|
||||
- [x] Pouvoir changer les paramètres du jeu pendant une partie.
|
||||
- [ ] Implémenter les wireframes
|
||||
|
||||
### Améliorations du jeu de la traque
|
||||
|
||||
- [x] Supprimer la pénalité de non envoi de position : envoyer la position automatiquement à la fin du timer.
|
||||
- [ ] Supprimer la pénalité d'hors zone : révéler la position de la team hors zone au bout d'un certain temps.
|
||||
- [x] Changer le système de zone de jeu pour qu'il soit fait d'un pavage de zones qui se ferment successivement.
|
||||
|
||||
### Autres idées
|
||||
|
||||
- Améliorer l'accessibilité du site et de l'appli (traduction anglaise notamment).
|
||||
- Nettoyer le code, le commenter, créer des tests, le rendre maintenable après la fin du projet.
|
||||
- Améliorer l'UI admin.
|
||||
- Améliorer l'UI team.
|
||||
146
doc/wireframes/app/v1/page_fin.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
293
doc/wireframes/app/v1/page_jeu.svg
Normal file
|
After Width: | Height: | Size: 877 KiB |
134
doc/wireframes/app/v1/page_login.svg
Normal file
|
After Width: | Height: | Size: 517 KiB |
202
doc/wireframes/app/v1/page_placement.svg
Normal file
|
After Width: | Height: | Size: 360 KiB |
411
doc/wireframes/app/v2/page_jeu_info.svg
Normal file
|
After Width: | Height: | Size: 653 KiB |
284
doc/wireframes/app/v2/page_jeu_map.svg
Normal file
|
After Width: | Height: | Size: 245 KiB |
58
doc/wireframes/implementation_summary.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Artishow : La Traque
|
||||
|
||||
## Central : Site web
|
||||
### Création de la partie
|
||||
**Définition des temps pour les envois de position et des pénalitées en cas**
|
||||
**de non envoi**
|
||||
**Définition des zones de jeux et évolution au cours de la partie**
|
||||
**Définition des messages à envoyer au joueur (victoire, défaite, capture,**
|
||||
**attente...)**
|
||||
|
||||
### Suivi de la partie
|
||||
#### Interface
|
||||
##### Etat de la partie
|
||||
| **Teams encore en jeu**
|
||||
| Map avec la position des teams
|
||||
| Stats des teams (nombre de capture, pénalités, vitesse moyenne, batterie...)
|
||||
Edition des paramètres de création de la partie
|
||||
Chat avec les équipes
|
||||
Dossier de médias
|
||||
|
||||
#### Background
|
||||
**Réception et envoi des positions des teams**
|
||||
Réception des chats
|
||||
Enregistrement de l’évolution de la partie
|
||||
|
||||
## Team : Appli mobile (Android, peut être IOS)
|
||||
|
||||
### Avant la partie
|
||||
**Enregistrement du nom de la team**
|
||||
**Envoie de la photo de la team**
|
||||
### Durant la partie
|
||||
|
||||
#### Interface
|
||||
**Envoi de la position**
|
||||
**Nom et photo de la team à traquer**
|
||||
**Capture d’une team**
|
||||
**Capture par une team**
|
||||
|
||||
##### Carte
|
||||
| **Dernière position envoyée**
|
||||
| **Dernière position de la team traquée**
|
||||
| **Evolution des zones**
|
||||
**Historique du déroulé de la partie (Début/fin de la partie, pénalités,**
|
||||
**captures...)**
|
||||
Chat avec le central
|
||||
Envoi de médias
|
||||
|
||||
#### Background
|
||||
**Envoi régulière de la position**
|
||||
Réception des chats
|
||||
|
||||
### Gestion de la partie
|
||||
**Quand une team envoie sa position, elle reçoit la position de la team qu’elle traque**
|
||||
**Mise à jour de la team à traquer après une capture**
|
||||
**Réception de pénalités par les teams qui ne sont pas dans la zone ou qui n’envoient pas leur position à temps**
|
||||
**Rétrécissement des zones**
|
||||
Envoie de chats entre le central et les teams
|
||||
Envoi de médias des teams vers le central
|
||||
974
doc/wireframes/siteweb/main.svg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
662
doc/wireframes/siteweb/parameters_placement.svg
Normal file
|
After Width: | Height: | Size: 320 KiB |
655
doc/wireframes/siteweb/parameters_zone.svg
Normal file
|
After Width: | Height: | Size: 320 KiB |
102
traque-app/apk_android.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# How to create an apk
|
||||
|
||||
An apk is an app file that can be installed on an android device without the need of google play store. This tutorial will explain the steps to create the apk of the current project and download it on an android device. Each step has precisions, however some of those precisions may not be suited for your device, don't hesitate to find help on the Internet or ask ChapGPT.
|
||||
|
||||
|
||||
|
||||
## Set up your environnement
|
||||
|
||||
This section will cover the set up of your environnement in order to have the tools to either send the apk of your app on your device or create a virtual android device.
|
||||
|
||||
### Tutorial
|
||||
|
||||
Follow this tutorial : https://reactnative.dev/docs/set-up-your-environment?os=linux&platform=android
|
||||
|
||||
### Precisions
|
||||
|
||||
* Android Studio and its dependancies can take up to 15 Go of space.
|
||||
* The `.bashrc` file is located in your home directory (`cd ~`).
|
||||
* The Watchman installation isn't necessary.
|
||||
* If you want to test your app on a physical android device, follow the next section.
|
||||
|
||||
|
||||
|
||||
## Create the android folder
|
||||
|
||||
This section will cover the creation of the android folder if it isn't created yet.
|
||||
|
||||
### Tutorial
|
||||
|
||||
* Go in your project folder (`traque-app`).
|
||||
* Run `npx expo install expo-dev-client`. This install the `expo-dev-client` package if it isn't already.
|
||||
* Run `npx expo prebuild --platform android`. This will add an android folder in your project taking about 1 Go of space.
|
||||
|
||||
|
||||
|
||||
## Create and add the app key
|
||||
|
||||
This section will cover the creation of the app key which is required by google play store. You will need the `.keystore` file associated to the app. If no one in the team have created it yet follow *Tutorial Part A* then *Tutorial Part B*, else get the `.keystore` file, the storePassword, the keyAlias and the keyPassword, and follow *Tutorial Part B*.
|
||||
|
||||
### Tutorial Part A
|
||||
|
||||
* Go in the folder `traque-app/android/app/`.
|
||||
* Run `keytool -genkey -v -keystore release.keystore -alias traque_key -keyalg RSA -keysize 2048 -validity 10000`, note that `release` and `traque_key` can be modified if you want.
|
||||
|
||||
### Tutorial Part B
|
||||
|
||||
* Go in the folder `traque-app/android/app/`.
|
||||
* Here paste the `.keystore` file associated with your app.
|
||||
* Modify the android bracket of the `traque-app/android/app/build.gradle` file as follows :
|
||||
|
||||
```
|
||||
android {
|
||||
...
|
||||
signingConfigs {
|
||||
...
|
||||
release {
|
||||
storeFile file('TO_BE_FILLED.keystore')
|
||||
storePassword 'TO_BE_FILLED'
|
||||
keyAlias 'TO_BE_FILLED'
|
||||
keyPassword 'TO_BE_FILLED'
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
...
|
||||
release {
|
||||
...
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Create the apk
|
||||
|
||||
This section will cover the creation of the apk of the app that can be download and installed on any android device.
|
||||
|
||||
### Tutorial
|
||||
|
||||
* Go in the folder `traque-app/android/`.
|
||||
* Run `./gradlew assembleRelease`.
|
||||
* At the end, the created apk will be located in the `traque-app/android/app/build/outputs/apk/release/` folder.
|
||||
|
||||
### Precisions
|
||||
|
||||
While running `./gradlew assembleRelease`, you can have this error :
|
||||
```
|
||||
Task :expo-modules-core:compileDebugKotlin FAILED
|
||||
e: This version (1.5.15) of the Compose Compiler requires Kotlin version 1.9.25 but you appear to be using Kotlin version 1.9.24 which is not known to be compatible.
|
||||
```
|
||||
Follow these steps to fix it :
|
||||
|
||||
* In the `android/build.gradle` file, replace the line `classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')` by `classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25')`.
|
||||
* In the `traque-app/android/` folder, run `./gradlew assembleRelease` again.
|
||||
|
||||
### Other precisions
|
||||
|
||||
If the build fail for obscure reasons, it may be usefull to delete entirely the `node_modules/` folder and recreate it by running `npm i`.
|
||||
If problems persist, you may also delete entirely the `android/` folder and recreate it.
|
||||
If the problem persist, you may also delete the `~/.gradle/caches/` folder.
|
||||
If the problem persist, stop working on the project, you can't go against god's will.
|
||||
61
traque-app/app.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"expo": {
|
||||
"scheme": "la-traque",
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"usesCleartextTraffic": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"expo-font"
|
||||
],
|
||||
"newArchEnabled": true,
|
||||
"name": "La Traque",
|
||||
"slug": "la-traque",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/logo/logo_traque.png",
|
||||
"splash": {
|
||||
"image": "./assets/images/logo/logo_traque.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"platforms": [
|
||||
"ios",
|
||||
"android"
|
||||
],
|
||||
"android": {
|
||||
"package": "com.anonymous.traqueapp",
|
||||
"permissions": [
|
||||
"ACCESS_FINE_LOCATION",
|
||||
"ACCESS_COARSE_LOCATION",
|
||||
"ACCESS_BACKGROUND_LOCATION",
|
||||
"FOREGROUND_SERVICE",
|
||||
"FOREGROUND_SERVICE_LOCATION",
|
||||
"READ_EXTERNAL_STORAGE",
|
||||
"WRITE_EXTERNAL_STORAGE",
|
||||
"INTERNET",
|
||||
"POST_NOTIFICATIONS"
|
||||
],
|
||||
"config": {
|
||||
"googleMaps": {
|
||||
"apiKey": "AIzaSyD0yuWIHFbsIDVfGQ9wEM3pOtVC2TgEO1U"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"bundleIdentifier": "com.anonymous.traqueapp",
|
||||
"infoPlist": {
|
||||
"UIBackgroundModes": [
|
||||
"location"
|
||||
],
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription": "Your location is used to track you in the background.",
|
||||
"NSLocationWhenInUseUsageDescription": "Location is used to track your movement."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
traque-app/app/+not-found.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Unmatched } from 'expo-router';
|
||||
|
||||
export default Unmatched;
|
||||
16
traque-app/app/_layout.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Slot } from 'expo-router';
|
||||
import SocketProvider from "../context/socketContext";
|
||||
import { TeamConnexionProvider } from "../context/teamConnexionContext";
|
||||
import { TeamProvider } from "../context/teamContext";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<SocketProvider>
|
||||
<TeamConnexionProvider>
|
||||
<TeamProvider>
|
||||
<Slot/>
|
||||
</TeamProvider>
|
||||
</TeamConnexionProvider>
|
||||
</SocketProvider>
|
||||
);
|
||||
}
|
||||
598
traque-app/app/display.js
Normal file
@@ -0,0 +1,598 @@
|
||||
// React
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { ScrollView, View, Text, Image, Alert, StyleSheet, TouchableOpacity, TouchableHighlight } from 'react-native';
|
||||
import MapView, { Marker, Circle, Polygon } from 'react-native-maps';
|
||||
// Expo
|
||||
import { useRouter } from 'expo-router';
|
||||
// Components
|
||||
import CustomImage from '../components/image';
|
||||
import CustomTextInput from '../components/input';
|
||||
import Stat from '../components/stat';
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
// Other
|
||||
import { useSocket } from '../context/socketContext';
|
||||
import { useTeamContext } from '../context/teamContext';
|
||||
import { useTeamConnexion } from '../context/teamConnexionContext';
|
||||
import { useDeadline, useTimeDifference } from '../hook/useTimeDifference';
|
||||
import { GameState } from '../util/gameState';
|
||||
import useGame from '../hook/useGame';
|
||||
|
||||
const backgroundColor = '#f5f5f5';
|
||||
const initialRegion = {latitude: 48.864, longitude: 2.342, latitudeDelta: 0, longitudeDelta: 50} // France centrée sur Paris
|
||||
|
||||
export default function Display() {
|
||||
const arrowUp = require('../assets/images/arrow.png');
|
||||
const [collapsibleState, setCollapsibleState] = useState(true);
|
||||
const [bottomContainerHeight, setBottomContainerHeight] = useState(0);
|
||||
const router = useRouter();
|
||||
const {SERVER_URL} = useSocket();
|
||||
const {gameSettings, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, zone} = useTeamContext();
|
||||
const {loggedIn, logout, loading} = useTeamConnexion();
|
||||
const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, penalties, teamId, outOfZone, outOfZoneDeadline, distance, startDate, finishDate, nCaptures, nSentLocation} = useGame();
|
||||
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
|
||||
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
|
||||
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
|
||||
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
|
||||
const [timeSinceStart] = useTimeDifference(startDate, 1000);
|
||||
const [avgSpeed, setAvgSpeed] = useState(0); // Speed in m/s
|
||||
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 mapRef = useRef(null);
|
||||
const [centerMap, setCenterMap] = useState(true);
|
||||
|
||||
// Router
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!loggedIn) {
|
||||
router.replace("/");
|
||||
}
|
||||
}
|
||||
}, [loggedIn, loading]);
|
||||
|
||||
// Activating geolocation tracking
|
||||
useEffect(() => {
|
||||
if (loggedIn && (gameState == GameState.SETUP || gameState == GameState.PLAYING || gameState == GameState.PLACEMENT) && !captured) {
|
||||
startLocationTracking();
|
||||
} else {
|
||||
stopLocationTracking();
|
||||
}
|
||||
}, [loggedIn, gameState, captured]);
|
||||
|
||||
// Refresh the image
|
||||
useEffect(() => {
|
||||
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
|
||||
}, [enemyName, teamId]);
|
||||
|
||||
// Capture state update
|
||||
useEffect(() => {
|
||||
if (captureStatus == 2 || captureStatus == 3) {
|
||||
const timeout = setTimeout(() => {
|
||||
setCaptureStatus(0);
|
||||
}, 3000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [captureStatus]);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}, [centerMap, mapRef, location]);
|
||||
|
||||
// Update the average speed
|
||||
useEffect(() => {
|
||||
const time = finishDate ? (finishDate - startDate) : timeSinceStart;
|
||||
setAvgSpeed(distance/time);
|
||||
}, [distance, finishDate, timeSinceStart]);
|
||||
|
||||
function toggleCollapsible() {
|
||||
setCollapsibleState(!collapsibleState);
|
||||
};
|
||||
|
||||
function handleCapture() {
|
||||
if (captureStatus != 1) {
|
||||
setCaptureStatus(1);
|
||||
capture(enemyCaptureCode)
|
||||
.then((response) => {
|
||||
if (response.hasCaptured) {
|
||||
setCaptureStatus(3);
|
||||
} else {
|
||||
setCaptureStatus(2);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Alert.alert("Échec", "La connexion au serveur a échoué.");
|
||||
setCaptureStatus(2);
|
||||
});
|
||||
setEnemyCaptureCode("");
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeMinutes(time) {
|
||||
// time is in seconds
|
||||
if (!Number.isInteger(time)) return "Inconnue";
|
||||
if (time < 0) time = 0;
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
||||
}
|
||||
|
||||
function formatTimeHours(time) {
|
||||
// time is in seconds
|
||||
if (!Number.isInteger(time)) return "Inconnue";
|
||||
if (time < 0) time = 0;
|
||||
const hours = Math.floor(time / 3600);
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
return String(hours).padStart(2,"0") + ":" + String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
||||
}
|
||||
|
||||
function circle(color, circle) {
|
||||
return (
|
||||
<Circle
|
||||
center={{ latitude: circle.center.lat, longitude: circle.center.lng }}
|
||||
radius={circle.radius}
|
||||
strokeWidth={2}
|
||||
strokeColor={`rgba(${color}, 1)`}
|
||||
fillColor={`rgba(${color}, 0.2)`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Logout = () => {
|
||||
return (
|
||||
<TouchableOpacity style={{width: 40, height: 40}} onPress={logout}>
|
||||
<Image source={require('../assets/images/logout.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<TouchableOpacity style={{width: 40, height: 40}} onPress={() => Alert.alert("Settings")}>
|
||||
<Image source={require('../assets/images/cogwheel.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const TeamName = () => {
|
||||
return(
|
||||
<Text style={{fontSize: 36, fontWeight: "bold", textAlign: "center"}}>{(name ?? "Indisponible")}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const GameLog = () => {
|
||||
return (
|
||||
<TouchableOpacity style={{width:"100%"}}>
|
||||
{ gameState == GameState.SETUP && <Text style={styles.gameState}>Préparation de la partie</Text>}
|
||||
{ gameState == GameState.PLACEMENT && <Text style={styles.gameState}>Phase de placement</Text>}
|
||||
{ gameState == GameState.PLAYING && !outOfZone && <Text style={styles.gameState}>La partie est en cours</Text>}
|
||||
{ gameState == GameState.PLAYING && outOfZone && <Text style={styles.gameStateOutOfZone}>Hors zone (pénalité dans {formatTimeMinutes(-timeLeftOutOfZone)})</Text>}
|
||||
{ gameState == GameState.FINISHED && <Text style={styles.gameState}>La partie est terminée</Text>}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const TimeBeforeNextZone = () => {
|
||||
return (
|
||||
<View style={{width: "100%", alignItems: "center", justifyContent: "center"}}>
|
||||
<Text style={{fontSize: 15}}>{isShrinking ? "Réduction de la zone" : "Durée de la zone"}</Text>
|
||||
<Text style={{fontSize: 30, fontWeight: "bold"}}>{formatTimeMinutes(-timeLeftNextZone)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const TimeBeforeNextPosition = () => {
|
||||
return (
|
||||
<View style={{width: "100%", alignItems: "center", justifyContent: "center"}}>
|
||||
<Text style={{fontSize: 15}}>Position envoyée dans</Text>
|
||||
<Text style={{fontSize: 30, fontWeight: "bold"}}>{formatTimeMinutes(-timeLeftSendLocation)}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Timers = () => {
|
||||
return (
|
||||
<View style={styles.timersContainer}>
|
||||
<View style={styles.zoneTimerContainer}>
|
||||
{ TimeBeforeNextZone() }
|
||||
</View>
|
||||
<View style={styles.positionTimerContainer}>
|
||||
{ TimeBeforeNextPosition() }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Ready = () => {
|
||||
return (
|
||||
<View style={styles.timersContainer}>
|
||||
<View style={[styles.readyIndicator, {backgroundColor: ready ? "#3C3" : "#C33"}]}>
|
||||
<Text style={{color: '#fff', fontSize: 16}}>{ready ? "Placé" : "Non placé"}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const CapturedMessage = () => {
|
||||
return (
|
||||
<View style={[styles.timersContainer, {height: 61}]}>
|
||||
<Text style={{fontSize: 20}}>{gameSettings?.capturedMessage || "Vous avez été éliminé..."}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const EndGameMessage = () => {
|
||||
return (
|
||||
<View style={[styles.timersContainer, {height: 61}]}>
|
||||
{captured && <Text style={{fontSize: 20}}>{gameSettings?.loserEndGameMessage || "Vous avez perdu..."}</Text>}
|
||||
{!captured && <Text style={{fontSize: 20}}>{gameSettings?.winnerEndGameMessage || "Vous avez gagné !"}</Text>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Map = () => {
|
||||
return (
|
||||
<MapView ref={mapRef} style={{flex: 1}} initialRegion={initialRegion} mapType="standard" onTouchMove={() => setCenterMap(false)}>
|
||||
{ gameState == GameState.PLACEMENT && startingArea && circle("0, 0, 255", startingArea)}
|
||||
{ gameState == GameState.PLAYING && zoneExtremities && <Polygon coordinates={zoneExtremities.begin.points} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
|
||||
{ gameState == GameState.PLAYING && zoneExtremities && <Polygon coordinates={zoneExtremities.end.points} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
|
||||
{ location &&
|
||||
<Marker coordinate={{ latitude: location[0], longitude: location[1] }} anchor={{ x: 0.33, y: 0.33 }}>
|
||||
<Image source={require("../assets/images/marker/blue.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
|
||||
</Marker>
|
||||
}
|
||||
{ gameState == GameState.PLAYING && lastSentLocation &&
|
||||
<Marker coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}>
|
||||
<Image source={require("../assets/images/marker/grey.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
|
||||
</Marker>
|
||||
}
|
||||
{ gameState == GameState.PLAYING && enemyLocation &&
|
||||
<Marker coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }}>
|
||||
<Image source={require("../assets/images/marker/red.png")} style={{width: 24, height: 24}} resizeMode="contain"/>
|
||||
</Marker>
|
||||
}
|
||||
</MapView>
|
||||
);
|
||||
}
|
||||
|
||||
const UpdatePositionButton = () => {
|
||||
return (
|
||||
<TouchableOpacity style={styles.updatePositionContainer} onPress={sendCurrentPosition}>
|
||||
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const CenterMapButton = () => {
|
||||
return (
|
||||
<TouchableOpacity style={styles.centerMapContainer} onPress={() => setCenterMap(true)}>
|
||||
<Image source={require("../assets/images/centerMap.png")} style={{width: 30, height: 30}} resizeMode="contain"></Image>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const LayerButton = () => {
|
||||
return(
|
||||
<TouchableOpacity style={styles.layerContainer} onPress={() => Alert.alert("Layer")}>
|
||||
<Image source={require('../assets/images/path.png')} style={{width: 40, height: 40}} resizeMode="contain"></Image>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const CollapsibleButton = () => {
|
||||
return (
|
||||
<TouchableHighlight onPress={toggleCollapsible} style={styles.collapsibleButton} underlayColor="#d9d9d9">
|
||||
<Image source={arrowUp} style={{width: 20, height: 20, transform: [{ scaleY: collapsibleState ? 1 : -1 }] }} resizeMode="contain"></Image>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
const TeamCaptureCode = () => {
|
||||
return (
|
||||
<Text style={{fontSize: 22, fontWeight: "bold", textAlign: "center"}}>Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const ChasedTeamImage = () => {
|
||||
return (
|
||||
<View style={styles.imageContainer}>
|
||||
<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>
|
||||
<CustomImage source={{ uri : enemyImageURI }} canZoom/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const CaptureCode = () => {
|
||||
return (
|
||||
<View style={styles.actionsLeftContainer}>
|
||||
<CustomTextInput style={{borderColor: captureStatusColor[captureStatus]}} value={enemyCaptureCode} inputMode="numeric" placeholder="Code cible" onChangeText={setEnemyCaptureCode}/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const CaptureButton = () => {
|
||||
return (
|
||||
<View style={styles.actionsRightContainer}>
|
||||
<TouchableOpacity style={styles.button} onPress={handleCapture}>
|
||||
<Image source={require("../assets/images/target/white.png")} style={{width: 40, height: 40}} resizeMode="contain"/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const Stats = () => {
|
||||
return (
|
||||
<View style={{gap: 15, width: "100%", marginVertical: 15}}>
|
||||
<View style={{flexDirection: "row", justifyContent: "space-around"}}>
|
||||
<Stat source={require('../assets/images/distance.png')} description={"Distance parcourue"}>{(distance / 1000).toFixed(1)}km</Stat>
|
||||
<Stat source={require('../assets/images/time.png')} description={"Temps écoulé au format HH:MM:SS"}>{formatTimeHours((finishDate ? Math.floor((finishDate - startDate) / 1000) : timeSinceStart))}</Stat>
|
||||
<Stat source={require('../assets/images/running.png')} description={"Vitesse moyenne"}>{(avgSpeed*3.6).toFixed(1)}km/h</Stat>
|
||||
</View>
|
||||
<View style={{flexDirection: "row", justifyContent: "space-around"}}>
|
||||
<Stat source={require('../assets/images/target/black.png')} description={"Nombre total de captures par votre équipe"}>{nCaptures}</Stat>
|
||||
<Stat source={require('../assets/images/update_position.png')} description={"Nombre total d'envois de votre position"}>{nSentLocation}</Stat>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.globalContainer}>
|
||||
<View style={styles.topContainer}>
|
||||
<View style={styles.topheadContainer}>
|
||||
{ Logout() }
|
||||
{ penalties > 0 && gameState == GameState.PLAYING &&
|
||||
<Text style={{marginTop: 15, fontSize: 15}}>Pénalités : {penalties}</Text>
|
||||
}
|
||||
{ false && Settings() }
|
||||
</View>
|
||||
<View style={styles.teamNameContainer}>
|
||||
{ TeamName() }
|
||||
</View>
|
||||
<View style={styles.logContainer}>
|
||||
{ GameLog() }
|
||||
</View>
|
||||
{ gameState == GameState.PLACEMENT &&
|
||||
Ready()
|
||||
}
|
||||
{ gameState == GameState.PLAYING && !captured &&
|
||||
Timers()
|
||||
}
|
||||
{ gameState == GameState.PLAYING && captured &&
|
||||
CapturedMessage()
|
||||
}
|
||||
{ gameState == GameState.FINISHED &&
|
||||
EndGameMessage()
|
||||
}
|
||||
</View>
|
||||
<View style={styles.bottomContainer} onLayout={(event) => {setBottomContainerHeight(event.nativeEvent.layout.height)}}>
|
||||
<View style={styles.mapContainer}>
|
||||
{ Map() }
|
||||
<LinearGradient colors={['rgba(0,0,0,0.3)', 'rgba(0,0,0,0)']} style={{height: 40, width: "100%", position: "absolute"}}/>
|
||||
{ !centerMap && CenterMapButton() }
|
||||
{ false && gameState == GameState.PLAYING && !captured &&
|
||||
<View style={styles.toolBarLeft}>
|
||||
{ LayerButton() }
|
||||
</View>
|
||||
}
|
||||
{ gameState == GameState.PLAYING && !captured &&
|
||||
<View style={styles.toolBarRight}>
|
||||
{ UpdatePositionButton() }
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
{ gameState == GameState.PLAYING && !captured &&
|
||||
<View style={styles.outerDrawerContainer}>
|
||||
<LinearGradient colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.5)']} style={{height: 70, width: "100%", position: "absolute", top: -30}}/>
|
||||
<View style={styles.innerDrawerContainer}>
|
||||
{ CollapsibleButton() }
|
||||
<Collapsible style={[styles.collapsibleWindow, {height: bottomContainerHeight - 44}]} title="Collapse" collapsed={collapsibleState}>
|
||||
<ScrollView contentContainerStyle={styles.collapsibleContent}>
|
||||
{ TeamCaptureCode() }
|
||||
{ ChasedTeamImage() }
|
||||
<View style={styles.actionsContainer}>
|
||||
{ CaptureCode() }
|
||||
{ CaptureButton() }
|
||||
</View>
|
||||
{ Stats() }
|
||||
</ScrollView>
|
||||
</Collapsible>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
globalContainer: {
|
||||
backgroundColor: backgroundColor,
|
||||
flex: 1,
|
||||
},
|
||||
topContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
padding: 15,
|
||||
},
|
||||
topheadContainer: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
teamNameContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
logContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 15
|
||||
},
|
||||
gameState: {
|
||||
borderWidth: 2,
|
||||
borderRadius: 10,
|
||||
width: "100%",
|
||||
backgroundColor: 'white',
|
||||
fontSize: 20,
|
||||
padding: 10,
|
||||
},
|
||||
gameStateOutOfZone: {
|
||||
borderWidth: 2,
|
||||
borderRadius: 10,
|
||||
width: "100%",
|
||||
backgroundColor: 'white',
|
||||
fontSize: 20,
|
||||
padding: 10,
|
||||
borderColor: 'red'
|
||||
},
|
||||
timersContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
marginTop: 15
|
||||
},
|
||||
zoneTimerContainer: {
|
||||
width: "50%",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
positionTimerContainer: {
|
||||
width: "50%",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
readyIndicator: {
|
||||
width: "100%",
|
||||
maxWidth: 240,
|
||||
height: 61,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 3,
|
||||
borderRadius: 10
|
||||
},
|
||||
bottomContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center'
|
||||
},
|
||||
mapContainer: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
borderTopLeftRadius: 30,
|
||||
borderTopRightRadius: 30,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
outerDrawerContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center'
|
||||
},
|
||||
innerDrawerContainer: {
|
||||
width: "100%",
|
||||
alignItems: 'center',
|
||||
backgroundColor: backgroundColor,
|
||||
borderTopLeftRadius: 30,
|
||||
borderTopRightRadius: 30,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
collapsibleButton: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: "100%",
|
||||
height: 45
|
||||
},
|
||||
collapsibleWindow: {
|
||||
width: "100%",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
collapsibleContent: {
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center'
|
||||
},
|
||||
centerMapContainer: {
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
top: 20,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'white',
|
||||
borderWidth: 2,
|
||||
borderColor: 'black',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
toolBarLeft: {
|
||||
position: 'absolute',
|
||||
left: 30,
|
||||
bottom: 80
|
||||
},
|
||||
toolBarRight: {
|
||||
position: 'absolute',
|
||||
right: 30,
|
||||
bottom: 80
|
||||
},
|
||||
updatePositionContainer: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: 'white',
|
||||
borderWidth: 4,
|
||||
borderColor: 'black',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
layerContainer: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: 'white',
|
||||
borderWidth: 4,
|
||||
borderColor: 'black',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
imageContainer: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginTop: 15
|
||||
},
|
||||
actionsContainer: {
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 15
|
||||
},
|
||||
actionsLeftContainer: {
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 15
|
||||
},
|
||||
actionsRightContainer: {
|
||||
width: 100,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
button: {
|
||||
borderRadius: 12,
|
||||
width: '100%',
|
||||
height: 75,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#444'
|
||||
},
|
||||
});
|
||||
113
traque-app/app/index.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// React
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ScrollView, View, Text, StyleSheet, Image, Alert } from 'react-native';
|
||||
// Expo
|
||||
import { useRouter } from 'expo-router';
|
||||
// Components
|
||||
import CustomButton from '../components/button';
|
||||
import CustomImage from '../components/image';
|
||||
import CustomTextInput from '../components/input';
|
||||
// Other
|
||||
import { useSocket } from '../context/socketContext';
|
||||
import { useTeamContext } from '../context/teamContext';
|
||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||
import { usePickImage } from '../hook/usePickImage';
|
||||
|
||||
const backgroundColor = '#f5f5f5';
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
const {SERVER_URL} = useSocket();
|
||||
const {login, loggedIn, loading} = useTeamConnexion();
|
||||
const {getLocationAuthorization, stopLocationTracking} = useTeamContext();
|
||||
const {image, pickImage, sendImage} = usePickImage();
|
||||
const [teamID, setTeamID] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Disbaling location tracking
|
||||
useEffect(() => {
|
||||
stopLocationTracking();
|
||||
}, []);
|
||||
|
||||
// Routeur
|
||||
useEffect(() => {
|
||||
if (!loading && loggedIn) {
|
||||
router.replace("/display");
|
||||
}
|
||||
}, [loggedIn, loading]);
|
||||
|
||||
function handleSubmit() {
|
||||
if (!isSubmitting && !loading) {
|
||||
setIsSubmitting(true);
|
||||
if (getLocationAuthorization()) {
|
||||
login(parseInt(teamID))
|
||||
.then((response) => {
|
||||
if (response.isLoggedIn) {
|
||||
sendImage(`${SERVER_URL}/upload?team=${teamID}`);
|
||||
} else {
|
||||
Alert.alert("Échec", "L'ID d'équipe est inconnu.");
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
})
|
||||
.catch(() => {
|
||||
Alert.alert("Échec", "La connection au serveur a échoué.");
|
||||
setIsSubmitting(false);
|
||||
});
|
||||
}
|
||||
setTeamID("");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<View style={styles.transitionContainer}>
|
||||
<View style={styles.subContainer}>
|
||||
<Image style={styles.logoImage} source={require('../assets/images/logo/logo_traque.png')}/>
|
||||
<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}/>
|
||||
</View>
|
||||
<View style={styles.subContainer}>
|
||||
<Text style={{fontSize: 15}}>Appuyer pour changer la photo d'équipe</Text>
|
||||
<Text style={{fontSize: 13, marginBottom: 3}}>(Le haut du corps doit être visible)</Text>
|
||||
<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}/>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
backgroundColor: backgroundColor
|
||||
},
|
||||
transitionContainer: {
|
||||
flexGrow: 1,
|
||||
width: '80%',
|
||||
maxWidth: 600,
|
||||
alignItems: 'center',
|
||||
},
|
||||
subContainer: {
|
||||
flexGrow: 1,
|
||||
width: "100%",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: 10,
|
||||
},
|
||||
logoImage: {
|
||||
width: 130,
|
||||
height: 130,
|
||||
margin: 10,
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 50,
|
||||
fontWeight: 'bold',
|
||||
}
|
||||
});
|
||||
BIN
traque-app/assets/images/arrow.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
traque-app/assets/images/battery/black.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
traque-app/assets/images/battery/green.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
traque-app/assets/images/battery/red.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
traque-app/assets/images/centerMap.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
traque-app/assets/images/cogwheel.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
traque-app/assets/images/distance.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
traque-app/assets/images/heart.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
traque-app/assets/images/knife.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
traque-app/assets/images/localisation/black.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
traque-app/assets/images/localisation/green.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
traque-app/assets/images/localisation/red.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
traque-app/assets/images/logo/logo_traque.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
traque-app/assets/images/logout.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
traque-app/assets/images/marker/blue.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
traque-app/assets/images/marker/grey.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
traque-app/assets/images/marker/red.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
traque-app/assets/images/missing_image.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
traque-app/assets/images/observed.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
traque-app/assets/images/outOfZone.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
traque-app/assets/images/path.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
traque-app/assets/images/ping/black.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
traque-app/assets/images/ping/green.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
traque-app/assets/images/ping/red.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
traque-app/assets/images/placement.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
traque-app/assets/images/running.jpg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
traque-app/assets/images/running.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
traque-app/assets/images/target/black.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
traque-app/assets/images/target/white.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
traque-app/assets/images/time.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
2
traque-app/assets/images/trash.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-e</title><path d="M112,112l20,320c.95,18.49,14.4,32,32,32H348c17.67,0,30.87-13.51,32-32l20-320" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="80" y1="112" x2="432" y2="112" style="stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;stroke-width:32px"/><path d="M192,112V72h0a23.93,23.93,0,0,1,24-24h80a23.93,23.93,0,0,1,24,24h0v40" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="256" y1="176" x2="256" y2="400" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="184" y1="176" x2="192" y2="400" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="328" y1="176" x2="320" y2="400" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
traque-app/assets/images/update_position.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
traque-app/assets/images/user/black.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
traque-app/assets/images/user/green.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
traque-app/assets/images/user/red.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
38
traque-app/components/button.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { TouchableHighlight, View, Text, StyleSheet } from "react-native";
|
||||
|
||||
export default CustomButton = forwardRef(function CustomButton({ label, onPress }, ref) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableHighlight style={styles.button} onPress={onPress} ref={ref}>
|
||||
<Text style={styles.buttonLabel}>{label}</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
width: "100%",
|
||||
maxWidth: 240,
|
||||
height: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 3,
|
||||
borderWidth: 4,
|
||||
borderColor: '#888',
|
||||
borderRadius: 18
|
||||
},
|
||||
button: {
|
||||
borderRadius: 10,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#555'
|
||||
},
|
||||
buttonLabel: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
36
traque-app/components/image.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useState } from 'react';
|
||||
import { StyleSheet, View, Image, TouchableOpacity } from "react-native";
|
||||
import ImageViewing from 'react-native-image-viewing';
|
||||
|
||||
export default function CustomImage({ source, canZoom, onPress }) {
|
||||
// canZoom : boolean
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity onPress={canZoom ? () => setIsModalVisible(true) : onPress}>
|
||||
<Image style={styles.image} resizeMode="contain" source={source}/>
|
||||
</TouchableOpacity>
|
||||
<ImageViewing
|
||||
images={[source]}
|
||||
visible={isModalVisible}
|
||||
onRequestClose={() => setIsModalVisible(false)}
|
||||
swipeToCloseEnabled={false}
|
||||
doubleTapToZoomEnabled={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
image: {
|
||||
width: "100%",
|
||||
height: undefined,
|
||||
aspectRatio: 1.5
|
||||
}
|
||||
});
|
||||
26
traque-app/components/input.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { TextInput, StyleSheet } from 'react-native';
|
||||
|
||||
export default function CustomTextInput({ style, value, inputMode, placeholder, onChangeText }) {
|
||||
return (
|
||||
<TextInput
|
||||
value={value}
|
||||
inputMode={inputMode}
|
||||
style={[styles.input, style]}
|
||||
placeholder={placeholder}
|
||||
multiline={false}
|
||||
onChangeText={onChangeText}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
input: {
|
||||
width: "100%",
|
||||
padding: 15,
|
||||
borderColor: '#777',
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
backgroundColor: '#fff',
|
||||
fontSize: 20,
|
||||
},
|
||||
});
|
||||
12
traque-app/components/stat.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TouchableOpacity, View, Image, Text, Alert } from 'react-native';
|
||||
|
||||
export default function Stat({ children, source, description }) {
|
||||
return (
|
||||
<TouchableOpacity onPress={description ? () => Alert.alert("Info", description) : null}>
|
||||
<View style={{height: 30, flexDirection: "row", justifyContent: 'center', alignItems: 'center'}}>
|
||||
{source && <Image source={source} style={{width: 30, height: 30, marginRight: 5}} resizeMode="contain"/>}
|
||||
<Text style={{fontSize: 15}}>{children}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
24
traque-app/context/socketContext.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const HOST = '192.168.15.129'; // IP of the machine hosting the server
|
||||
|
||||
const SOCKET_URL = (HOST == "traque.rezel.net" ? "wss://" : "ws://") + HOST + "/player";
|
||||
const SERVER_URL = (HOST == "traque.rezel.net" ? "https://" : "http://") + HOST + "/back";
|
||||
|
||||
export const teamSocket = io(SOCKET_URL, {
|
||||
path: "/back/socket.io",
|
||||
});
|
||||
|
||||
export const SocketContext = createContext();
|
||||
|
||||
export default function SocketProvider({ children }) {
|
||||
const value = useMemo(() => ({ teamSocket, SERVER_URL }), [teamSocket]);
|
||||
return (
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSocket() {
|
||||
return useContext(SocketContext);
|
||||
}
|
||||
24
traque-app/context/teamConnexionContext.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createContext, useContext, useMemo } from "react";
|
||||
import { useSocket } from "./socketContext";
|
||||
import { useSocketAuth } from "../hook/useSocketAuth";
|
||||
|
||||
const teamConnexionContext = createContext();
|
||||
const TeamConnexionProvider = ({ children }) => {
|
||||
const { teamSocket } = useSocket();
|
||||
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
|
||||
|
||||
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading}), [teamId, login, loggedIn, loading]);
|
||||
|
||||
return (
|
||||
<teamConnexionContext.Provider value={value}>
|
||||
{children}
|
||||
</teamConnexionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useTeamConnexion() {
|
||||
return useContext(teamConnexionContext);
|
||||
}
|
||||
|
||||
export { TeamConnexionProvider, useTeamConnexion };
|
||||
|
||||
50
traque-app/context/teamContext.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useLocation } from "../hook/useLocation";
|
||||
import { useSocketListener } from "../hook/useSocketListener";
|
||||
import { createContext, useContext, useMemo, useRef, useState } from "react";
|
||||
import { useSocket } from "./socketContext";
|
||||
import { GameState } from "../util/gameState";
|
||||
import useSendDeviceInfo from "../hook/useSendDeviceInfo";
|
||||
|
||||
|
||||
const teamContext = createContext()
|
||||
function TeamProvider({children}) {
|
||||
const [teamInfos, setTeamInfos] = useState({});
|
||||
const [gameState, setGameState] = useState(GameState.SETUP);
|
||||
const [gameSettings, setGameSettings] = useState(null);
|
||||
const [zoneExtremities, setZoneExtremities] = useState(null);
|
||||
const [nextZoneDate, setNextZoneDate] = useState(null);
|
||||
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
|
||||
const {teamSocket} = useSocket();
|
||||
const teamInfosRef = useRef();
|
||||
|
||||
useSendDeviceInfo();
|
||||
|
||||
teamInfosRef.current = teamInfos;
|
||||
|
||||
function setCurrentZone(data) {
|
||||
const newBegin = {points : data.begin.points.map( p => ({latitude: p.lat,longitude: p.lng}) ), duration: data.begin.duration};
|
||||
const newEnd = {points : data.end.points.map( p => ({latitude: p.lat,longitude: p.lng}) ), duration: data.end.duration};
|
||||
setZoneExtremities({begin: newBegin, end: newEnd});
|
||||
setNextZoneDate(data.endDate);
|
||||
}
|
||||
|
||||
useSocketListener(teamSocket, "update_team", (newTeamInfos) => {setTeamInfos({...teamInfosRef.current, ...newTeamInfos})});
|
||||
useSocketListener(teamSocket, "game_state", setGameState);
|
||||
useSocketListener(teamSocket, "current_zone", setCurrentZone);
|
||||
useSocketListener(teamSocket, "game_settings", setGameSettings);
|
||||
|
||||
const value = useMemo(() => (
|
||||
{teamInfos, gameState, zoneExtremities, nextZoneDate, gameSettings, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
|
||||
), [teamInfos, gameState, zoneExtremities, nextZoneDate, gameSettings, location]);
|
||||
return (
|
||||
<teamContext.Provider value={value}>
|
||||
{children}
|
||||
</teamContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useTeamContext() {
|
||||
return useContext(teamContext);
|
||||
}
|
||||
|
||||
export { TeamProvider, useTeamContext };
|
||||
69
traque-app/dev_build_android.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# How to create a local developpement build with Expo
|
||||
Expo go is great to start a React Native project with Expo but reaches its limits when background localisation or server notifications have to be implemented and tested. This tutorial will explain the steps to create the debug android app of the current project (ie developpement build) and download it on an android device. A similar process can be done to test the IOS app but it requires a Mac so this won't be covered in this tutorial. A virtual android device can be created with Android Studio (explained later) but the process to send the app on the virtual device isn't covered in this tutorial. Here is a link that may help you : https://youtu.be/cs-zgHjt5RQ?si=Fzxik7zreek07uC0. Each step has precisions, however some of those precisions may not be suited for your device, don't hesitate to find help on the Internet or ask ChapGPT.
|
||||
|
||||
|
||||
## Set up your environnement
|
||||
|
||||
This section will cover the set up of your environnement in order to have the tools to either send the apk of your app on your device or create a virtual android device.
|
||||
|
||||
### Tutorial
|
||||
|
||||
Follow this tutorial : https://reactnative.dev/docs/set-up-your-environment?os=linux&platform=android
|
||||
|
||||
### Precisions
|
||||
|
||||
* Android Studio and its dependancies can take up to 15 Go of space.
|
||||
* The `.bashrc` file is located in your home directory (`cd ~`).
|
||||
* The Watchman installation isn't necessary.
|
||||
* If you want to test your app on a physical android device, follow the next section.
|
||||
|
||||
|
||||
## Set up your physical android device
|
||||
|
||||
This section will cover the actions to perform on your device to be able to download the apk of the app.
|
||||
|
||||
### Tutorial
|
||||
|
||||
Follow the two first sections of this tutorial : https://reactnative.dev/docs/running-on-device?os=linux&platform=android
|
||||
|
||||
### Precisions
|
||||
|
||||
* In some devices, you may also enable the *install via USB* option.
|
||||
* There is no need to modify the `/etc/udev/rules.d/51-android-usb.rules`, you can skip this part of the tutorial.
|
||||
* When your device is connected, keep in your mind that a pop up asking authorizations can appear on it.
|
||||
|
||||
|
||||
## Build the native app
|
||||
|
||||
This section will cover the building of the app and the sending on your device.
|
||||
|
||||
### Tutorial
|
||||
|
||||
* Go in your project folder (`traque-app`).
|
||||
* Run `npx expo install expo-dev-client`. This install the `expo-dev-client` package if it isn't already.
|
||||
* Run `npx expo prebuild --platform android`. This will add an android folder in your project taking about 1 Go of space.
|
||||
* Connect your device to your computer with a USB cable and and run `npx expo run:android`. This will build the app and send it on your device. The operation can last for up to 10 minutes and will take up to 10 Go of space in total (mainly cache in the `~/.gradle` folder).
|
||||
* The application should appear on your device. At this point there is no more need of the USB cable (you can disable *USB debugging*).
|
||||
|
||||
### Precisions
|
||||
|
||||
While running `npx expo run:android`, you can have this error :
|
||||
```
|
||||
Task :expo-modules-core:compileDebugKotlin FAILED
|
||||
e: This version (1.5.15) of the Compose Compiler requires Kotlin version 1.9.25 but you appear to be using Kotlin version 1.9.24 which is not known to be compatible.
|
||||
```
|
||||
Follow these steps to fix it :
|
||||
|
||||
* In the `android/build.gradle` file, replace the line `classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')` by `classpath('org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25')`.
|
||||
* In the `traque-app` folder, run `npx expo run:android` again.
|
||||
|
||||
|
||||
## Run the app
|
||||
|
||||
This section will explain how to run the app. The process is really similar to the one to run the app on Expo go and can be done with your device on the same WI-FI network as your computer.
|
||||
|
||||
### Tutorial
|
||||
|
||||
* Start the developpement server by running `npm start`, a blue *development build* should appear in your terminal.
|
||||
* Enter the created app on your device. The app will update as well as if you were on Expo go if you modify the code on your computer.
|
||||
* If the app crashes, you may need to restart the developpement server.
|
||||
33
traque-app/hook/useGame.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useSocket } from "../context/socketContext";
|
||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||
import { useTeamContext } from "../context/teamContext";
|
||||
|
||||
export default function useGame() {
|
||||
const { teamSocket } = useSocket();
|
||||
const { teamId } = useTeamConnexion();
|
||||
const { teamInfos, gameState } = useTeamContext();
|
||||
|
||||
function sendCurrentPosition() {
|
||||
console.log("Reveal position.")
|
||||
teamSocket.emit("send_position");
|
||||
}
|
||||
|
||||
function capture(captureCode) {
|
||||
console.log("Try to capture :", captureCode);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn("Server did not respond to capture emit.");
|
||||
reject();
|
||||
}, 3000);
|
||||
|
||||
teamSocket.emit("capture", captureCode, (response) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(response.message);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {...teamInfos, sendCurrentPosition, capture, teamId, gameState};
|
||||
}
|
||||
34
traque-app/hook/useLocalStorage.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useLocalStorage(key, initialValue) {
|
||||
const [storedValue, setStoredValue] = useState(initialValue);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const item = await AsyncStorage.getItem(key);
|
||||
setStoredValue(item ? JSON.parse(item) : initialValue);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const setValue = 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);
|
||||
}
|
||||
}
|
||||
|
||||
return [storedValue, setValue, loading];
|
||||
}
|
||||
80
traque-app/hook/useLocation.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
|
||||
import * as Location from 'expo-location';
|
||||
import { useSocket } from "../context/socketContext";
|
||||
|
||||
export function useLocation(timeInterval, distanceInterval) {
|
||||
const [location, setLocation] = useState(null); // [latitude, longitude]
|
||||
const { teamSocket } = useSocket();
|
||||
const LOCATION_TASK_NAME = "background-location-task";
|
||||
const locationUpdateParameters = {
|
||||
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
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
console.log("Sending position :", new_location);
|
||||
teamSocket.emit("update_position", new_location);
|
||||
} else {
|
||||
console.log("No location measured.")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getLocationAuthorization();
|
||||
}, []);
|
||||
|
||||
async function getLocationAuthorization() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function startLocationTracking() {
|
||||
if (await getLocationAuthorization()) {
|
||||
if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) {
|
||||
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters);
|
||||
console.log("Location tracking started.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function stopLocationTracking() {
|
||||
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
|
||||
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
|
||||
console.log("Location tracking stopped.")
|
||||
}
|
||||
}
|
||||
|
||||
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
|
||||
}
|
||||
58
traque-app/hook/usePickImage.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState, } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
|
||||
|
||||
export function usePickImage() {
|
||||
const [image, setImage] = useState(null);
|
||||
|
||||
const pickImage = async () => {
|
||||
try {
|
||||
const permissionResult = await requestMediaLibraryPermissionsAsync();
|
||||
|
||||
if (permissionResult.granted === false) {
|
||||
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,
|
||||
allowsEditing: true,
|
||||
aspect: [4, 3],
|
||||
quality: 1,
|
||||
});
|
||||
|
||||
if (!result.canceled && result) {
|
||||
setImage(result.assets[0]);
|
||||
}
|
||||
else {
|
||||
console.log('Image picker cancelled.');
|
||||
}
|
||||
}
|
||||
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};
|
||||
}
|
||||
35
traque-app/hook/useSendDeviceInfo.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import { useSocket } from "../context/socketContext";
|
||||
import { useTeamConnexion } from "../context/teamConnexionContext";
|
||||
|
||||
export default function useSendDeviceInfo() {
|
||||
const batteryUpdateTimeout = 5*60*1000;
|
||||
const { teamSocket } = useSocket();
|
||||
const {loggedIn} = useTeamConnexion();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedIn) return;
|
||||
|
||||
const sendInfo = async () => {
|
||||
const brand = DeviceInfo.getBrand();
|
||||
const model = DeviceInfo.getModel();
|
||||
const name = await DeviceInfo.getDeviceName();
|
||||
teamSocket.emit('deviceInfo', {model: brand + " " + model, name: name});
|
||||
};
|
||||
|
||||
const sendBattery = async () => {
|
||||
const level = await DeviceInfo.getBatteryLevel();
|
||||
teamSocket.emit('batteryUpdate', Math.round(level * 100));
|
||||
};
|
||||
|
||||
sendInfo();
|
||||
sendBattery();
|
||||
|
||||
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout);
|
||||
|
||||
return () => {clearInterval(batteryCheckInterval)};
|
||||
}, [loggedIn]);
|
||||
|
||||
return null;
|
||||
}
|
||||
72
traque-app/hook/useSocketAuth.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
const LOGIN_MESSAGE = "login";
|
||||
const LOGOUT_MESSAGE = "logout";
|
||||
|
||||
export function useSocketAuth(socket, passwordName) {
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
function login(password) {
|
||||
console.log("Try to log in with :", password);
|
||||
setSavedPassword(password);
|
||||
setWaitingForResponse(true);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn("Server did not respond to login emit.");
|
||||
setWaitingForResponse(false);
|
||||
reject();
|
||||
}, 3000);
|
||||
|
||||
socket.emit(LOGIN_MESSAGE, password, (response) => {
|
||||
clearTimeout(timeout);
|
||||
console.log(response.message);
|
||||
setLoggedIn(response.isLoggedIn);
|
||||
setWaitingForResponse(false);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
console.log("Logout");
|
||||
setSavedPassword(null);
|
||||
setLoggedIn(false);
|
||||
socket.emit(LOGOUT_MESSAGE);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(!waitingForResponse && !savedPasswordLoading) {
|
||||
setLoading(false);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
}, [waitingForResponse, savedPasswordLoading]);
|
||||
|
||||
return {login, logout, password: savedPassword, loggedIn, loading};
|
||||
}
|
||||
10
traque-app/hook/useSocketListener.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useSocketListener(socket, event, callback) {
|
||||
useEffect(() => {
|
||||
socket.on(event,callback);
|
||||
return () => {
|
||||
socket.off(event, callback);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
21
traque-app/hook/useTimeDifference.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useTimeDifference(refTime, timeout) {
|
||||
// If refTime is in the past, time will be positive
|
||||
// If refTime is in the future, time will be negative
|
||||
// The time is updated every timeout milliseconds
|
||||
const [time, setTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const updateTime = () => {
|
||||
setTime(Math.floor((Date.now() - refTime) / 1000));
|
||||
};
|
||||
|
||||
updateTime();
|
||||
const interval = setInterval(updateTime, timeout);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [refTime]);
|
||||
|
||||
return [time];
|
||||
}
|
||||
39682
traque-app/package-lock.json
generated
Normal file
50
traque-app/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "traque-app",
|
||||
"version": "1.0.0",
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
"@react-navigation/stack": "^7.1.1",
|
||||
"axxios": "^0.1.0",
|
||||
"expo": "~52.0.46",
|
||||
"expo-build-properties": "~0.13.3",
|
||||
"expo-constants": "~17.0.2",
|
||||
"expo-dev-client": "~5.0.20",
|
||||
"expo-font": "~13.0.4",
|
||||
"expo-image-picker": "~16.0.6",
|
||||
"expo-linking": "~7.0.2",
|
||||
"expo-location": "~18.0.10",
|
||||
"expo-router": "~4.0.21",
|
||||
"expo-splash-screen": "~0.29.24",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"expo-task-manager": "~12.0.6",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.9",
|
||||
"react-native-collapsible": "^1.6.2",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-dotenv": "^3.4.11",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-image-picker": "^4.0.6",
|
||||
"react-native-image-viewing": "^0.2.2",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-maps": "1.18.0",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "~4.4.0",
|
||||
"react-native-web": "~0.19.6",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@react-native-community/cli": "latest"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
6
traque-app/util/gameState.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const GameState = {
|
||||
SETUP: "setup",
|
||||
PLACEMENT: "placement",
|
||||
PLAYING: "playing",
|
||||
FINISHED: "finished"
|
||||
}
|
||||