Compare commits

...

7 Commits

  1. BIN
      builds/application-40b1ad7e-a2d3-41bb-acf7-bc0e40645c41.aab
  2. 1
      vds-app/.expo-shared/assets.json
  3. 12
      vds-app/App/components/Banner.js
  4. 4
      vds-app/App/components/Variables.js
  5. 133
      vds-app/App/index.js
  6. 4
      vds-app/App/screens/Dictionary.js
  7. 348
      vds-app/App/screens/Exam.js
  8. 4
      vds-app/App/screens/Info.js
  9. 318
      vds-app/App/screens/Quiz.js
  10. 5
      vds-app/App/screens/QuizIndex.js
  11. 291
      vds-app/App/screens/Recap.js
  12. 200
      vds-app/App/screens/RecapExam.js
  13. 4
      vds-app/App/screens/RecapTrueFalse.js
  14. 99
      vds-app/App/screens/Results.js
  15. 243
      vds-app/App/screens/ResultsTrueFalse.js
  16. 4
      vds-app/App/screens/Setup.js
  17. 317
      vds-app/App/screens/Splash.js
  18. 215
      vds-app/App/screens/TrueFalse.js
  19. 16
      vds-app/android/.gitignore
  20. 182
      vds-app/android/app/build.gradle
  21. BIN
      vds-app/android/app/debug.keystore
  22. 14
      vds-app/android/app/proguard-rules.pro
  23. 7
      vds-app/android/app/src/debug/AndroidManifest.xml
  24. 7
      vds-app/android/app/src/debugOptimized/AndroidManifest.xml
  25. 36
      vds-app/android/app/src/main/AndroidManifest.xml
  26. 61
      vds-app/android/app/src/main/java/com/dslak/vdsquiz/MainActivity.kt
  27. 56
      vds-app/android/app/src/main/java/com/dslak/vdsquiz/MainApplication.kt
  28. BIN
      vds-app/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
  29. BIN
      vds-app/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
  30. BIN
      vds-app/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
  31. BIN
      vds-app/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
  32. BIN
      vds-app/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
  33. 6
      vds-app/android/app/src/main/res/drawable/ic_launcher_background.xml
  34. 37
      vds-app/android/app/src/main/res/drawable/rn_edit_text_material.xml
  35. BIN
      vds-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  36. BIN
      vds-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
  37. BIN
      vds-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  38. BIN
      vds-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
  39. BIN
      vds-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  40. BIN
      vds-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
  41. BIN
      vds-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  42. BIN
      vds-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  43. BIN
      vds-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  44. BIN
      vds-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  45. 1
      vds-app/android/app/src/main/res/values-night/colors.xml
  46. 6
      vds-app/android/app/src/main/res/values/colors.xml
  47. 5
      vds-app/android/app/src/main/res/values/strings.xml
  48. 11
      vds-app/android/app/src/main/res/values/styles.xml
  49. 24
      vds-app/android/build.gradle
  50. 69
      vds-app/android/gradle.properties
  51. BIN
      vds-app/android/gradle/wrapper/gradle-wrapper.jar
  52. 7
      vds-app/android/gradle/wrapper/gradle-wrapper.properties
  53. 251
      vds-app/android/gradlew
  54. 94
      vds-app/android/gradlew.bat
  55. 39
      vds-app/android/settings.gradle
  56. 49
      vds-app/app.json
  57. BIN
      vds-app/builds/application-5340e044-a24c-439e-a724-182a853ebd65.aab
  58. 1
      vds-app/dist/assetmap.json
  59. BIN
      vds-app/dist/assets/02bc1fa7c0313217bde2d65ccbff40c9
  60. BIN
      vds-app/dist/assets/0ea69b5077e7c4696db85dbcba75b0e1
  61. BIN
      vds-app/dist/assets/319b7d9681027a1ef8d3fb9520d73a17
  62. BIN
      vds-app/dist/assets/35ba0eaec5a4f5ed12ca16fabeae451d
  63. BIN
      vds-app/dist/assets/376d6a4c7f622917c39feb23671ef71d
  64. BIN
      vds-app/dist/assets/4ade0cd9affb7de397ece858c4ef9212
  65. BIN
      vds-app/dist/assets/5223c8d9b0d08b82a5670fb5f71faf78
  66. BIN
      vds-app/dist/assets/778ffc9fe8773a878e9c30a6304784de
  67. BIN
      vds-app/dist/assets/7d40544b395c5949f4646f5e150fe020
  68. BIN
      vds-app/dist/assets/a132ecc4ba5c1517ff83c0fb321bc7fc
  69. BIN
      vds-app/dist/assets/c79c3606a1cf168006ad3979763c7e0c
  70. BIN
      vds-app/dist/assets/cdd04e13d4ec83ff0cd13ec8dabdc341
  71. BIN
      vds-app/dist/assets/e6b3e73c7c9600ce768eba302c6ad054
  72. BIN
      vds-app/dist/assets/f5b790e2ac193b3d41015edb3551f9b8
  73. BIN
      vds-app/dist/bundles/android-21da78b5401f0954d05cf90eba32fc85.js
  74. 1
      vds-app/dist/bundles/android-21da78b5401f0954d05cf90eba32fc85.map
  75. BIN
      vds-app/dist/bundles/ios-62b68d2e0eb54df3fa8210b6752aa722.js
  76. 1
      vds-app/dist/bundles/ios-62b68d2e0eb54df3fa8210b6752aa722.map
  77. 6
      vds-app/dist/debug.html
  78. 1
      vds-app/dist/metadata.json
  79. 3
      vds-app/eas.json
  80. 8
      vds-app/index.js
  81. 7
      vds-app/metro.config.js
  82. 60
      vds-app/package.json
  83. 8286
      vds-app/yarn-error.log
  84. 22
      yarn.lock

BIN
builds/application-40b1ad7e-a2d3-41bb-acf7-bc0e40645c41.aab

Binary file not shown.

1
vds-app/.expo-shared/assets.json

@ -1 +0,0 @@
{}

12
vds-app/App/components/Banner.js

@ -2,8 +2,8 @@ import React from "react"
import { View, StyleSheet, StatusBar, Text, Dimensions } from "react-native"
import { colors, texts, credentials } from "./Variables"
import { BannerAd, BannerAdSize, TestIds } from 'react-native-google-mobile-ads'
const adUnitId = __DEV__ ? TestIds.INTERSTITIAL : 'ca-app-pub-4145771316565790/1848957462'
// import { BannerAd, BannerAdSize, TestIds } from 'react-native-google-mobile-ads'
// const adUnitId = __DEV__ ? TestIds.INTERSTITIAL : 'ca-app-pub-4145771316565790/1848957462'
const screen = Dimensions.get("window")
@ -13,8 +13,8 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: "center",
justifyContent: "center",
marginTop: 20,
height: 100,
//marginTop: 20,
//height: 100,
width: "100%"
}
})
@ -24,9 +24,9 @@ export const Banner = () => {
let banner
if(__DEV__) {
banner = <Text>DEV BANNER</Text>
banner = <Text></Text>
} else {
banner = <BannerAd size={BannerAdSize.LARGE_BANNER} unitId={adUnitId} />
banner = <Text></Text> //<BannerAd size={BannerAdSize.LARGE_BANNER} unitId={adUnitId} />
}
return (
<View style={styles.container}>

4
vds-app/App/components/Variables.js

@ -105,8 +105,8 @@ export const examScheme = [
{section: "firstAid", questions: 1, points: 2},
{section: "flightSafety", questions: 1, points: 4},
{section: "instruments", questions: 1, points: 2}
]
*/
]*/
export const resultsScheme = [
{points: "da 86 a 100 punti", result: "idoneo"},

133
vds-app/App/index.js

@ -1,94 +1,45 @@
import 'react-native-gesture-handler'
import { createAppContainer } from "react-navigation"
import { createStackNavigator } from "react-navigation-stack"
import 'react-native-gesture-handler';
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Splash from "./screens/Splash"
import QuizIndex from "./screens/QuizIndex"
import Quiz from "./screens/Quiz"
import TrueFalse from "./screens/TrueFalse"
import Exam from "./screens/Exam"
import Results from "./screens/Results"
import ResultsTrueFalse from "./screens/ResultsTrueFalse"
import Recap from "./screens/Recap"
import RecapTrueFalse from "./screens/RecapTrueFalse"
import Info from "./screens/Info"
import Setup from "./screens/Setup"
import Dictionary from "./screens/Dictionary"
import { colors, texts} from "./components/Variables"
import Splash from "./screens/Splash";
import QuizIndex from "./screens/QuizIndex";
import Quiz from "./screens/Quiz";
import TrueFalse from "./screens/TrueFalse";
import Exam from "./screens/Exam";
import Results from "./screens/Results";
import ResultsTrueFalse from "./screens/ResultsTrueFalse";
import Recap from "./screens/Recap";
import RecapExam from "./screens/RecapExam";
import RecapTrueFalse from "./screens/RecapTrueFalse";
import Info from "./screens/Info";
import Setup from "./screens/Setup";
import Dictionary from "./screens/Dictionary";
const MainStack = createStackNavigator({
Splash: {
screen: Splash,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
RecapTrueFalse: {
screen: RecapTrueFalse,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Recap: {
screen: Recap,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Results: {
screen: Results,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
ResultsTrueFalse: {
screen: ResultsTrueFalse,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Info: {
screen: Info,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Dictionary: {
screen: Dictionary,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
QuizIndex: {
screen: QuizIndex,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Quiz: {
screen: Quiz,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
TrueFalse: {
screen: TrueFalse,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Exam: {
screen: Exam,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
},
Setup: {
screen: Setup,
navigationOptions: ({ navigation }) => ({
headerShown: false
})
}
})
const Stack = createNativeStackNavigator();
export default createAppContainer(MainStack)
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="Splash"
screenOptions={{ headerShown: false }} // hides all headers
>
<Stack.Screen name="Splash" component={Splash} />
<Stack.Screen name="RecapTrueFalse" component={RecapTrueFalse} />
<Stack.Screen name="Recap" component={Recap} />
<Stack.Screen name="RecapExam" component={RecapExam} />
<Stack.Screen name="Results" component={Results} />
<Stack.Screen name="ResultsTrueFalse" component={ResultsTrueFalse} />
<Stack.Screen name="Info" component={Info} />
<Stack.Screen name="Dictionary" component={Dictionary} />
<Stack.Screen name="QuizIndex" component={QuizIndex} />
<Stack.Screen name="Quiz" component={Quiz} />
<Stack.Screen name="TrueFalse" component={TrueFalse} />
<Stack.Screen name="Exam" component={Exam} />
<Stack.Screen name="Setup" component={Setup} />
</Stack.Navigator>
</NavigationContainer>
);
}

4
vds-app/App/screens/Dictionary.js

@ -130,11 +130,11 @@ class Dictionary extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove()
}
handleBackButton = () => {

348
vds-app/App/screens/Exam.js

@ -1,20 +1,20 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, ImageBackground, BackHandler } from "react-native"
import SafeAreaView from 'react-native-safe-area-view'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Button, ButtonContainer } from "../components/Button"
import { colors, texts, examScheme } from "../components/Variables"
import aerodynamicsQuestions from "../data/aerodynamics"
import firstAidQuestions from "../data/firstAid"
import flightSafetyQuestions from "../data/flightSafety"
import instrumentsQuestions from "../data/instruments"
import legislationQuestions from "../data/legislation"
import materialsQuestions from "../data/materials"
import meteorologyQuestions from "../data/meteorology"
import physiopathologyQuestions from "../data/physiopathology"
import pilotingTechniquesQuestions from "../data/pilotingTechniques"
import React from "react";
import { View, ScrollView, StyleSheet, StatusBar, Text, ImageBackground, BackHandler } from "react-native";
import SafeAreaView from 'react-native-safe-area-view';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Button, ButtonContainer } from "../components/Button";
import { colors, texts, examScheme } from "../components/Variables";
import aerodynamicsQuestions from "../data/aerodynamics";
import firstAidQuestions from "../data/firstAid";
import flightSafetyQuestions from "../data/flightSafety";
import instrumentsQuestions from "../data/instruments";
import legislationQuestions from "../data/legislation";
import materialsQuestions from "../data/materials";
import meteorologyQuestions from "../data/meteorology";
import physiopathologyQuestions from "../data/physiopathology";
import pilotingTechniquesQuestions from "../data/pilotingTechniques";
const allQuestions = {
aerodynamics: aerodynamicsQuestions,
@ -26,107 +26,93 @@ const allQuestions = {
meteorology: meteorologyQuestions,
physiopathology: physiopathologyQuestions,
pilotingTechniques: pilotingTechniquesQuestions
}
};
const bgImage = require("../assets/bg.jpg")
const bgImage = require("../assets/bg.jpg");
const styles = StyleSheet.create({
container: {
flex: 1
},
text: {
color: colors.white,
fontSize: 20,
textAlign: "center",
fontWeight: "600",
paddingTop: 5,
paddingBottom: 20
},
textCode: {
color: colors.white,
fontSize: 12,
textAlign: "center",
fontWeight: "500",
paddingTop: 20,
paddingBottom: 0
},
timer: {
color: colors.white,
fontSize: 30,
textAlign: "center",
fontWeight: "600",
paddingVertical: 10,
marginTop: 30,
backgroundColor: colors.white_alpha,
borderRadius: 10,
textShadowColor: 'rgba(0, 0, 0, 0.45)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 2
},
safearea: {
flex: 1,
marginTop: 20,
paddingHorizontal: 20,
justifyContent: "space-between"
},
bg: {
width: "100%",
height: "100%"
},
})
let interval = null
const maxTime = 1800
container: { flex: 1 },
text: { color: colors.white, fontSize: 20, textAlign: "center", fontWeight: "600", paddingTop: 5, paddingBottom: 20 },
textCode: { color: colors.white, fontSize: 12, textAlign: "center", fontWeight: "500", paddingTop: 20, paddingBottom: 0 },
timer: { color: colors.white, fontSize: 30, textAlign: "center", fontWeight: "600", paddingVertical: 10, marginTop: 30, backgroundColor: colors.white_alpha, borderRadius: 10, textShadowColor: 'rgba(0, 0, 0, 0.45)', textShadowOffset: { width: -1, height: 1 }, textShadowRadius: 2 },
safearea: { flex: 1, marginTop: 20, paddingHorizontal: 20, justifyContent: "space-between" },
bg: { width: "100%", height: "100%" }
});
class Exam extends React.Component {
const MAX_TIME = 1800; // 30 minutes
state = {
correctCount: 0,
pointsCount: 0,
totalPoints: 0,
wrongCount: 0,
wrongAnswers: [],
totalCount: this.props.navigation.getParam("questions", []).length,
availableIds: this.props.navigation.getParam("questions", []).map(a => a.id),
activeQuestionId: this.props.navigation.getParam("questions", [])[
Math.floor(Math.random() * this.props.navigation.getParam("questions", []).length)
].id,
answered: false,
answerCorrect: false,
results: false,
timer: maxTime
class Exam extends React.Component {
constructor(props) {
super(props);
const { questions = [] } = props.route.params || {};
this.state = {
correctCount: 0,
wrongCount: 0,
pointsCount: 0,
totalPoints: 0,
wrongAnswers: [],
totalCount: questions.length,
availableIds: questions.map(q => q.id),
activeQuestionId: questions.length ? questions[Math.floor(Math.random() * questions.length)].id : null,
answered: false,
answerCorrect: false,
results: false,
timer: MAX_TIME,
clickedId: null
};
this.interval = null;
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
// BackHandler subscription
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
// Start timer
this.startTimer();
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove()
if (this.interval) clearInterval(this.interval);
}
handleBackButton = () => {
startTimer = () => {
this.interval = setInterval(() => {
this.setState(prev => {
if (prev.timer <= 1) {
clearInterval(this.interval);
this.showResults();
return { timer: 0, results: true };
}
return { timer: prev.timer - 1 };
});
}, 1000);
}
let tmpQuestions = []
handleBackButton = () => {
let tmpQuestions = [];
AsyncStorage.getItem('setupData').then((value) => {
let setupData = JSON.parse(value)
examScheme.forEach( (elem) => {
let currentSection = setupData.excludeDelta ? allQuestions[elem.section].filter(item => !item.delta) : allQuestions[elem.section]
for(let i=0; i<elem.questions; i++) {
const currentIndex = Math.floor(Math.random() * currentSection.length)
tmpQuestions.push(currentSection[currentIndex])
currentSection = currentSection.filter( (item, index) => index != currentIndex)
}
})
const setupData = JSON.parse(value) || {};
this.props.navigation.navigate("Splash", {
examQuestions: tmpQuestions
})
examScheme.forEach(elem => {
let currentSection = setupData.excludeDelta
? allQuestions[elem.section].filter(item => !item.delta)
: allQuestions[elem.section];
for (let i = 0; i < elem.questions; i++) {
const currentIndex = Math.floor(Math.random() * currentSection.length);
tmpQuestions.push(currentSection[currentIndex]);
currentSection = currentSection.filter((_, index) => index !== currentIndex);
}
});
return true
this.props.navigation.navigate("Splash", { examQuestions: tmpQuestions });
});
})
return true;
}
showResults = () => {
@ -140,125 +126,87 @@ class Exam extends React.Component {
totalPoints: this.state.totalPoints,
wrongAnswers: this.state.wrongAnswers
}
})
});
}
answer = (correct, id, question) => {
this.setState(
state => {
const nextState = { answered: true, clickedId: id, totalPoints: state.totalPoints + parseInt(question.points)}
if (correct) {
nextState.correctCount = state.correctCount + 1
nextState.pointsCount = state.pointsCount + parseInt(question.points)
nextState.answerCorrect = true
} else {
nextState.wrongCount = state.wrongCount + 1
nextState.answerCorrect = false
nextState.wrongAnswers = state.wrongAnswers
nextState.wrongAnswers.push(
{ question: question.question,
id: question.id,
clicked: id,
answers: question.answers
}
)
}
return nextState
},
() => {
if(this.state.timer > 1 || (this.state.correctCount+this.state.wrongCount) < this.state.totalCount) {
setTimeout(() => this.nextQuestion(), correct ? 750 : 3500)
}
this.setState(prev => {
const nextState = {
answered: true,
clickedId: id,
totalPoints: prev.totalPoints + parseInt(question.points)
};
if (correct) {
nextState.correctCount = prev.correctCount + 1;
nextState.pointsCount = prev.pointsCount + parseInt(question.points);
nextState.answerCorrect = true;
} else {
nextState.wrongCount = prev.wrongCount + 1;
nextState.answerCorrect = false;
nextState.wrongAnswers = [...prev.wrongAnswers, {
question: question.question,
id: question.id,
clicked: id,
answers: question.answers
}];
}
)
return nextState;
}, () => setTimeout(() => this.nextQuestion(), correct ? 750 : 3500));
}
nextQuestion = () => {
const { availableIds, activeQuestionId, correctCount, wrongCount, totalCount } = this.state;
const updatedIndexes = availableIds.filter(id => id !== activeQuestionId);
const updatedIndexes = this.state.availableIds.filter( item => item != this.state.activeQuestionId)
const nextId = updatedIndexes[Math.floor(Math.random() * updatedIndexes.length)]
let resultsShow = (this.state.timer <= 1 || (this.state.correctCount+this.state.wrongCount) == this.state.totalCount) ? true : false
if (!updatedIndexes.length) {
clearInterval(interval)
this.showResults()
} else {
this.setState( (state) => {
return {
availableIds: updatedIndexes,
activeQuestionId: nextId,
answered: false,
results: resultsShow
}
})
if (!updatedIndexes.length || (correctCount + wrongCount) === totalCount) {
clearInterval(this.interval);
this.showResults();
return;
}
}
componentWillUnmount(){
clearInterval(interval)
const nextId = updatedIndexes[Math.floor(Math.random() * updatedIndexes.length)];
this.setState({ availableIds: updatedIndexes, activeQuestionId: nextId, answered: false, results: false, clickedId: null });
}
render() {
const questions = this.props.navigation.getParam("questions", [])
const question = questions.filter(item => item.id == this.state.activeQuestionId)[0] || questions[0]
if(this.state.timer==maxTime) {
interval = setInterval( () => {
this.setState( (state) => {
return {
timer: this.state.timer-1,
results: this.state.timer <= 1 || false
}
})
}, 1000)
}
if(this.state.timer < 1 || (this.state.correctCount+this.state.wrongCount) == this.state.totalCount) {
clearInterval(interval)
setTimeout ( () => {
this.showResults()
}, 1000)
}
const { availableIds, activeQuestionId, results, correctCount, wrongCount, totalCount, clickedId } = this.state;
const questions = this.props.route?.params?.questions || [];
const question = questions.find(q => q.id === activeQuestionId) || questions[0];
return (
<ImageBackground source={bgImage} style={styles.bg} resizeMode="cover">
<ScrollView style={styles.container}>
<StatusBar barStyle="light-content" />
{!this.state.results ?
<SafeAreaView style={styles.safearea}>
<Text style={styles.timer}>{new Date(this.state.timer * 1000).toISOString().substr(11, 8)}</Text>
<View>
<Text style={styles.textCode}>{question.id}</Text>
<Text style={styles.text}>{question.question}</Text>
<ButtonContainer>
{question.answers.map(answer => (
<Button
key={answer.id}
text={answer.text}
noBorder={true}
colorize={{id: answer.id, clicked: this.state.clickedId, answered: this.state.answered, isCorrect: answer.correct}}
onPress={() => this.answer(answer.correct, answer.id, question)}
/>
))}
</ButtonContainer>
</View>
<Text style={styles.text}>
{`${this.state.correctCount+this.state.wrongCount}/${this.state.totalCount}`}
</Text>
</SafeAreaView>
: <SafeAreaView style={styles.safearea}></SafeAreaView>}
</ScrollView>
<ScrollView style={styles.container}>
<StatusBar barStyle="light-content" />
{!results && question ? (
<SafeAreaView style={styles.safearea}>
<Text style={styles.timer}>{new Date(this.state.timer * 1000).toISOString().substr(11, 8)}</Text>
<Text style={styles.textCode}>{question.id}</Text>
<Text style={styles.text}>{question.question}</Text>
<ButtonContainer>
{question.answers.map(answer => (
<Button
key={answer.id}
text={answer.text}
noBorder
colorize={{ id: answer.id, clicked: clickedId, answered: this.state.answered, isCorrect: answer.correct }}
onPress={() => this.answer(answer.correct, answer.id, question)}
/>
))}
</ButtonContainer>
<Text style={styles.text}>{`${correctCount + wrongCount}/${totalCount}`}</Text>
</SafeAreaView>
) : (
<SafeAreaView style={styles.safearea}></SafeAreaView>
)}
</ScrollView>
</ImageBackground>
)
);
}
}
export default Exam
export default Exam;

4
vds-app/App/screens/Info.js

@ -127,11 +127,11 @@ class Info extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove()
}
handleBackButton = () => {

318
vds-app/App/screens/Quiz.js

@ -1,105 +1,58 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, ImageBackground, BackHandler } from "react-native"
import { Picker } from '@react-native-picker/picker'
import SafeAreaView from 'react-native-safe-area-view'
import AsyncStorage from '@react-native-async-storage/async-storage'
import React from "react";
import { View, ScrollView, StyleSheet, Text, Dimensions, ImageBackground, BackHandler } from "react-native";
import { Picker } from '@react-native-picker/picker';
import SafeAreaView from 'react-native-safe-area-view';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Button, ButtonContainer } from "../components/Button"
import { Banner } from "../components/Banner"
import { texts, colors, credentials } from "../components/Variables"
import { Button, ButtonContainer } from "../components/Button";
import { Banner } from "../components/Banner";
import { texts, colors } from "../components/Variables";
const bgImage = require("../assets/bg.jpg")
const screen = Dimensions.get("window")
const bgImage = require("../assets/bg.jpg");
const screen = Dimensions.get("window");
const styles = StyleSheet.create({
container: {
flex: 1
},
text: {
color: colors.white,
fontSize: 20,
textAlign: "center",
fontWeight: "600",
paddingTop: 5,
paddingBottom: 20
},
textCode: {
color: colors.white,
fontSize: 12,
textAlign: "center",
fontWeight: "500",
paddingTop: 20,
paddingBottom: 0
},
safearea: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 20,
justifyContent: "space-between"
},
box: {
width: screen.width,
paddingVertical: 10,
overflow: "hidden"
},
scrollView: {
//margin: 10,
height: screen.height-20
},
bg: {
width: "100%",
height: "100%"
},
dropdownContainer: {
marginTop: 20,
borderRadius: 5,
width: "100%",
textAlign: "center",
backgroundColor: colors.white_alpha2
},
dropdown: {
color: colors.black,
fontSize: 16,
width: "100%",
textAlign: "center",
fontWeight: "600",
backgroundColor: "transparent"
},
dropdownItem: {
color: colors.white,
backgroundColor: "red",
fontSize: 16,
borderRadius: 10,
textAlign: "center",
fontWeight: "600"
},
bannerContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
}
})
container: { flex: 1 },
text: { color: colors.white, fontSize: 20, textAlign: "center", fontWeight: "600", paddingTop: 5, paddingBottom: 20 },
textCode: { color: colors.white, fontSize: 12, textAlign: "center", fontWeight: "500", paddingTop: 20, paddingBottom: 0 },
safearea: { flex: 1, paddingHorizontal: 20, paddingTop: 20, justifyContent: "space-between" },
box: { width: screen.width, paddingVertical: 10, overflow: "hidden" },
scrollView: { height: screen.height - 20 },
bg: { width: "100%", height: "100%" },
dropdownContainer: { marginTop: 20, borderRadius: 5, width: "100%", textAlign: "center", backgroundColor: colors.white_alpha2 },
dropdown: { color: colors.black, fontSize: 16, width: "100%", textAlign: "center", fontWeight: "600", backgroundColor: "transparent" },
dropdownItem: { color: colors.white, backgroundColor: "red", fontSize: 16, borderRadius: 10, textAlign: "center", fontWeight: "600" },
bannerContainer: { flex: 1, alignItems: "center", justifyContent: "center" }
});
class Quiz extends React.Component {
state = {
correctCount: 0,
wrongCount: 0,
wrongAnswers: [],
pointsCount: 0,
totalPoints: 0,
totalCount: this.props.navigation.getParam("questions", []).length,
availableIds: this.props.navigation.getParam("questions", []).map(a => a.id),
availableQuestions: this.props.navigation.getParam("questions", []),
activeQuestionId: this.props.navigation.getParam("questions", [])[
this.props.navigation.getParam("randomQuestions") ?
Math.floor(Math.random() * this.props.navigation.getParam("questions", []).length) : 0
].id,
minIndex: 0,
answered: false,
answerCorrect: false,
results: false,
setupData: {}
constructor(props) {
super(props);
// Access route params safely
const { questions = [], randomQuestions = false, isWrong = false } = props.route.params || {};
this.state = {
correctCount: 0,
wrongCount: 0,
wrongAnswers: [],
pointsCount: 0,
totalPoints: 0,
totalCount: questions.length,
availableIds: questions.map(q => q.id),
availableQuestions: questions,
activeQuestionId: randomQuestions
? questions[Math.floor(Math.random() * questions.length)]?.id
: questions[0]?.id,
minIndex: 0,
answered: false,
answerCorrect: false,
results: false,
setupData: {},
isWrongParam: isWrong,
randomQuestionsParam: randomQuestions,
clickedId: null
};
}
bannerError = (e) => {
@ -107,66 +60,66 @@ class Quiz extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
// BackHandler subscription
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('setupData').then((value) => {
this.setState( (state) => {
return {
setupData: JSON.parse(value)
}
})
})
this.setState({ setupData: JSON.parse(value) || {} });
});
}
componentWillUnmount() {
this.backHandler?.remove();
}
handleBackButton = () => {
this.props.navigation.navigate("Splash")
return true
this.props.navigation.navigate("Splash");
return true;
}
answer = (correct, id, question) => {
this.setState(
state => {
const nextState = { answered: true, clickedId: id, totalPoints: state.totalPoints + parseInt(question.points)}
const nextState = { answered: true, clickedId: id, totalPoints: state.totalPoints + parseInt(question.points) };
if (correct) {
nextState.correctCount = state.correctCount + 1
nextState.pointsCount = state.pointsCount + parseInt(question.points)
nextState.answerCorrect = true
nextState.correctCount = state.correctCount + 1;
nextState.pointsCount = state.pointsCount + parseInt(question.points);
nextState.answerCorrect = true;
} else {
nextState.wrongCount = state.wrongCount + 1
nextState.answerCorrect = false,
nextState.wrongAnswers = state.wrongAnswers
nextState.wrongAnswers.push(
{ question: question.question,
id: question.id,
clicked: id,
answers: question.answers
}
)
nextState.wrongCount = state.wrongCount + 1;
nextState.answerCorrect = false;
nextState.wrongAnswers = [...state.wrongAnswers, {
question: question.question,
id: question.id,
clicked: id,
answers: question.answers
}];
}
return nextState
return nextState;
},
() => {
setTimeout(() => this.nextQuestion(), correct ? 750 : 3000)
}
)
() => setTimeout(() => this.nextQuestion(), correct ? 750 : 3000)
);
}
nextQuestion = () => {
const { availableIds, availableQuestions, activeQuestionId, minIndex } = this.state;
const updatedIndexes = this.state.availableIds.filter( item => item != this.state.activeQuestionId)
const updatedQuestions = this.state.availableQuestions.filter( item => updatedIndexes.indexOf(item.id) > -1)
const nextId = this.props.navigation.getParam("randomQuestions") ?
updatedIndexes[Math.floor(Math.random() * updatedIndexes.length)] :
updatedIndexes[this.state.minIndex]
let resultsShow = (this.state.timer <= 1 || (this.state.correctCount+this.state.wrongCount) == this.state.totalCount) ? true : false
const updatedIndexes = availableIds.filter(item => item !== activeQuestionId);
const updatedQuestions = availableQuestions.filter(item => updatedIndexes.includes(item.id));
const nextId = this.state.randomQuestionsParam
? updatedIndexes[Math.floor(Math.random() * updatedIndexes.length)]
: updatedIndexes[minIndex];
const resultsShow = !updatedIndexes.length || (this.state.correctCount + this.state.wrongCount) === this.state.totalCount;
if (!updatedIndexes.length) {
this.props.navigation.navigate("Results", {
results: {
isExam: false,
isWrong: this.props.navigation.getParam("isWrong"),
isWrong: this.state.isWrongParam,
total: this.state.totalCount,
correct: this.state.correctCount,
wrong: this.state.wrongCount,
@ -174,95 +127,84 @@ class Quiz extends React.Component {
totalPoints: this.state.totalPoints,
wrongAnswers: this.state.wrongAnswers
}
})
});
} else {
this.setState( (state) => {
return {
availableIds: updatedIndexes,
availableQuestions: updatedQuestions,
minIndex: this.state.minIndex >= updatedIndexes.length - 1 ? 0 : this.state.minIndex,
activeQuestionId: nextId,
answered: false,
results: resultsShow
}
})
this.setState({
availableIds: updatedIndexes,
availableQuestions: updatedQuestions,
minIndex: minIndex >= updatedIndexes.length - 1 ? 0 : minIndex,
activeQuestionId: nextId,
answered: false,
results: resultsShow,
clickedId: null
});
}
}
jumpTo = (questionId, itemIndex) => {
if(itemIndex) {
this.setState( (state) => {
return {
activeQuestionId: questionId,
minIndex: itemIndex-1
}
})
if (itemIndex) {
this.setState({
activeQuestionId: questionId,
minIndex: itemIndex - 1
});
}
}
render() {
const questions = this.props.navigation.getParam("questions", [])
const question = questions.filter(item => item.id == this.state.activeQuestionId)[0] || questions[0]
const { availableQuestions, activeQuestionId, results, correctCount, wrongCount, totalCount, clickedId } = this.state;
const question = availableQuestions.find(item => item.id === activeQuestionId) || availableQuestions[0];
return (
<ImageBackground source={bgImage} style={styles.bg} resizeMode="cover">
<View style={styles.box}>
<View style={styles.scrollView}>
<ScrollView style={ styles.container } >
{!this.state.results ?
<SafeAreaView style={styles.safearea}>
<View>
<Text style={styles.textCode}>{question.id}</Text>
<Text style={styles.text}>{question.question}</Text>
<ScrollView style={styles.container}>
{!results ? (
<SafeAreaView style={styles.safearea}>
<Text style={styles.textCode}>{question?.id}</Text>
<Text style={styles.text}>{question?.question}</Text>
<ButtonContainer>
{question.answers.map( (answer, index) => (
{question?.answers.map(answer => (
<Button
key={answer.id}
text={answer.text}
noBorder={true}
colorize={{id: answer.id, clicked: this.state.clickedId, answered: this.state.answered, isCorrect: answer.correct}}
noBorder
colorize={{ id: answer.id, clicked: clickedId, answered: this.state.answered, isCorrect: answer.correct }}
onPress={() => this.answer(answer.correct, answer.id, question)}
/>
))}
</ButtonContainer>
</View>
<Text style={styles.text}>
{`${this.state.correctCount+this.state.wrongCount}/${this.state.totalCount}`}
</Text>
<View style={styles.dropdownContainer}>
<Picker
style={styles.dropdown}
itemStyle={styles.dropdownItem}
onValueChange={(itemValue, itemIndex) => this.jumpTo(itemValue, itemIndex)}
>
<Picker.Item key={`itemPlaceholder`} label={texts.changeQuestion} value={0} />
{this.state.availableQuestions.map( (item, index) => (
<Picker.Item key={`item${item.id}`} label={`${item.id} - ${item.question}`} value={item.id} />
))}
</Picker>
</View>
</SafeAreaView>
: <SafeAreaView></SafeAreaView>}
<Text style={styles.text}>{`${correctCount + wrongCount}/${totalCount}`}</Text>
<View style={styles.dropdownContainer}>
<Picker
style={styles.dropdown}
itemStyle={styles.dropdownItem}
onValueChange={(itemValue, itemIndex) => this.jumpTo(itemValue, itemIndex)}
>
<Picker.Item key="itemPlaceholder" label={texts.changeQuestion} value={0} />
{availableQuestions.map((item, index) => (
<Picker.Item key={`item${item.id}`} label={`${item.id} - ${item.question}`} value={item.id} />
))}
</Picker>
</View>
</SafeAreaView>
) : (
<SafeAreaView></SafeAreaView>
)}
<View style={styles.bannerContainer}>
<Banner />
</View>
</ScrollView>
</View>
</View>
</ImageBackground>
)
);
}
}
export default Quiz
export default Quiz;

5
vds-app/App/screens/QuizIndex.js

@ -47,7 +47,8 @@ class QuizIndex extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then((value) => {
this.setState( (state) => {
return {
@ -67,7 +68,7 @@ class QuizIndex extends React.Component {
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove()
}
handleBackButton = () => {

291
vds-app/App/screens/Recap.js

@ -1,25 +1,29 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, Image, ImageBackground, BackHandler} from "react-native"
import SafeAreaView from 'react-native-safe-area-view'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Button, ButtonContainer } from "../components/Button"
import { Banner } from "../components/Banner"
import { colors, texts, examScheme, credentials } from "../components/Variables"
import aerodynamicsQuestions from "../data/aerodynamics"
import firstAidQuestions from "../data/firstAid"
import flightSafetyQuestions from "../data/flightSafety"
import instrumentsQuestions from "../data/instruments"
import legislationQuestions from "../data/legislation"
import materialsQuestions from "../data/materials"
import meteorologyQuestions from "../data/meteorology"
import physiopathologyQuestions from "../data/physiopathology"
import pilotingTechniquesQuestions from "../data/pilotingTechniques"
const screen = Dimensions.get("window")
const header = require("../assets/header.png")
const bgImage = require("../assets/bg.jpg")
import React, { useEffect, useState, useCallback } from "react";
import {
View,
ScrollView,
StyleSheet,
Text,
Image,
BackHandler,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useNavigation, useRoute, useFocusEffect } from "@react-navigation/native";
import { Button, ButtonContainer } from "../components/Button";
import { Banner } from "../components/Banner";
import { colors, texts, examScheme } from "../components/Variables";
import aerodynamicsQuestions from "../data/aerodynamics";
import firstAidQuestions from "../data/firstAid";
import flightSafetyQuestions from "../data/flightSafety";
import instrumentsQuestions from "../data/instruments";
import legislationQuestions from "../data/legislation";
import materialsQuestions from "../data/materials";
import meteorologyQuestions from "../data/meteorology";
import physiopathologyQuestions from "../data/physiopathology";
import pilotingTechniquesQuestions from "../data/pilotingTechniques";
const allQuestions = {
aerodynamics: aerodynamicsQuestions,
@ -30,45 +34,42 @@ const allQuestions = {
materials: materialsQuestions,
meteorology: meteorologyQuestions,
physiopathology: physiopathologyQuestions,
pilotingTechniques: pilotingTechniquesQuestions
}
pilotingTechniques: pilotingTechniquesQuestions,
};
const header = require("../assets/header.png");
const styles = StyleSheet.create({
container: {
backgroundColor: colors.dark_blue,
flex: 1
},
container: { backgroundColor: colors.dark_blue, flex: 1 },
safearea: {
flex: 1,
marginTop: 0,
justifyContent: "space-between",
paddingHorizontal: 20,
paddingBottom: 40
paddingBottom: 40,
},
headerContainer: {
marginTop: -40,
alignItems: "center",
justifyContent: "center",
width: "100%",
height: screen.width/1.5
},
header: {
width: "100%"
height: 200,
},
header: { width: "100%" },
box: {
marginTop: 30,
borderColor: colors.black_alpha,
borderWidth: 1,
padding: 15,
borderRadius: 5,
backgroundColor: colors.white_alpha
backgroundColor: colors.white_alpha,
},
text: {
color: colors.white,
fontSize: 20,
textAlign: "center",
fontWeight: "600",
paddingTop: 0
paddingTop: 0,
},
textCode: {
color: colors.white,
@ -76,7 +77,7 @@ const styles = StyleSheet.create({
textAlign: "center",
fontWeight: "500",
paddingTop: 10,
paddingBottom: 0
paddingBottom: 0,
},
textBig: {
color: colors.white,
@ -85,121 +86,149 @@ const styles = StyleSheet.create({
fontWeight: "400",
paddingBottom: 15,
textTransform: "uppercase",
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10
},
bg: {
width: "100%",
height: "100%"
textShadowColor: "rgba(0, 0, 0, 0.75)",
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
bannerContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
}
})
class Recap extends React.Component {
state = {
storeWrongAnswers: []
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then((value) => {
//console.log('storeWrongAnswers: ', JSON.parse(value))
this.setState( (state) => {
return {
storeWrongAnswers: JSON.parse(value)
}
})
})
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
}
handleBackButton = () => {
let tmpQuestions = []
AsyncStorage.getItem('setupData').then((value) => {
let setupData = JSON.parse(value)
examScheme.forEach( (elem) => {
let currentSection = setupData.excludeDelta ? allQuestions[elem.section].filter(item => !item.delta) : allQuestions[elem.section]
for(let i=0; i<elem.questions; i++) {
const currentIndex = Math.floor(Math.random() * currentSection.length)
tmpQuestions.push(currentSection[currentIndex])
currentSection = currentSection.filter( (item, index) => index != currentIndex)
bannerContainer: { flex: 1, alignItems: "center", justifyContent: "center" },
});
const Recap = () => {
const navigation = useNavigation();
const route = useRoute();
const [storeWrongAnswers, setStoreWrongAnswers] = useState([]);
// Load stored wrong answers
useEffect(() => {
AsyncStorage.getItem("storeWrongAnswers").then((value) => {
if (value) {
setStoreWrongAnswers(JSON.parse(value));
}
});
}, []);
const handleBackButton = useCallback(() => {
const tmpQuestions = [];
AsyncStorage.getItem("setupData").then((value) => {
const setupData = JSON.parse(value) || {};
examScheme.forEach((elem) => {
let currentSection = setupData.excludeDelta
? allQuestions[elem.section].filter((item) => !item.delta)
: allQuestions[elem.section];
for (let i = 0; i < elem.questions; i++) {
const currentIndex = Math.floor(Math.random() * currentSection.length);
tmpQuestions.push(currentSection[currentIndex]);
currentSection = currentSection.filter((_, idx) => idx !== currentIndex);
}
})
});
this.props.navigation.navigate("Splash", {
navigation.navigate("Splash", {
examQuestions: tmpQuestions,
storeWrongAnswers: this.state.storeWrongAnswers
})
storeWrongAnswers,
});
});
return true;
}, [navigation, storeWrongAnswers]);
return true
const showResults = useCallback(() => {
navigation.navigate("Results", {
wrongAnswers: wrongAnswers
})
return true;
}, [navigation, storeWrongAnswers]);
useFocusEffect(
useCallback(() => {
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
handleBackButton
);
return () => backHandler.remove();
}, [handleBackButton])
);
const questions = route.params?.wrongAnswers || [];
const currentResults = route.params
const wrongAnswers = currentResults.wrongAnswers || null
const percentage = currentResults.total ? (100/currentResults.total) * currentResults.correct : 0
let resultStyle = ''//currentResults.points >= 80 ? currentResults.points >= 85 ? styles.correct : styles.unsafe : styles.wrong
let boxStyle = currentResults.points >= 80 ? currentResults.points >= 85 ? styles.boxCorrect : styles.boxUnsafe : styles.boxWrong
if(!currentResults.isExam) {
resultStyle = ''//percentage >= 80 ? percentage >= 85 ? styles.correct : styles.unsafe : styles.wrong
boxStyle = percentage >= 80 ? percentage >= 85 ? styles.boxCorrect : styles.boxUnsafe : styles.boxWrong
}
render() {
const questions = this.props.navigation.getParam("wrongAnswers", [])
return (
<View style={styles.container} >
<View style={styles.headerContainer} >
<Image source={header} style={styles.header} resizeMode="contain" />
</View>
<Text style={styles.textBig}>{texts.recapTitle}</Text>
<ScrollView>
<SafeAreaView style={styles.safearea}>
{questions.map( (question, index) => (
<View style={styles.box} key={question.id}>
<Text style={styles.textCode}>{question.id}</Text>
<Text style={styles.text}>{question.question}</Text>
<ButtonContainer>
{question.answers.map( (answer, index) => (
<Button
key={answer.id}
text={answer.text}
colorize={{id: answer.id, clicked: question.clicked, answered: true, isCorrect: answer.correct}}
/>
))}
</ButtonContainer>
</View>
))}
<View style={styles.button}>
<Button
color={colors.white_alpha2}
hasShadow={true}
hasBg={true}
text={texts.restart}
onPress={() => {this.handleBackButton()}
}
/>
<View style={[styles.box, boxStyle]}>
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.corrects}: ${currentResults.correct}`}</Text>
</Text>
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.wrongs}: ${currentResults.wrong}`}</Text>
</Text>
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.percentage}: ${Math.round(percentage)}%`}</Text>
</Text>
{
currentResults.points ? (
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.points}: ${currentResults.points}/${currentResults.totalPoints}`}</Text>
</Text>
) : null
}
{currentResults.isExam ?
<Text style={[styles.textSmall, resultStyle]}>
{currentResults.points >= 80 ? currentResults.points >= 85 ? texts.exam_passed : texts.exam_needs_oral : texts.exam_not_passed}
</Text> : <Text/>
}
</View>
</SafeAreaView>
<View style={styles.bannerContainer}>
<Banner />
</View>
{wrongAnswers.length ?
<View style={styles.button}>
<Button
color={colors.red_light}
text={texts.recap}
hasBg={true}
onPress={showResults}/>
<Button
isBig={false}
hasBg={true}
color={colors.white_alpha}
text={texts.restart}
onPress={handleBackButton}
/>
</View> :
<View style={styles.button}>
<Button
isBig={false}
hasBg={true}
color={colors.white_alpha}
text={texts.restart}
onPress={handleBackButton}
/>
</View>
}
</SafeAreaView>
</ScrollView>
</View>
)
}
}
};
export default Recap
export default Recap;

200
vds-app/App/screens/RecapExam.js

@ -0,0 +1,200 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, Image, ImageBackground, BackHandler } from "react-native"
import SafeAreaView from 'react-native-safe-area-view'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Button, ButtonContainer } from "../components/Button"
import { Banner } from "../components/Banner"
import { colors, texts, examScheme, credentials } from "../components/Variables"
import aerodynamicsQuestions from "../data/aerodynamics"
import firstAidQuestions from "../data/firstAid"
import flightSafetyQuestions from "../data/flightSafety"
import instrumentsQuestions from "../data/instruments"
import legislationQuestions from "../data/legislation"
import materialsQuestions from "../data/materials"
import meteorologyQuestions from "../data/meteorology"
import physiopathologyQuestions from "../data/physiopathology"
import pilotingTechniquesQuestions from "../data/pilotingTechniques"
const screen = Dimensions.get("window")
const header = require("../assets/header.png")
const bgImage = require("../assets/bg.jpg")
const allQuestions = {
aerodynamics: aerodynamicsQuestions,
firstAid: firstAidQuestions,
flightSafety: flightSafetyQuestions,
instruments: instrumentsQuestions,
legislation: legislationQuestions,
materials: materialsQuestions,
meteorology: meteorologyQuestions,
physiopathology: physiopathologyQuestions,
pilotingTechniques: pilotingTechniquesQuestions
}
const styles = StyleSheet.create({
container: {
backgroundColor: colors.blue,
flex: 1
},
safearea: {
flex: 1,
marginTop: 0,
justifyContent: "space-between",
paddingHorizontal: 20,
paddingBottom: 40
},
headerContainer: {
marginTop: -40,
alignItems: "center",
justifyContent: "center",
width: "100%",
height: screen.width/1.5
},
header: {
width: "100%"
},
box: {
marginTop: 30,
borderColor: colors.black_alpha,
borderWidth: 1,
padding: 15,
borderRadius: 5,
backgroundColor: colors.white_alpha
},
text: {
color: colors.white,
fontSize: 20,
textAlign: "center",
fontWeight: "600",
paddingTop: 0
},
textCode: {
color: colors.white,
fontSize: 12,
textAlign: "center",
fontWeight: "500",
paddingTop: 10,
paddingBottom: 0
},
textBig: {
color: colors.white,
fontSize: 22,
textAlign: "center",
fontWeight: "400",
paddingBottom: 15,
textTransform: "uppercase",
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10
},
bg: {
width: "100%",
height: "100%"
},
bannerContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
}
})
class RecapTrueFalse extends React.Component {
componentDidMount() {
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then((value) => {
//console.log(value)
})
}
componentWillUnmount() {
this.backHandler?.remove()
}
handleBackButton = () => {
let tmpQuestions = []
let fullQuestions = []
examScheme.forEach( (elem) => {
let currentSection = allQuestions[elem.section]
for(let i=0; i<currentSection.length; i++) {
fullQuestions.push(currentSection[i])
}
})
for(let i=0; i<10; i++) {
const currentIndex = Math.floor(Math.random() * fullQuestions.length)
tmpQuestions.push(fullQuestions[currentIndex])
fullQuestions = fullQuestions.filter( (item, index) => index != currentIndex)
}
this.props.navigation.navigate("Splash", {
trueFalseQuestions: tmpQuestions
})
return true
}
render() {
const questions = this.props.route?.params?.wrongAnswers || []
return (
<View style={styles.container} >
<View style={styles.headerContainer} >
<Image source={header} style={styles.header} resizeMode="contain" />
</View>
<Text style={styles.textBig}>{texts.recapTitle}</Text>
<ScrollView>
<SafeAreaView style={styles.safearea}>
{questions.map( (question, index) => (
<View style={styles.box} key={question.id}>
<Text style={styles.textCode}>{question.id}</Text>
<Text style={styles.text}>{question.question}</Text>
<ButtonContainer>
{question.answers.map( (answer, index) => {
if(question.clicked == answer.id) {
return (
<Button
noBorder={true}
key={answer.id}
text={answer.text}
colorize={{id: answer.id, clicked: question.clicked, answered: true, isCorrect: answer.correct}}
/>
)}
}
)}
</ButtonContainer>
</View>
))}
<View style={styles.button}>
<Button
color={colors.white_alpha2}
hasShadow={true}
hasBg={true}
text={texts.restart}
onPress={() => {this.handleBackButton()}
}
/>
</View>
</SafeAreaView>
<View style={styles.bannerContainer}>
<Banner />
</View>
</ScrollView>
</View>
)
}
}
export default RecapTrueFalse

4
vds-app/App/screens/RecapTrueFalse.js

@ -103,14 +103,14 @@ const styles = StyleSheet.create({
class RecapTrueFalse extends React.Component {
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then((value) => {
//console.log(value)
})
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove()
}
handleBackButton = () => {

99
vds-app/App/screens/Results.js

@ -1,25 +1,31 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, Image, BackHandler} from "react-native"
import SafeAreaView from 'react-native-safe-area-view'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Button, ButtonContainer } from "../components/Button"
import { colors, texts, examScheme } from "../components/Variables"
import aerodynamicsQuestions from "../data/aerodynamics"
import firstAidQuestions from "../data/firstAid"
import flightSafetyQuestions from "../data/flightSafety"
import instrumentsQuestions from "../data/instruments"
import legislationQuestions from "../data/legislation"
import materialsQuestions from "../data/materials"
import meteorologyQuestions from "../data/meteorology"
import physiopathologyQuestions from "../data/physiopathology"
import pilotingTechniquesQuestions from "../data/pilotingTechniques"
const screen = Dimensions.get("window")
const header = require("../assets/header.png")
const maxTime = 0 // 10
let interval = null
import React from "react";
import {
View,
ScrollView,
StyleSheet,
Text,
Image,
BackHandler,
Dimensions,
} from "react-native"; // 👈 Add Dimensions here
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation, useRoute, useFocusEffect } from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Button, ButtonContainer } from "../components/Button";
import { Banner } from "../components/Banner";
import { colors, texts, examScheme } from "../components/Variables";
import aerodynamicsQuestions from "../data/aerodynamics";
import firstAidQuestions from "../data/firstAid";
import flightSafetyQuestions from "../data/flightSafety";
import instrumentsQuestions from "../data/instruments";
import legislationQuestions from "../data/legislation";
import materialsQuestions from "../data/materials";
import meteorologyQuestions from "../data/meteorology";
import physiopathologyQuestions from "../data/physiopathology";
import pilotingTechniquesQuestions from "../data/pilotingTechniques";
const allQuestions = {
aerodynamics: aerodynamicsQuestions,
@ -30,8 +36,13 @@ const allQuestions = {
materials: materialsQuestions,
meteorology: meteorologyQuestions,
physiopathology: physiopathologyQuestions,
pilotingTechniques: pilotingTechniquesQuestions
}
pilotingTechniques: pilotingTechniquesQuestions,
};
const header = require("../assets/header.png");
// Get screen dimensions
const screen = Dimensions.get("window");
const maxTime = 0
const styles = StyleSheet.create({
container: {
@ -126,28 +137,31 @@ class Results extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then( (value) => {
const currentResults = this.props.navigation.getParam("results")
const wrongAnswers = currentResults.wrongAnswers || []
const stored = JSON.parse(value) || []
const result = currentResults.isWrong ? wrongAnswers : Object.assign([], wrongAnswers, stored);
AsyncStorage.setItem('storeWrongAnswers', JSON.stringify(result))
this.setState( (state) => {
return {
storeWrongAnswers: result
}
})
this.backHandler = BackHandler.addEventListener(
"hardwareBackPress",
this.handleBackButton
);
})
AsyncStorage.getItem("storeWrongAnswers").then((value) => {
const currentResults = this.props.route.params.results;
const wrongAnswers = currentResults.wrongAnswers || [];
const stored = JSON.parse(value) || [];
const result = currentResults.isWrong
? wrongAnswers
: Object.assign([], wrongAnswers, stored);
AsyncStorage.setItem("storeWrongAnswers", JSON.stringify(result));
this.setState({ storeWrongAnswers: result });
});
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
if (this.backHandler) { this.backHandler.remove(); }
}
handleBackButton = () => {
let tmpQuestions = []
@ -176,7 +190,8 @@ class Results extends React.Component {
render() {
const currentResults = this.props.navigation.getParam("results")
const currentResults = this.props.route?.params?.results || [];
const wrongAnswers = currentResults.wrongAnswers || null
const percentage = currentResults.total ? (100/currentResults.total) * currentResults.correct : 0
let resultStyle = ''//currentResults.points >= 80 ? currentResults.points >= 85 ? styles.correct : styles.unsafe : styles.wrong
@ -227,7 +242,7 @@ class Results extends React.Component {
text={texts.recap}
hasBg={true}
onPress={ ()=> {
this.props.navigation.navigate("Recap", {
this.props.navigation.navigate("RecapExam", {
wrongAnswers: wrongAnswers
})
}}/>

243
vds-app/App/screens/ResultsTrueFalse.js

@ -1,9 +1,10 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, Image, BackHandler} from "react-native"
import SafeAreaView from 'react-native-safe-area-view'
import React from "react";
import { View, ScrollView, StyleSheet, Text, Image, BackHandler } from "react-native";
import SafeAreaView from 'react-native-safe-area-view';
import { Button, ButtonContainer } from "../components/Button"
import { colors, texts, examScheme } from "../components/Variables"
import { Button, ButtonContainer } from "../components/Button";
import { Banner } from "../components/Banner";
import { colors, texts, examScheme } from "../components/Variables";
import aerodynamicsQuestions from "../data/aerodynamics"
import firstAidQuestions from "../data/firstAid"
@ -15,11 +16,6 @@ import meteorologyQuestions from "../data/meteorology"
import physiopathologyQuestions from "../data/physiopathology"
import pilotingTechniquesQuestions from "../data/pilotingTechniques"
const screen = Dimensions.get("window")
const header = require("../assets/header.png")
const maxTime = 0 // 10
let interval = null
const allQuestions = {
aerodynamics: aerodynamicsQuestions,
firstAid: firstAidQuestions,
@ -30,205 +26,94 @@ const allQuestions = {
meteorology: meteorologyQuestions,
physiopathology: physiopathologyQuestions,
pilotingTechniques: pilotingTechniquesQuestions
}
};
const header = require("../assets/header.png");
const styles = StyleSheet.create({
container: {
backgroundColor: colors.dark_blue,
flex: 1
},
safearea: {
flex: 1,
marginTop: 0,
justifyContent: "space-between",
paddingHorizontal: 20,
paddingBottom: 40
},
headerContainer: {
marginTop: -40,
alignItems: "center",
justifyContent: "center",
width: "100%",
height: screen.width/1.5
},
header: {
width: "100%"
},
box: {
marginTop: 20,
marginBottom: 20,
marginHorizontal: 20,
width: screen.width-80,
borderRadius: 10,
borderBottomEndRadius: 80,
borderTopStartRadius: 100,
backgroundColor: colors.white_alpha,
borderColor: colors.white,
borderWidth: 2,
paddingVertical: 30
},
boxCorrect: {
backgroundColor: colors.green_light,
},
boxWrong: {
backgroundColor: colors.red,
},
boxUnsafe: {
backgroundColor: colors.orange,
},
button: {
width: screen.width-80,
marginHorizontal: 20
},
text: {
color: colors.white,
fontSize: 22,
textAlign: "center",
fontWeight: "400",
lineHeight: 40,
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10
},
textSmall: {
color: colors.white,
marginTop: 20,
fontSize: 26,
textAlign: "center",
fontWeight: "500",
lineHeight: 30,
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10
},
textLabel: {
paddingHorizontal: 20,
paddingVertical: 20
},
correct: {
color: colors.green
},
wrong: {
color: colors.red
},
unsafe: {
color: colors.yellow
}
})
container: { backgroundColor: colors.blue, flex: 1 },
safearea: { flex: 1, marginTop: 0, justifyContent: "space-between", paddingHorizontal: 20, paddingBottom: 40 },
headerContainer: { marginTop: -40, alignItems: "center", justifyContent: "center", width: "100%", height: 200 },
header: { width: "100%" },
box: { marginTop: 30, borderColor: colors.black_alpha, borderWidth: 1, padding: 15, borderRadius: 5, backgroundColor: colors.white_alpha },
text: { color: colors.white, fontSize: 20, textAlign: "center", fontWeight: "600" },
textCode: { color: colors.white, fontSize: 12, textAlign: "center", fontWeight: "500", paddingTop: 10 },
textBig: { color: colors.white, fontSize: 22, textAlign: "center", fontWeight: "400", paddingBottom: 15, textTransform: "uppercase", textShadowColor: 'rgba(0,0,0,0.75)', textShadowOffset: {width:-1, height:1}, textShadowRadius:10 },
bannerContainer: { flex: 1, alignItems: "center", justifyContent: "center" }
});
class Results extends React.Component {
state = {
bannerExpanded: true,
timer: maxTime
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove();
}
handleBackButton = () => {
const tmpQuestions = [];
let fullQuestions = [];
let tmpQuestions = []
let fullQuestions = []
examScheme.forEach(elem => {
fullQuestions.push(...allQuestions[elem.section]);
});
examScheme.forEach( (elem) => {
let currentSection = allQuestions[elem.section]
for(let i=0; i<currentSection.length; i++) {
fullQuestions.push(currentSection[i])
}
})
for(let i=0; i<10; i++) {
const currentIndex = Math.floor(Math.random() * fullQuestions.length)
tmpQuestions.push(fullQuestions[currentIndex])
fullQuestions = fullQuestions.filter( (item, index) => index != currentIndex)
for (let i = 0; i < 10; i++) {
const index = Math.floor(Math.random() * fullQuestions.length);
tmpQuestions.push(fullQuestions[index]);
fullQuestions.splice(index, 1);
}
this.props.navigation.navigate("Splash", {
trueFalseQuestions: tmpQuestions
})
return true
this.props.navigation.navigate("Splash", { trueFalseQuestions: tmpQuestions });
return true;
}
render() {
const currentResults = this.props.navigation.getParam("results")
const wrongAnswers = currentResults.wrongAnswers || null
const percentage = currentResults.total ? (100/currentResults.total) * currentResults.correct : 0
let resultStyle = ''
let boxStyle = currentResults.points >= 80 ? currentResults.points >= 85 ? styles.boxCorrect : styles.boxUnsafe : styles.boxWrong
if(!currentResults.isExam) {
resultStyle = ''
boxStyle = percentage >= 80 ? percentage >= 85 ? styles.boxCorrect : styles.boxUnsafe : styles.boxWrong
}
//console.log(currentResults)
const questions = this.props.route?.params?.results?.wrongAnswers || [];
return (
<View style={styles.container} >
<View style={styles.headerContainer} >
<View style={styles.container}>
<View style={styles.headerContainer}>
<Image source={header} style={styles.header} resizeMode="contain" />
</View>
<Text style={styles.textBig}>{texts.recapTitle}</Text>
<ScrollView>
<SafeAreaView style={styles.safearea}>
<View style={[styles.box, boxStyle]}>
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.corrects}: ${currentResults.correct}`}</Text>
</Text>
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.wrongs}: ${currentResults.wrong}`}</Text>
</Text>
<Text style={styles.text}>
<Text style={styles.textLabel}>{`${texts.percentage}: ${Math.round(percentage)}%`}</Text>
</Text>
</View>
{wrongAnswers.length ?
<View style={styles.button}>
<Button
color={colors.red_light}
text={texts.recap}
hasBg={true}
onPress={ ()=> {
this.props.navigation.navigate("RecapTrueFalse", {
wrongAnswers: wrongAnswers
})
}}/>
<Button
isBig={false}
hasBg={true}
color={colors.white_alpha}
text={texts.restart}
onPress={() => {this.handleBackButton()}
}
/>
</View> :
<View style={styles.button}>
<Button
isBig={false}
hasBg={true}
color={colors.white_alpha}
text={texts.restart}
onPress={() => {this.handleBackButton()}
}
/>
{questions.map(q => (
<View style={styles.box} key={q.id}>
<Text style={styles.textCode}>{q.id}</Text>
<Text style={styles.text}>{q.question}</Text>
<ButtonContainer>
{q.answers.map(ans => (
q.clicked === ans.id && (
<Button
key={ans.id}
text={ans.text}
noBorder
colorize={{id: ans.id, clicked: q.clicked, answered: true, isCorrect: ans.correct}}
/>
)
))}
</ButtonContainer>
</View>
}
))}
<View style={{ marginTop: 20 }}>
<Button color={colors.white_alpha2} hasBg hasShadow text={texts.restart} onPress={this.handleBackButton} />
</View>
</SafeAreaView>
</ScrollView>
<View style={styles.bannerContainer}>
<Banner />
</View>
</ScrollView>
</View>
)
);
}
}
export default Results
export default Results;

4
vds-app/App/screens/Setup.js

@ -137,7 +137,7 @@ class Setup extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('setupData').then((value) => {
let setupData = {}
@ -161,7 +161,7 @@ class Setup extends React.Component {
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove()
}
handleBackButton = () => {

317
vds-app/App/screens/Splash.js

@ -1,78 +1,39 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, Image, Alert, BackHandler } from "react-native"
import AsyncStorage from '@react-native-async-storage/async-storage'
import SafeAreaView from 'react-native-safe-area-view'
import { Button, ButtonContainer } from "../components/Button"
import { Banner } from "../components/Banner"
import { colors, texts, credentials } from "../components/Variables"
import { examQuestions } from "../components/ExamQuestions"
import { trueFalseQuestions } from "../components/TrueFalseQuestions"
const screen = Dimensions.get("window")
const header = require("../assets/header.png")
const pkg = require('../../app.json')
const maxTime = 0 // 10
let interval = null
import React from "react";
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, Image, Alert, BackHandler } from "react-native";
import AsyncStorage from '@react-native-async-storage/async-storage';
import SafeAreaView from 'react-native-safe-area-view';
import { Button, ButtonContainer } from "../components/Button";
import { Banner } from "../components/Banner";
import { colors, texts } from "../components/Variables";
import { examQuestions } from "../components/ExamQuestions";
import { trueFalseQuestions } from "../components/TrueFalseQuestions";
const screen = Dimensions.get("window");
const header = require("../assets/header.png");
const maxTime = 0; // 10
let interval = null;
const styles = StyleSheet.create({
container: {
backgroundColor: colors.dark_blue,
flex: 1
},
title: {
color: colors.white,
fontSize: 25,
textAlign: "center",
fontWeight: "600",
paddingVertical: 20
},
text: {
color: colors.white,
fontSize: 20,
textAlign: "center",
fontWeight: "400",
paddingVertical: 20,
marginTop: 20,
},
timer: {
color: colors.white,
fontSize: 30,
textAlign: "center",
fontWeight: "600",
paddingVertical: 20,
marginBottom: 20,
},
safearea: {
flex: 1,
marginTop: 0,
justifyContent: "space-between",
paddingHorizontal: 20
},
headerContainer: {
marginTop: 20,
alignItems: "center",
justifyContent: "center",
width: "100%",
height: 150
},
header: {
width: "100%"
},
bannerContainer: {
flex: 1,
alignItems: "center",
justifyContent: "center"
}
})
container: { backgroundColor: colors.dark_blue, flex: 1 },
safearea: { flex: 1, justifyContent: "space-between", paddingHorizontal: 20 },
headerContainer: { marginTop: 20, alignItems: "center", justifyContent: "center", width: "100%", height: 150 },
header: { width: "100%" },
bannerContainer: { flex: 1, alignItems: "center", justifyContent: "center" }
});
class Splash extends React.Component {
constructor(props) {
super(props);
const params = props.route.params || {};
state = {
bannerExpanded: true,
timer: maxTime,
storeWrongAnswers: this.props.navigation.getParam("storeWrongAnswers", []) || [],
setupData: {}
this.state = {
bannerExpanded: true,
timer: maxTime,
storeWrongAnswers: params.storeWrongAnswers || [],
setupData: {}
};
}
bannerError = (e) => {
@ -80,73 +41,65 @@ class Splash extends React.Component {
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then( (value) => {
if(!value) {
AsyncStorage.setItem('storeWrongAnswers', JSON.stringify([]))
}
AsyncStorage.getItem('setupData').then((setup) => {
if(!setup) {
AsyncStorage.setItem('setupData', JSON.stringify({}))
}
this.setState( (state) => {
return {
storeWrongAnswers: value ? JSON.parse(value) : [],
setupData: setup ? JSON.parse(setup) : {}
}
})
})
})
// Save subscription for removal later
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
AsyncStorage.getItem('storeWrongAnswers').then((value) => {
if (!value) AsyncStorage.setItem('storeWrongAnswers', JSON.stringify([]));
AsyncStorage.getItem('setupData').then((setup) => {
if (!setup) AsyncStorage.setItem('setupData', JSON.stringify({}));
this.setState({
storeWrongAnswers: value ? JSON.parse(value) : [],
setupData: setup ? JSON.parse(setup) : {}
});
});
});
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
// Remove subscription correctly
this.backHandler?.remove();
if (interval) clearInterval(interval);
}
handleBackButton = () => {
Alert.alert(
texts.exit,
texts.exitQuestion,
[
{text: 'No', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
{text: 'Si', onPress: () => BackHandler.exitApp()},
{ text: 'No', onPress: () => console.log('Cancel Pressed'), style: 'cancel' },
{ text: 'Si', onPress: () => BackHandler.exitApp() },
],
{ cancelable: false }
)
return true
);
return true;
}
render() {
const storeWrongAnswers = this.props.navigation.getParam("storeWrongAnswers") || this.state.storeWrongAnswers
if(this.state.timer==maxTime) {
interval = setInterval( () => {
this.setState( (state) => {
return {
timer: this.state.timer-1,
}
})
}, 1000)
startTimer = () => {
if (this.state.timer === maxTime) {
interval = setInterval(() => {
this.setState((state) => ({ timer: state.timer - 1 }));
}, 1000);
}
if(this.state.timer < 1) {
clearInterval(interval)
setTimeout( () => {
this.setState( (state) => {
return {
bannerExpanded: false
}
})
}, 500)
if (this.state.timer < 1 && interval) {
clearInterval(interval);
interval = null;
setTimeout(() => this.setState({ bannerExpanded: false }), 500);
}
}
render() {
const { storeWrongAnswers } = this.state;
this.startTimer();
return (
<ScrollView style={styles.container} >
<View style={styles.headerContainer} >
<ScrollView style={styles.container}>
<View style={styles.headerContainer}>
<Image source={header} style={styles.header} resizeMode="contain" />
</View>
@ -157,127 +110,123 @@ class Splash extends React.Component {
text={texts.section_quizzes}
subtitle={`(${texts.section_quizzes_subtitle})`}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() =>
this.props.navigation.navigate("QuizIndex", {
title: texts.section_quizzes,
color: colors.white_alpha
})}
onPress={() => this.props.navigation.navigate("QuizIndex", {
title: texts.section_quizzes,
color: colors.white_alpha
})}
/>
<Button
text={texts.exam}
subtitle={`(${texts.exam_simulation})`}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() =>
this.props.navigation.navigate("Exam", {
title: texts.exam,
questions: this.props.navigation.getParam("examQuestions") || examQuestions,
color: colors.white_alpha
})}
onPress={() => this.props.navigation.navigate("Exam", {
title: texts.exam,
questions: this.props.route.params?.examQuestions || examQuestions,
color: colors.white_alpha
})}
/>
{
storeWrongAnswers.length ? (
<Button
text={texts.wrong_review}
subtitle={`(${storeWrongAnswers.length} ${texts.wrong_title})`}
isBig={false}
hasBg={true}
noPadding={false}
hasShadow={true}
noBorder={true}
color={colors.white_alpha}
onPress={() =>
this.props.navigation.navigate("Quiz", {
title: texts.wrong_review,
questions: storeWrongAnswers,
isWrong: true,
color: colors.blue
})}
/>
) : null
}
{storeWrongAnswers.length ? (
<Button
text={texts.wrong_review}
subtitle={`(${storeWrongAnswers.length} ${texts.wrong_title})`}
isBig={false}
hasBg
noPadding={false}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() => this.props.navigation.navigate("Quiz", {
title: texts.wrong_review,
questions: storeWrongAnswers,
isWrong: true,
color: colors.blue
})}
/>
) : null}
<Button
text={texts.trueFalse}
subtitle={`(${texts.trueFalseSubtitle})`}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() =>
this.props.navigation.navigate("TrueFalse", {
title: texts.trueFalse,
questions: this.props.navigation.getParam("trueFalseQuestions") || trueFalseQuestions,
color: colors.white_alpha
})}
onPress={() => this.props.navigation.navigate("TrueFalse", {
title: texts.trueFalse,
questions: this.props.route.params?.trueFalseQuestions || trueFalseQuestions,
color: colors.white_alpha
})}
/>
<Button
text={texts.dictionaryTitle}
subtitle={`(${texts.dictionarySubtitle})`}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() => this.props.navigation.navigate("Dictionary", {})}
onPress={() => this.props.navigation.navigate("Dictionary")}
/>
<Button
text={texts.setupTitle}
subtitle={`(${texts.setupSubtitle})`}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() => this.props.navigation.navigate("Setup", {})}
onPress={() => this.props.navigation.navigate("Setup")}
/>
<Button
text={texts.infoTitle}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha}
onPress={() => this.props.navigation.navigate("Info", {})}
onPress={() => this.props.navigation.navigate("Info")}
/>
<Button
text={texts.exit}
isBig={false}
hasBg={true}
hasBg
noPadding={false}
hasShadow={true}
noBorder={true}
hasShadow
noBorder
color={colors.white_alpha2}
onPress={() => this.handleBackButton()}
onPress={this.handleBackButton}
/>
</ButtonContainer>
</View>
</SafeAreaView>
<View style={styles.bannerContainer}>
<Banner />
</View>
</ScrollView>
)
);
}
}
export default Splash
export default Splash;

215
vds-app/App/screens/TrueFalse.js

@ -1,10 +1,10 @@
import React from "react"
import { View, ScrollView, StyleSheet, StatusBar, Text, Dimensions, ImageBackground, BackHandler } from "react-native"
import SafeAreaView from 'react-native-safe-area-view'
import React from "react";
import { View, ScrollView, StyleSheet, Text, ImageBackground, Dimensions, BackHandler } from "react-native";
import SafeAreaView from 'react-native-safe-area-view';
import { Button, ButtonContainer } from "../components/Button"
import { Banner } from "../components/Banner"
import { texts, colors, examScheme, credentials } from "../components/Variables"
import { Button, ButtonContainer } from "../components/Button";
import { Banner } from "../components/Banner";
import { texts, colors, examScheme } from "../components/Variables";
import aerodynamicsQuestions from "../data/aerodynamics"
import firstAidQuestions from "../data/firstAid"
@ -26,10 +26,11 @@ const allQuestions = {
meteorology: meteorologyQuestions,
physiopathology: physiopathologyQuestions,
pilotingTechniques: pilotingTechniquesQuestions
}
};
const bgImage = require("../assets/bg.jpg")
const screen = Dimensions.get("window")
const bgImage = require("../assets/bg.jpg");
const screen = Dimensions.get("window");
const styles = StyleSheet.create({
container: {
@ -54,7 +55,7 @@ const styles = StyleSheet.create({
textAnswer: {
color: colors.white,
backgroundColor: colors.black_alpha,
borderColor: colors.white,
borderColor: colors.white,
borderWidth: 2,
fontSize: 18,
textAlign: "center",
@ -91,175 +92,111 @@ const styles = StyleSheet.create({
}
})
class Quiz extends React.Component {
class Quiz extends React.Component {
state = {
correctCount: 0,
wrongCount: 0,
wrongAnswers: [],
totalCount: this.props.navigation.getParam("questions", []).length,
availableIds: this.props.navigation.getParam("questions", []).map(a => a.id),
activeQuestionId: this.props.navigation.getParam("questions", [])[
Math.floor(Math.random() * this.props.navigation.getParam("questions", []).length)
].id,
activeAnswerId: Math.floor(Math.random() * 3),
availableIds: [],
activeQuestionId: null,
activeAnswerId: 0,
clickedAnswer: false,
answered: false,
results: false
}
bannerError = (e) => {
//console.log("Banner error (footer): ", e)
}
answered: false
};
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler = BackHandler.addEventListener( 'hardwareBackPress', this.handleBackButton)
const questions = this.props.route?.params?.questions || [];
this.setState({
availableIds: questions.map(q => q.id),
activeQuestionId: questions.length ? questions[Math.floor(Math.random() * questions.length)].id : null
});
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButton)
this.backHandler?.remove();
}
handleBackButton = () => {
const tmpQuestions = [];
let fullQuestions = [];
let tmpQuestions = []
let fullQuestions = []
examScheme.forEach(elem => fullQuestions.push(...allQuestions[elem.section]));
examScheme.forEach( (elem) => {
let currentSection = allQuestions[elem.section]
for(let i=0; i<currentSection.length; i++) {
fullQuestions.push(currentSection[i])
}
})
for(let i=0; i<10; i++) {
const currentIndex = Math.floor(Math.random() * fullQuestions.length)
tmpQuestions.push(fullQuestions[currentIndex])
fullQuestions = fullQuestions.filter( (item, index) => index != currentIndex)
for (let i = 0; i < 10; i++) {
const idx = Math.floor(Math.random() * fullQuestions.length);
tmpQuestions.push(fullQuestions[idx]);
fullQuestions.splice(idx, 1);
}
this.props.navigation.navigate("Splash", {
trueFalseQuestions: tmpQuestions
})
return true
this.props.navigation.navigate("Splash", { trueFalseQuestions: tmpQuestions });
return true;
}
answer = (answer, correct, id, question) => {
this.setState(
state => {
const nextState = { answered: true, clickedId: id, clickedAnswer: answer }
if ((correct && answer) || (!correct && !answer)) {
//console.log('ok')
nextState.correctCount = state.correctCount + 1
} else {
//console.log('ko')
nextState.wrongCount = state.wrongCount + 1
nextState.wrongAnswers = state.wrongAnswers
nextState.wrongAnswers.push(
{ question: question.question,
id: question.id,
clicked: question.answers[state.activeAnswerId].id,
answers: question.answers
}
)
}
return nextState
},
() => {
setTimeout(() => this.nextQuestion(), 2000)
this.setState(state => {
const nextState = { answered: true, clickedId: id, clickedAnswer: answer };
if ((correct && answer) || (!correct && !answer)) {
nextState.correctCount = state.correctCount + 1;
} else {
nextState.wrongCount = state.wrongCount + 1;
nextState.wrongAnswers = [...state.wrongAnswers, { question: question.question, id: question.id, clicked: question.answers[state.activeAnswerId].id, answers: question.answers }];
}
)
return nextState;
}, () => setTimeout(this.nextQuestion, 2000));
}
nextQuestion = () => {
const updatedIndexes = this.state.availableIds.filter( item => item != this.state.activeQuestionId)
const nextId = updatedIndexes[Math.floor(Math.random() * updatedIndexes.length)]
const updatedIndexes = this.state.availableIds.filter(id => id !== this.state.activeQuestionId);
if (!updatedIndexes.length) {
//console.log(this.state.wrongAnswers)
this.props.navigation.navigate("ResultsTrueFalse", {
results: {
isExam: false,
total: this.state.totalCount,
total: this.state.availableIds.length,
correct: this.state.correctCount,
wrong: this.state.wrongCount,
wrongAnswers: this.state.wrongAnswers
}
})
} else {
this.setState( (state) => {
return {
availableIds: updatedIndexes,
activeQuestionId: nextId,
activeAnswerId: Math.floor(Math.random() * 3),
answered: false
}
})
});
return;
}
const nextId = updatedIndexes[Math.floor(Math.random() * updatedIndexes.length)];
this.setState({ availableIds: updatedIndexes, activeQuestionId: nextId, activeAnswerId: Math.floor(Math.random() * 3), answered: false });
}
render() {
const questions = this.props.navigation.getParam("questions", [])
const question = questions.filter(item => item.id == this.state.activeQuestionId)[0] || questions[0]
const randomAnswer = question.answers[this.state.activeAnswerId]
//console.log({id: randomAnswer.id, clicked: this.state.clickedId, answered: this.state.answered, isCorrect: randomAnswer.correct || false})
const questions = this.props.route.params?.questions || [];
const question = questions.find(q => q.id === this.state.activeQuestionId) || questions[0];
const randomAnswer = question?.answers[this.state.activeAnswerId];
return (
<ImageBackground source={bgImage} style={styles.bg} resizeMode="cover">
<View style={styles.box}>
<View style={styles.scrollView}>
<ScrollView style={ styles.container } >
<StatusBar barStyle="light-content" />
{!this.state.results ?
<SafeAreaView style={styles.safearea}>
<View>
<Text style={styles.textCode}>{question.id}</Text>
<Text style={styles.text}>{question.question}</Text>
<Text style={styles.textAnswer}>{randomAnswer.text}</Text>
<ButtonContainer>
<Button
halfSize={true}
text={texts.true}
noBorder={true}
colorize={{id: randomAnswer.id, clicked: this.state.clickedAnswer ? randomAnswer.id : false, answered: this.state.answered, isCorrect: randomAnswer.correct || false}}
onPress={() => this.answer(true, randomAnswer.correct || false, randomAnswer.id, question)}
/>
<Button
halfSize={true}
text={texts.false}
noBorder={true}
colorize={{id: randomAnswer.id, clicked: !this.state.clickedAnswer ? randomAnswer.id : false, answered: this.state.answered, isCorrect: !randomAnswer.correct}}
onPress={() => this.answer(false, randomAnswer.correct || false, randomAnswer.id, question)}
/>
</ButtonContainer>
</View>
<Text style={styles.text}>
{`${this.state.correctCount+this.state.wrongCount}/${this.state.totalCount}`}
</Text>
</SafeAreaView>
: <SafeAreaView></SafeAreaView>}
</ScrollView>
</View>
</View>
<View style={styles.bannerContainer}>
<Banner />
</View>
<ImageBackground source={bgImage} style={{ flex: 1 }} resizeMode="cover">
<ScrollView>
<SafeAreaView style={styles.safearea}>
<Text style={styles.textCode}>{question?.id}</Text>
<Text style={styles.text}>{question?.question}</Text>
<Text style={styles.textAnswer}>{randomAnswer?.text}</Text>
<ButtonContainer>
<Button
colorize={{id: randomAnswer.id, clicked: this.state.clickedAnswer ? randomAnswer.id : false, answered: this.state.answered, isCorrect: randomAnswer.correct || false}}
text={texts.true} onPress={() => this.answer(true, randomAnswer?.correct, randomAnswer?.id, question)} />
<Button
colorize={{id: randomAnswer.id, clicked: !this.state.clickedAnswer ? randomAnswer.id : false, answered: this.state.answered, isCorrect: !randomAnswer.correct}}
text={texts.false} onPress={() => this.answer(false, randomAnswer?.correct, randomAnswer?.id, question)} />
</ButtonContainer>
<Text style={styles.text}>{`${this.state.correctCount + this.state.wrongCount}/${10/*this.state.availableIds.length*/}`}</Text>
</SafeAreaView>
</ScrollView>
<Banner />
</ImageBackground>
)
);
}
}
export default Quiz
export default Quiz;

16
vds-app/android/.gitignore

@ -0,0 +1,16 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# Bundle artifacts
*.jsbundle

182
vds-app/android/app/build.gradle

@ -0,0 +1,182 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
*/
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace 'com.dslak.vdsquiz'
defaultConfig {
applicationId 'com.dslak.vdsquiz'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 35
versionName "3.9.0"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
shrinkResources enableShrinkResources.toBoolean()
minifyEnabled enableMinifyInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
crunchPngs enablePngCrunchInRelease.toBoolean()
}
}
packagingOptions {
jniLibs {
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
useLegacyPackaging enableLegacyPackaging.toBoolean()
}
}
androidResources {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
}
}
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}

BIN
vds-app/android/app/debug.keystore

Binary file not shown.

14
vds-app/android/app/proguard-rules.pro

@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

7
vds-app/android/app/src/debug/AndroidManifest.xml

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

7
vds-app/android/app/src/debugOptimized/AndroidManifest.xml

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

36
vds-app/android/app/src/main/AndroidManifest.xml

@ -0,0 +1,36 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3940256099942544~3347511713" tools:replace="android:value"/>
<meta-data android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT" android:value="false" tools:replace="android:value"/>
<meta-data android:name="com.google.android.gms.ads.flag.OPTIMIZE_AD_LOADING" android:value="true" tools:replace="android:value"/>
<meta-data android:name="com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATION" android:value="true" tools:replace="android:value"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="10"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://u.expo.dev/7a0112f0-f4a6-11e9-b7eb-0ba61596acb6"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="exp+vds-quiz"/>
</intent-filter>
</activity>
</application>
</manifest>

61
vds-app/android/app/src/main/java/com/dslak/vdsquiz/MainActivity.kt

@ -0,0 +1,61 @@
package com.dslak.vdsquiz
import android.os.Build
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
super.onCreate(null)
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
){})
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
}

56
vds-app/android/app/src/main/java/com/dslak/vdsquiz/MainApplication.kt

@ -0,0 +1,56 @@
package com.dslak.vdsquiz
import android.app.Application
import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.common.ReleaseLevel
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactNativeHost
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
}
)
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
DefaultNewArchitectureEntryPoint.releaseLevel = try {
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
} catch (e: IllegalArgumentException) {
ReleaseLevel.STABLE
}
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

BIN
vds-app/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
vds-app/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
vds-app/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
vds-app/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
vds-app/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

6
vds-app/android/app/src/main/res/drawable/ic_launcher_background.xml

@ -0,0 +1,6 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
<item>
<bitmap android:gravity="center" android:src="@drawable/splashscreen_logo"/>
</item>
</layer-list>

37
vds-app/android/app/src/main/res/drawable/rn_edit_text_material.xml

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

BIN
vds-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
vds-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
vds-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
vds-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
vds-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
vds-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
vds-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
vds-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
vds-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
vds-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

1
vds-app/android/app/src/main/res/values-night/colors.xml

@ -0,0 +1 @@
<resources/>

6
vds-app/android/app/src/main/res/values/colors.xml

@ -0,0 +1,6 @@
<resources>
<color name="splashscreen_background">#8c0072</color>
<color name="iconBackground">#ffffff</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#8c0072</color>
</resources>

5
vds-app/android/app/src/main/res/values/strings.xml

@ -0,0 +1,5 @@
<resources>
<string name="app_name">VDS Quiz</string>
<string name="expo_splash_screen_resize_mode" translatable="false">cover</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources>

11
vds-app/android/app/src/main/res/values/styles.xml

@ -0,0 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">true</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#8c0072</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>
</resources>

24
vds-app/android/build.gradle

@ -0,0 +1,24 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"

69
vds-app/android/gradle.properties

@ -0,0 +1,69 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=true
# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=true
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=true
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn't support animated webp
expo.webp.animated=false
# Enable network inspector
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false
# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
expo.edgeToEdgeEnabled=true
android.compileSdkVersion=35
android.targetSdkVersion=35
android.buildToolsVersion=35.0.0

BIN
vds-app/android/gradle/wrapper/gradle-wrapper.jar

Binary file not shown.

7
vds-app/android/gradle/wrapper/gradle-wrapper.properties

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
vds-app/android/gradlew

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
vds-app/android/gradlew.bat

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

39
vds-app/android/settings.gradle

@ -0,0 +1,39 @@
pluginManagement {
def reactNativeGradlePlugin = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
}.standardOutput.asText.get().trim()
).getParentFile().absolutePath
includeBuild(reactNativeGradlePlugin)
def expoPluginsPath = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
}.standardOutput.asText.get().trim(),
"../android/expo-gradle-plugin"
).absolutePath
includeBuild(expoPluginsPath)
}
plugins {
id("com.facebook.react.settings")
id("expo-autolinking-settings")
}
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
}
expoAutolinking.useExpoModules()
rootProject.name = 'VDS Quiz'
expoAutolinking.useExpoVersionCatalog()
include ':app'
includeBuild(expoAutolinking.reactNativeGradlePlugin)

49
vds-app/app.json

@ -1,16 +1,13 @@
{
"displayName": "VDS Quiz",
"name": "VDS Quiz",
"expo": {
"name": "VDS Quiz",
"slug": "VDS-Quiz",
"privacy": "public",
"platforms": [
"ios",
"android",
"web"
],
"version": "3.8.3",
"version": "3.9.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
@ -27,9 +24,8 @@
],
"android": {
"icon": "./assets/icon.png",
"package": "com.dslak.vdsquiz",
"permissions": [],
"versionCode": 30
"versionCode": 35
},
"ios": {
"icon": "./assets/iconIOS.png",
@ -39,34 +35,37 @@
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "7a0112f0-f4a6-11e9-b7eb-0ba61596acb6"
}
},
"plugins": [
[
"expo-build-properties",
{
"android": {
"compileSdkVersion": 33,
"targetSdkVersion": 33,
"buildToolsVersion": "33.0.0"
"compileSdkVersion": 35,
"targetSdkVersion": 35,
"buildToolsVersion": "35.0.0"
},
"ios": {
"deploymentTarget": "13.0"
"deploymentTarget": "15.1"
}
}
],
[
"react-native-google-mobile-ads",
{
"androidAppId": "ca-app-pub-3940256099942544~3347511713",
"iosAppId": "ca-app-pub-3940256099942544~1458002511",
"delayAppMeasurementInit": false,
"optimizeInitialization": true,
"optimizeAdLoading": true,
"userTrackingUsageDescription": "This identifier will be used to deliver personalized ads to you."
}
]
]
},
"react-native-google-mobile-ads": {
"android_app_id": "ca-app-pub-3940256099942544~3347511713",
"ios_app_id": "ca-app-pub-3940256099942544~1458002511",
"delay_app_measurement_init": false,
"optimize_initialization": true,
"optimize_ad_loading": true,
"user_tracking_usage_description": "This identifier will be used to deliver personalized ads to you."
],
"owner": "dslaky",
"extra": {
"eas": {
"projectId": "7a0112f0-f4a6-11e9-b7eb-0ba61596acb6"
}
}
}
}

BIN
vds-app/builds/application-5340e044-a24c-439e-a724-182a853ebd65.aab

Binary file not shown.

1
vds-app/dist/assetmap.json

@ -1 +0,0 @@
{"c90fb4585dd852a3d67af39baf923f67":{"__packager_asset":true,"fileSystemLocation":"/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets","httpServerLocation":"/assets/node_modules/react-navigation-stack/lib/module/vendor/views/assets","width":12,"height":21,"scales":[1,1.5,2,3,4],"files":["/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@1x.ios.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@1.5x.ios.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@2x.ios.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@3x.ios.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@4x.ios.png"],"hash":"c90fb4585dd852a3d67af39baf923f67","name":"back-icon","type":"png","fileHashes":["7d40544b395c5949f4646f5e150fe020","cdd04e13d4ec83ff0cd13ec8dabdc341","a132ecc4ba5c1517ff83c0fb321bc7fc","0ea69b5077e7c4696db85dbcba75b0e1","f5b790e2ac193b3d41015edb3551f9b8"]},"5223c8d9b0d08b82a5670fb5f71faf78":{"__packager_asset":true,"fileSystemLocation":"/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets","httpServerLocation":"/assets/node_modules/react-navigation-stack/lib/module/vendor/views/assets","width":50,"height":85,"scales":[1],"files":["/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon-mask.png"],"hash":"5223c8d9b0d08b82a5670fb5f71faf78","name":"back-icon-mask","type":"png","fileHashes":["5223c8d9b0d08b82a5670fb5f71faf78"]},"319b7d9681027a1ef8d3fb9520d73a17":{"__packager_asset":true,"fileSystemLocation":"/home/dslak/Desktop/vds_quiz/vds-app/App/assets","httpServerLocation":"/assets/App/assets","width":1553,"height":500,"scales":[1],"files":["/home/dslak/Desktop/vds_quiz/vds-app/App/assets/buttonBg.png"],"hash":"319b7d9681027a1ef8d3fb9520d73a17","name":"buttonBg","type":"png","fileHashes":["319b7d9681027a1ef8d3fb9520d73a17"]},"e6b3e73c7c9600ce768eba302c6ad054":{"__packager_asset":true,"fileSystemLocation":"/home/dslak/Desktop/vds_quiz/vds-app/App/assets","httpServerLocation":"/assets/App/assets","width":1553,"height":938,"scales":[1],"files":["/home/dslak/Desktop/vds_quiz/vds-app/App/assets/header.png"],"hash":"e6b3e73c7c9600ce768eba302c6ad054","name":"header","type":"png","fileHashes":["e6b3e73c7c9600ce768eba302c6ad054"]},"4ade0cd9affb7de397ece858c4ef9212":{"__packager_asset":true,"fileSystemLocation":"/home/dslak/Desktop/vds_quiz/vds-app/App/assets","httpServerLocation":"/assets/App/assets","width":800,"height":1400,"scales":[1],"files":["/home/dslak/Desktop/vds_quiz/vds-app/App/assets/bg.jpg"],"hash":"4ade0cd9affb7de397ece858c4ef9212","name":"bg","type":"jpg","fileHashes":["4ade0cd9affb7de397ece858c4ef9212"]},"a364dc7a784101f7c8f6791c7b4514ce":{"__packager_asset":true,"fileSystemLocation":"/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets","httpServerLocation":"/assets/node_modules/react-navigation-stack/lib/module/vendor/views/assets","width":24,"height":24,"scales":[1,1.5,2,3,4],"files":["/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@1x.android.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@1.5x.android.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@2x.android.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@3x.android.png","/home/dslak/Desktop/vds_quiz/vds-app/node_modules/react-navigation-stack/lib/module/vendor/views/assets/back-icon@4x.android.png"],"hash":"a364dc7a784101f7c8f6791c7b4514ce","name":"back-icon","type":"png","fileHashes":["778ffc9fe8773a878e9c30a6304784de","376d6a4c7f622917c39feb23671ef71d","c79c3606a1cf168006ad3979763c7e0c","02bc1fa7c0313217bde2d65ccbff40c9","35ba0eaec5a4f5ed12ca16fabeae451d"]}}

BIN
vds-app/dist/assets/02bc1fa7c0313217bde2d65ccbff40c9

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 B

BIN
vds-app/dist/assets/0ea69b5077e7c4696db85dbcba75b0e1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

BIN
vds-app/dist/assets/319b7d9681027a1ef8d3fb9520d73a17

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

BIN
vds-app/dist/assets/35ba0eaec5a4f5ed12ca16fabeae451d

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

BIN
vds-app/dist/assets/376d6a4c7f622917c39feb23671ef71d

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

BIN
vds-app/dist/assets/4ade0cd9affb7de397ece858c4ef9212

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

BIN
vds-app/dist/assets/5223c8d9b0d08b82a5670fb5f71faf78

Binary file not shown.

Before

Width:  |  Height:  |  Size: 913 B

BIN
vds-app/dist/assets/778ffc9fe8773a878e9c30a6304784de

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

BIN
vds-app/dist/assets/7d40544b395c5949f4646f5e150fe020

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 B

BIN
vds-app/dist/assets/a132ecc4ba5c1517ff83c0fb321bc7fc

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 B

BIN
vds-app/dist/assets/c79c3606a1cf168006ad3979763c7e0c

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

BIN
vds-app/dist/assets/cdd04e13d4ec83ff0cd13ec8dabdc341

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 B

BIN
vds-app/dist/assets/e6b3e73c7c9600ce768eba302c6ad054

Binary file not shown.

Before

Width:  |  Height:  |  Size: 808 KiB

BIN
vds-app/dist/assets/f5b790e2ac193b3d41015edb3551f9b8

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 B

BIN
vds-app/dist/bundles/android-21da78b5401f0954d05cf90eba32fc85.js

Binary file not shown.

1
vds-app/dist/bundles/android-21da78b5401f0954d05cf90eba32fc85.map

File diff suppressed because one or more lines are too long

BIN
vds-app/dist/bundles/ios-62b68d2e0eb54df3fa8210b6752aa722.js

Binary file not shown.

1
vds-app/dist/bundles/ios-62b68d2e0eb54df3fa8210b6752aa722.map

File diff suppressed because one or more lines are too long

6
vds-app/dist/debug.html

@ -1,6 +0,0 @@
<script src="bundles/ios-62b68d2e0eb54df3fa8210b6752aa722.js"></script>
<script src="bundles/android-21da78b5401f0954d05cf90eba32fc85.js"></script>
Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab.
You can see a red colored folder containing the original source code from your bundle.

1
vds-app/dist/metadata.json

@ -1 +0,0 @@
{"version":0,"bundler":"metro","fileMetadata":{"ios":{"bundle":"bundles/ios-62b68d2e0eb54df3fa8210b6752aa722.js","assets":[{"path":"assets/7d40544b395c5949f4646f5e150fe020","ext":"png"},{"path":"assets/cdd04e13d4ec83ff0cd13ec8dabdc341","ext":"png"},{"path":"assets/a132ecc4ba5c1517ff83c0fb321bc7fc","ext":"png"},{"path":"assets/0ea69b5077e7c4696db85dbcba75b0e1","ext":"png"},{"path":"assets/f5b790e2ac193b3d41015edb3551f9b8","ext":"png"},{"path":"assets/5223c8d9b0d08b82a5670fb5f71faf78","ext":"png"},{"path":"assets/319b7d9681027a1ef8d3fb9520d73a17","ext":"png"},{"path":"assets/e6b3e73c7c9600ce768eba302c6ad054","ext":"png"},{"path":"assets/4ade0cd9affb7de397ece858c4ef9212","ext":"jpg"}]},"android":{"bundle":"bundles/android-21da78b5401f0954d05cf90eba32fc85.js","assets":[{"path":"assets/778ffc9fe8773a878e9c30a6304784de","ext":"png"},{"path":"assets/376d6a4c7f622917c39feb23671ef71d","ext":"png"},{"path":"assets/c79c3606a1cf168006ad3979763c7e0c","ext":"png"},{"path":"assets/02bc1fa7c0313217bde2d65ccbff40c9","ext":"png"},{"path":"assets/35ba0eaec5a4f5ed12ca16fabeae451d","ext":"png"},{"path":"assets/5223c8d9b0d08b82a5670fb5f71faf78","ext":"png"},{"path":"assets/319b7d9681027a1ef8d3fb9520d73a17","ext":"png"},{"path":"assets/e6b3e73c7c9600ce768eba302c6ad054","ext":"png"},{"path":"assets/4ade0cd9affb7de397ece858c4ef9212","ext":"jpg"}]}}}

3
vds-app/eas.json

@ -1,6 +1,7 @@
{
"cli": {
"version": ">= 3.8.1"
"version": ">= 3.17.1",
"appVersionSource": "local"
},
"build": {
"development": {

8
vds-app/index.js

@ -0,0 +1,8 @@
import { registerRootComponent } from 'expo';
import App from './App';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

7
vds-app/metro.config.js

@ -0,0 +1,7 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
module.exports = config;

60
vds-app/package.json

@ -1,51 +1,45 @@
{
"name": "vds-quiz",
"version": "3.8.3",
"version": "3.9.0",
"license": "MIT",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"android": "expo run:android",
"web": "expo start --web",
"build-android-old": "expo build:android -t app-bundle",
"build-android": "eas build -p android",
"build-android-prod": "eas build --clear-cache --platform android --profile production",
"build-android-local": "eas build -p android --profile preview",
"build-ios": "expo build:ios",
"ios": "expo start --ios",
"ios": "expo run:ios",
"eject": "expo eject",
"lint": "eslint ."
},
"dependencies": {
"@expo/config-plugins": "^6.0.1",
"@react-native-admob/admob": "^2.0.1",
"@react-native-async-storage/async-storage": "1.17.11",
"@react-native-masked-view/masked-view": "^0.2.8",
"@react-native-picker/picker": "2.4.8",
"expo": "^48.0.8",
"expo-build-properties": "~0.5.1",
"expo-dev-client": "~2.1.6",
"expo-permissions": "^14.1.1",
"expo-updates": "~0.16.3",
"react": "^18.2.0",
"react-native": "0.71.4",
"react-native-gesture-handler": "^2.9.0",
"react-native-google-mobile-ads": "^10.0.0",
"react-native-reanimated": "~2.14.4",
"react-native-safe-area-context": "^4.5.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/cli": "^20.0.2",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-native-picker/picker": "2.11.1",
"@react-navigation/bottom-tabs": "^7.4.7",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.26",
"expo": "^54.0.4",
"expo-build-properties": "^1.0.8",
"expo-dev-client": "^6.0.12",
"expo-updates": "^29.0.10",
"react": "19.1.0",
"react-native": "^0.81.4",
"react-native-gesture-handler": "^2.28.0",
"react-native-google-mobile-ads": "^15.7.0",
"react-native-gradle-plugin": "^0.71.19",
"react-native-reanimated": "^4.1.0",
"react-native-safe-area-context": "^5.6.1",
"react-native-safe-area-view": "^1.1.1",
"react-native-screens": "^3.20.0",
"react-navigation": "^4.4.4",
"react-navigation-stack": "^2.10.4",
"react-native-screens": "^4.16.0",
"react-native-worklets": "^0.5.1",
"remove-console-logs": "^0.1.0"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.2",
"babel-preset-expo": "^9.3.0",
"eslint": "^8.36.0",
"eslint-config-handlebarlabs": "^0.0.6",
"prettier": "^2.8.6"
},
"peerDependencies": {
"expo-modules-autolinking": "^1.1.2"
},
@ -54,5 +48,9 @@
"./assets/fonts/"
]
},
"private": true
"private": true,
"devDependencies": {
"@babel/core": "^7.28.4",
"@babel/preset-env": "^7.28.3"
}
}

8286
vds-app/yarn-error.log

File diff suppressed because it is too large

22
yarn.lock

@ -1,22 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@react-native-async-storage/async-storage@^1.17.12":
version "1.17.12"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.12.tgz#a39e4df5b06795ce49b2ca5b7ca9b8faadf8e621"
integrity sha512-BXg4OxFdjPTRt+8MvN6jz4muq0/2zII3s7HeT/11e4Zeh3WCgk/BleLzUcDfVqF3OzFHUqEkSrb76d6Ndjd/Nw==
dependencies:
merge-options "^3.0.4"
is-plain-obj@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
merge-options@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7"
integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==
dependencies:
is-plain-obj "^2.1.0"
Loading…
Cancel
Save