From 53fb5cdc73cdf262d485451bf4a329752e80ae6e Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 29 Sep 2021 02:58:43 -0500 Subject: [PATCH 01/38] Added TBA Event Import & Display --- components/TBA.tsx | 94 ++++++++++++++++++++++++++++++++ components/Themed.tsx | 4 +- hooks/useCachedResources.ts | 5 ++ package.json | 1 + screens/MatchesScreen.tsx | 91 +++++++++++++++++++++++++------ screens/RegionalModal.tsx | 106 ++++++++++++++++++++++++++++++++++++ screens/SettingsScreen.tsx | 29 ++++++---- screens/TeamsScreen.tsx | 37 ++++++++++++- yarn.lock | 19 +++++++ 9 files changed, 354 insertions(+), 32 deletions(-) create mode 100644 components/TBA.tsx create mode 100644 screens/RegionalModal.tsx diff --git a/components/TBA.tsx b/components/TBA.tsx new file mode 100644 index 0000000..eeca927 --- /dev/null +++ b/components/TBA.tsx @@ -0,0 +1,94 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Alert } from 'react-native'; +const APIKey = ""; +const prefix = "https://www.thebluealliance.com/api/v3/"; +const suffix = "?X-TBA-Auth-Key=" + APIKey; + +const MATCH_TYPES = ["qm", "qf", "sf", "f"]; + +export class TBA +{ + static eventID?: string; + static teams?: any[]; + static matches?: any[]; + + static async downloadData(id: string) + { + TBA.eventID = id; + AsyncStorage.setItem('event_id', TBA.eventID); + + let teams = await this.getTeams(); + if (!(teams)) + return Alert.alert("Error","Could not connect to The Blue Alliance"); + TBA.teams = teams; + TBA._sortTeams(); + AsyncStorage.setItem('team_data', JSON.stringify(teams)); + + let matches = await this.getMatches(); + if (!(matches)) + return Alert.alert("Error","Could not connect to The Blue Alliance"); + TBA.matches = matches; + this._sortMatches(); + AsyncStorage.setItem('match_data', JSON.stringify(matches)); + + Alert.alert("Success", "Successfully downloaded data from The Blue Alliance"); + } + + static getEvents() + { + return TBA._fetch("events/2019"); + } + + static getTeams() + { + return TBA._fetch("event/" + TBA.eventID + "/teams/simple"); + } + + static getMatches() + { + return TBA._fetch("event/" + TBA.eventID + "/matches/simple"); + } + + static async loadSave() + { + if (TBA.eventID) + return; + + let id = await AsyncStorage.getItem('event_id'); + if (id) + TBA.eventID = id; + + let matches = await AsyncStorage.getItem('match_data'); + if (matches) + TBA.matches = JSON.parse(matches); + + let teams = await AsyncStorage.getItem('team_data'); + if (teams) + TBA.teams = JSON.parse(teams); + } + + static _sortMatches() + { + if (TBA.matches) + { + TBA.matches.sort((a, b) => + (a.match_number + MATCH_TYPES.indexOf(a.comp_level) * 500) - (b.match_number + MATCH_TYPES.indexOf(b.comp_level) * 500) + ) + } + } + + static _sortTeams() + { + if (TBA.teams) + { + TBA.teams.sort((a, b) => + (a.team_number - b.team_number) + ) + } + } + + static _fetch(path: string): Promise + { + return fetch(prefix + path + suffix).then(response => response.json()); + } +} \ No newline at end of file diff --git a/components/Themed.tsx b/components/Themed.tsx index b713121..8ad60a8 100644 --- a/components/Themed.tsx +++ b/components/Themed.tsx @@ -10,7 +10,9 @@ const styles = StyleSheet.create({ marginTop: 40, paddingTop: 30, paddingLeft: 20, - paddingRight: 20 + paddingRight: 20, + paddingBottom: 20, + marginBottom: 0 }, title: { color: "#fff", diff --git a/hooks/useCachedResources.ts b/hooks/useCachedResources.ts index 14dba57..22e2643 100644 --- a/hooks/useCachedResources.ts +++ b/hooks/useCachedResources.ts @@ -2,6 +2,7 @@ import { FontAwesome } from '@expo/vector-icons'; import * as Font from 'expo-font'; import * as SplashScreen from 'expo-splash-screen'; import * as React from 'react'; +import { TBA } from '../components/TBA'; export default function useCachedResources() { const [isLoadingComplete, setLoadingComplete] = React.useState(false); @@ -17,6 +18,10 @@ export default function useCachedResources() { ...FontAwesome.font, 'space-mono': require('../assets/fonts/SpaceMono-Regular.ttf'), }); + + // Load Save File + await TBA.loadSave(); + } catch (e) { // We might want to provide this error information to an error reporting service console.warn(e); diff --git a/package.json b/package.json index 6bff359..7b8d4ef 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@expo/vector-icons": "^12.0.0", + "@react-native-async-storage/async-storage": "^1.15.8", "@react-native-picker/picker": "^2.1.0", "@react-navigation/bottom-tabs": "^6.0.5", "@react-navigation/native": "^6.0.2", diff --git a/screens/MatchesScreen.tsx b/screens/MatchesScreen.tsx index 7259c66..3ed2586 100644 --- a/screens/MatchesScreen.tsx +++ b/screens/MatchesScreen.tsx @@ -1,25 +1,82 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; +import { TBA } from '../components/TBA'; -import { Text, Container, Title } from '../components/Themed'; +import { Text, Container, Title, Button } from '../components/Themed'; export default function MatchesScreen() { - return ( - - Matches - Match data has not been downloaded from TBA yet. Download is available under settings - - ); + let matchDisplay: JSX.Element[] = []; + + if (TBA.matches) + { + for (let match of TBA.matches) + { + let key = match.key; + let matchName = match.comp_level + "-" + match.match_number; + switch (match.comp_level) + { + case "qm": + matchName = "Qualification " + match.match_number; + break; + case "qf": + matchName = "Quarter-Finals " + match.match_number; + break; + case "sf": + matchName = "Semi-Finals " + match.match_number; + break; + case "f": + matchName = "Finals " + match.match_number; + break; + } + + let matchDesc = ""; + for (let team of match.alliances.blue.team_keys) + matchDesc += team.substring(3) + " "; + matchDesc += " - " + for (let team of match.alliances.red.team_keys) + matchDesc += team.substring(3) + " "; + + matchDisplay.push( + + ); + } + } + else + { + matchDisplay.push( + Match data has not been downloaded from TBA yet. Download is available under settings + ); + } + + return ( + + Matches + {matchDisplay} + + ); } const styles = StyleSheet.create({ - title: { - fontSize: 20, - fontWeight: 'bold', - }, - separator: { - marginVertical: 30, - height: 1, - width: '80%', - }, -}); + title: { + fontSize: 20, + fontWeight: 'bold', + }, + separator: { + marginVertical: 30, + height: 1, + width: '80%', + }, + matchButton: { + alignItems: "flex-start" + }, + matchName: { + fontSize: 18, + textAlign: "left" + }, + matchDesc: { + color: "#bbb" + }, +}); \ No newline at end of file diff --git a/screens/RegionalModal.tsx b/screens/RegionalModal.tsx new file mode 100644 index 0000000..90af746 --- /dev/null +++ b/screens/RegionalModal.tsx @@ -0,0 +1,106 @@ +import React from "react"; +import { Alert, Modal, ModalProps, ScrollView, StyleSheet, View } from "react-native"; +import { TextInput } from "react-native-gesture-handler"; +import { TBA } from "../components/TBA"; +import { Button, Container, Text, Title } from "../components/Themed"; + +export default function RegionalModal(props: ModalProps) +{ + const [searchTerm, updateSearch] = React.useState(""); + const [regionalList, updateRegionals] = React.useState([] as any[]); + + // Generate List + let regionalsDisplay: JSX.Element[] = []; + if (regionalList.length <= 0) + { + TBA.getEvents().then((events: any[]) => { + updateRegionals(events); + }).catch(() => { + Alert.alert("Error","Could not connect to The Blue Alliance"); + }); + + regionalsDisplay.push( + + Loading All Regionals... + + ); + } + else + { + for (let regional of regionalList) + { + let key = regional.key; + if (regional.name.toLowerCase().includes(searchTerm)) + regionalsDisplay.push( + + ) + } + } + + // Display Data + + return ( + + + + Regional: + {updateSearch(text.toLowerCase())}} + /> + + {regionalsDisplay} + + + + + + ); +} + +const styles = StyleSheet.create({ + button: { + backgroundColor: "#deda04", + position: "absolute", + bottom: 80, + right: 20, + left: 20, + borderRadius: 10 + }, + buttonText: { + color: "#000" + }, + regionalButton: { + textAlign: "center" + }, + textInput: { + color: "#fff", + borderBottomColor: "#fff", + borderBottomWidth: 1, + marginBottom: 10 + }, + modal: { + backgroundColor: "#0b0b0b", + flex: 1, + borderRadius: 10, + marginTop: 30, + paddingTop: 30, + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 70, + marginBottom: 60, + }, + loadingText: { + textAlign: "center", + fontStyle: "italic" + } +}); diff --git a/screens/SettingsScreen.tsx b/screens/SettingsScreen.tsx index 6f5f38f..b336bd4 100644 --- a/screens/SettingsScreen.tsx +++ b/screens/SettingsScreen.tsx @@ -1,12 +1,15 @@ import { Picker } from '@react-native-picker/picker'; import * as React from 'react'; -import { Modal, StyleSheet } from 'react-native'; -import { Switch } from 'react-native-gesture-handler'; +import { Modal, ScrollView, StyleSheet } from 'react-native'; +import { Switch, TextInput } from 'react-native-gesture-handler'; import { TBA } from '../components/TBA'; import { Text, Container, Title, Button } from '../components/Themed'; +import RegionalModal from './RegionalModal'; -export default function SettingsScreen() { +export default function SettingsScreen() +{ const [modalVisible, setModalVisible] = React.useState(false); + return ( Settings @@ -15,17 +18,11 @@ export default function SettingsScreen() { Download TBA Data - { setModalVisible(!modalVisible); - }}> - - Regional: - - + }} /> ); } @@ -37,9 +34,19 @@ const styles = StyleSheet.create({ buttonText: { color: "#000" }, + textInput: { + color: "#fff", + borderBottomColor: "#fff", + borderBottomWidth: 1, + marginBottom: 10 + }, modal: { backgroundColor: "#111", margin: 20, marginBottom: 100 + }, + loadingText: { + textAlign: "center", + fontStyle: "italic" } }); diff --git a/screens/TeamsScreen.tsx b/screens/TeamsScreen.tsx index 7803222..41153a8 100644 --- a/screens/TeamsScreen.tsx +++ b/screens/TeamsScreen.tsx @@ -2,18 +2,49 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; import { TBA } from '../components/TBA'; -import { Text, Title, Container } from '../components/Themed'; +import { Text, Title, Container, Button } from '../components/Themed'; import { RootTabScreenProps } from '../types'; export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) { + let teamDisplay: JSX.Element[] = []; + + if (TBA.teams) + { + for (let team of TBA.teams) + { + let key = team.key; + teamDisplay.push( + + ); + } + } + else + { + teamDisplay.push( + Team data has not been downloaded from TBA yet. Download is available under settings + ); + } + return ( Teams - Team data has not been downloaded from TBA yet. Download is available under settings + {teamDisplay} ); } const styles = StyleSheet.create({ - + teamButton: { + alignItems: "flex-start", + }, + teamName: { + fontSize: 18, + textAlign: "left" + }, + teamNumber: { + color: "#bbb" + }, }); diff --git a/yarn.lock b/yarn.lock index 4d95e82..4041cf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2011,6 +2011,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@react-native-async-storage/async-storage@^1.15.8": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.15.8.tgz#c2263646b7261803125b555cf1bbd48b72dedafc" + integrity sha512-SIpsnmUt2Af8f/In7wu/HMeIiWBx9+T14GL4VrwtZv8+RceMejPtOwRMP8kc6xifkgg0gxwwHJ5+pEG/cEt1Mw== + dependencies: + merge-options "^3.0.4" + "@react-native-community/cli-debugger-ui@^4.13.1": version "4.13.1" resolved "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.13.1.tgz" @@ -5020,6 +5027,11 @@ is-plain-obj@^1.0.0: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +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== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" @@ -6093,6 +6105,13 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +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" + merge-stream@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz" From 03b2cda52d07160c73b29321235de403da05404b Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 29 Sep 2021 04:46:30 -0500 Subject: [PATCH 02/38] Added Team Thumbnails --- components/TBA.tsx | 43 +++++++++++++++++++++++++++++++------- navigation/index.tsx | 2 +- screens/RegionalModal.tsx | 15 +++++++++---- screens/SettingsScreen.tsx | 19 ++++++++++------- screens/SharingScreen.tsx | 5 +++-- screens/TeamsScreen.tsx | 20 ++++++++++++++---- 6 files changed, 79 insertions(+), 25 deletions(-) diff --git a/components/TBA.tsx b/components/TBA.tsx index eeca927..3e61317 100644 --- a/components/TBA.tsx +++ b/components/TBA.tsx @@ -5,6 +5,8 @@ const prefix = "https://www.thebluealliance.com/api/v3/"; const suffix = "?X-TBA-Auth-Key=" + APIKey; const MATCH_TYPES = ["qm", "qf", "sf", "f"]; +const YEAR = 2019; +const DEFAULT_IMG = "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; export class TBA { @@ -12,31 +14,53 @@ export class TBA static teams?: any[]; static matches?: any[]; - static async downloadData(id: string) + static async downloadData(id?: string) { - TBA.eventID = id; - AsyncStorage.setItem('event_id', TBA.eventID); + if (id) + { + TBA.eventID = id; + await AsyncStorage.setItem('event_id', TBA.eventID); + } + // Teams let teams = await this.getTeams(); if (!(teams)) return Alert.alert("Error","Could not connect to The Blue Alliance"); TBA.teams = teams; TBA._sortTeams(); - AsyncStorage.setItem('team_data', JSON.stringify(teams)); + // Team Media + let imageCount = 0; + for (let team of teams) + { + let media = await TBA.getTeamMedia(team.key); + team.thumb = DEFAULT_IMG; + if (media.length > 0) + { + if ("base64Image" in media[0].details) + { + team.thumb = "data:image/png;base64, " + media[0].details.base64Image; + imageCount++; + } + } + } + + await AsyncStorage.setItem('team_data', JSON.stringify(teams)); + + // Matches let matches = await this.getMatches(); if (!(matches)) return Alert.alert("Error","Could not connect to The Blue Alliance"); TBA.matches = matches; this._sortMatches(); - AsyncStorage.setItem('match_data', JSON.stringify(matches)); + await AsyncStorage.setItem('match_data', JSON.stringify(matches)); - Alert.alert("Success", "Successfully downloaded data from The Blue Alliance"); + Alert.alert("Success", "Successfully downloaded data from The Blue Alliance. (" + imageCount + " / " + teams.length + " images)"); } static getEvents() { - return TBA._fetch("events/2019"); + return TBA._fetch("events/" + YEAR); } static getTeams() @@ -44,6 +68,11 @@ export class TBA return TBA._fetch("event/" + TBA.eventID + "/teams/simple"); } + static getTeamMedia(teamKey: string) + { + return TBA._fetch("team/" + teamKey + "/media/" + YEAR); + } + static getMatches() { return TBA._fetch("event/" + TBA.eventID + "/matches/simple"); diff --git a/navigation/index.tsx b/navigation/index.tsx index 66bfe73..2ba3b3c 100644 --- a/navigation/index.tsx +++ b/navigation/index.tsx @@ -54,7 +54,7 @@ function BottomTabNavigator() { diff --git a/screens/RegionalModal.tsx b/screens/RegionalModal.tsx index 90af746..25bd529 100644 --- a/screens/RegionalModal.tsx +++ b/screens/RegionalModal.tsx @@ -1,9 +1,15 @@ import React from "react"; -import { Alert, Modal, ModalProps, ScrollView, StyleSheet, View } from "react-native"; +import { Alert, Modal, ScrollView, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; import { TBA } from "../components/TBA"; import { Button, Container, Text, Title } from "../components/Themed"; +interface ModalProps +{ + visible: boolean; + setVisible: Function; +} + export default function RegionalModal(props: ModalProps) { const [searchTerm, updateSearch] = React.useState(""); @@ -32,7 +38,7 @@ export default function RegionalModal(props: ModalProps) let key = regional.key; if (regional.name.toLowerCase().includes(searchTerm)) regionalsDisplay.push( - ) @@ -45,7 +51,8 @@ export default function RegionalModal(props: ModalProps) + visible={props.visible} + onRequestClose={() => props.setVisible(false)} > Regional: @@ -60,7 +67,7 @@ export default function RegionalModal(props: ModalProps) - diff --git a/screens/SettingsScreen.tsx b/screens/SettingsScreen.tsx index b336bd4..a141b3e 100644 --- a/screens/SettingsScreen.tsx +++ b/screens/SettingsScreen.tsx @@ -13,16 +13,18 @@ export default function SettingsScreen() return ( Settings + + + - { - setModalVisible(!modalVisible); - }} /> + ); } @@ -30,9 +32,12 @@ export default function SettingsScreen() const styles = StyleSheet.create({ button: { backgroundColor: "#deda04", + marginBottom: 10, + borderRadius: 5 }, buttonText: { - color: "#000" + color: "#000", + fontSize: 16 }, textInput: { color: "#fff", diff --git a/screens/SharingScreen.tsx b/screens/SharingScreen.tsx index cd764ec..242f938 100644 --- a/screens/SharingScreen.tsx +++ b/screens/SharingScreen.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import QRCode from "react-qr-code"; -import { Container } from '../components/Themed'; +import { Container, Text } from '../components/Themed'; export default function SharingScreen() { return ( - ) for (let team of TBA.teams) { let key = team.key; + let image = team.thumb; teamDisplay.push( ); } @@ -38,7 +43,14 @@ export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) const styles = StyleSheet.create({ teamButton: { - alignItems: "flex-start", + flexDirection: "row", + justifyContent: "flex-start" + }, + teamImage: { + width: 40, + height: 40, + marginRight: 10, + resizeMode: 'stretch' }, teamName: { fontSize: 18, From 79717b58f0e0af71709472a1e2ec0f93280a8b12 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Thu, 30 Sep 2021 06:36:06 -0500 Subject: [PATCH 03/38] Added Team/Match Modals and Image Upload TODO: Code refreactor --- app.json | 3 +- assets/images/adaptive-icon.png | Bin 17547 -> 13460 bytes assets/images/favicon.png | Bin 1466 -> 11772 bytes assets/images/icon.png | Bin 22380 -> 8571 bytes assets/images/splash.png | Bin 48478 -> 35700 bytes components/DarkBackground.tsx | 21 ++++ components/DownloadingModal.tsx | 54 +++++++++ components/PhotoModal.tsx | 45 ++++++++ components/TBA.tsx | 126 ++++++++++++++++++--- components/TBAModels.ts | 29 +++++ package.json | 1 + screens/MatchModal.tsx | 92 +++++++++++++++ screens/MatchesScreen.tsx | 40 +++---- screens/RegionalModal.tsx | 23 +++- screens/SettingsScreen.tsx | 10 +- screens/TeamModal.tsx | 194 ++++++++++++++++++++++++++++++++ screens/TeamsScreen.tsx | 15 ++- yarn.lock | 14 +++ 18 files changed, 615 insertions(+), 52 deletions(-) create mode 100644 components/DarkBackground.tsx create mode 100644 components/DownloadingModal.tsx create mode 100644 components/PhotoModal.tsx create mode 100644 components/TBAModels.ts create mode 100644 screens/MatchModal.tsx create mode 100644 screens/TeamModal.tsx diff --git a/app.json b/app.json index 043ff4d..2624511 100644 --- a/app.json +++ b/app.json @@ -25,7 +25,8 @@ "adaptiveIcon": { "foregroundImage": "./assets/images/adaptive-icon.png", "backgroundColor": "#000000" - } + }, + "package": "org.team5148.blitzscouter" }, "web": { "favicon": "./assets/images/favicon.png" diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png index 03d6f6b6c6727954aec1d8206222769afd178d8d..c0d5baf5ced6427ed32312e214c87876006a88d3 100644 GIT binary patch literal 13460 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 `hWP8qx(?PmF|?@ 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 literal 17547 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 literal 22380 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 diff --git a/assets/images/splash.png b/assets/images/splash.png index 6f47774733be408640c3db372cf91117354b0479..a111ec13237dbf8bd471f0c6e4b29bb237e6c428 100644 GIT binary patch literal 35700 zcmeEuWmHw|_V1!Q3_wH>k&y0gFaRm(?v&U_cZYz8ZX_fGlt#L{M5R%rYom0y>Dt77 zw(mLTf6ln~%NgU|`{~6YWAC-*n$MiS8PA;aS%YYG)kh?RHwhsKB2iShuK__g*bqcG zg^vraX#FJTgdhS&UoAa%4RbGg7gv~#y`wd~yN`=Cy|uT!4Fq{lZ6zDS7wbvF=RF8- z%OY;K|9XW@y+Ct(e34b;taj8d;@L$LSW*0*G4Z|zBj0NU6V&Y>>GAnv#!>ggqA_0w z;mq%k)xwCHOfz?GEpZL)9I$!?%%8hn?(R6ht4|bN8OL%ctMZ?@ys;XP)S-QDPH%|B z+uH~;F3~}|9e02o<5*%vs7`dIybF^{x`;ri=jh(c+RTY(g zskGm``!4>EOwB0NwrIys?=d{h}Mtnp-dn}J}OGzc>c+^!^vZI zgWJ0EGCf-!mp?y`o7E2OaU&m}dtOQ+4#JP4gjWtumbs7gy)IkRm!YoZCpKq_ZcL8Z%@Cto_BiyeQr=pbP>_FH5a#%qRY3Tp9-@B5;ko|OKndbW zVDO;8$8^&M=5-_n{uN}Y@8V?lLaX_M1zEs74WD8s+EFrQY^^OQQXpywbb<81%@S3wWn z(=}PPo9ir3-L=ne$jPZbHN&+i#DDHU4`pkbd8WOAmj-k3+->QJNlmdPaWTT=CFd~I zklc-)ZF_|Zo!0IR3O78J`AxWbLAY2N{9E|z`HN|8^}Pb2jluCS^&E5TgTb8JB(-r? zcVnh*OPL1Mb_G4lT7%bptw3FWWIip@wLTHD$d+5@7H|C_f?6S$dw<{gVZ{XY&liuM z1g*VnJe0o|;4T@cBYEE;-{x6uUWEDn$K!EavIBg{lKAI^-7f~eRwY@=31Wynw)Dub zq#XCNWFnrF-P84qb7l0SN_bi%8fl`RJ+A0syiqOsd5vMYIH&Qk!_teUPzTM2WgZ>3 z-sWK~&wp6?zVbUY)@QwVJY#bv@Jjk@_ch}bDwS-SJ5S|tn~B@hv-1182jGQeh82I7GOj_5=fd#`dv*p@Mp#Au`F_d7UXn)IpN@~tl>eEN> zUnLw-CvU>r3F0N`t{r7m3U8cLic50dqg;@WCv6-(_^6Opcb9`oFNJ|$;8RGVm8f-@ zp#uZ+4R(!-DFeKY`%gX}K49L^e7oo5j!`c+osJkZsxlOB@GR}ukiEyp{Yr<8-bnb- zas?6JojY0$$79u=oZXH)^f~1|4;TaztD5JjwK1w9WXrX$!ET?suDLt-2=GXG%sH

^TJx&$?amf?MZ^Q5X?u@ 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 Iol`;+02ibJL;wH) diff --git a/components/DarkBackground.tsx b/components/DarkBackground.tsx new file mode 100644 index 0000000..4e1db09 --- /dev/null +++ b/components/DarkBackground.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { StyleSheet, View } from "react-native"; + +export default function DarkBackground() +{ + return ( + + ); +} + +const styles = StyleSheet.create({ + background: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "black", + opacity: 0.6 + } +}); diff --git a/components/DownloadingModal.tsx b/components/DownloadingModal.tsx new file mode 100644 index 0000000..ea2de71 --- /dev/null +++ b/components/DownloadingModal.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { Modal, StyleSheet, View } from "react-native"; +import DarkBackground from "./DarkBackground"; +import { Title } from "./Themed"; + +interface ModalProps +{ + status: string; +} + +export default function DownloadingModal(props: ModalProps) +{ + + // Return Modal + return ( + + + + + + + Downloading, Please Wait + {props.status} + + + + ); +} + +const styles = StyleSheet.create({ + modal: { + backgroundColor: "#0e0e0e", + position: "absolute", + top: "40%", + width: "100%", + height: 100, + borderRadius: 10, + paddingTop: 30, + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 80, + }, + title: { + fontSize: 24, + marginBottom: 0 + }, + subtitle: { + color: "#bbb", + fontSize: 18 + } +}); diff --git a/components/PhotoModal.tsx b/components/PhotoModal.tsx new file mode 100644 index 0000000..b67533a --- /dev/null +++ b/components/PhotoModal.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { Dimensions, Image, Modal, StyleSheet, View } from "react-native"; +import DarkBackground from "./DarkBackground"; + +interface PhotoProps +{ + imageData: string; + setImageData: Function; +} + +export default function PhotoModal(props: PhotoProps) +{ + if (props.imageData === "") + return null; + else + return ( + props.setImageData("")} > + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: "absolute", + justifyContent: "center", + top: 0, + left: 0, + bottom: 0, + right: 0, + + }, + image: { + width: Dimensions.get('window').width, + height: Dimensions.get('window').width + } +}); diff --git a/components/TBA.tsx b/components/TBA.tsx index 3e61317..a95a59c 100644 --- a/components/TBA.tsx +++ b/components/TBA.tsx @@ -1,21 +1,23 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { Alert } from 'react-native'; +import Match, { Team } from './TBAModels'; const APIKey = ""; const prefix = "https://www.thebluealliance.com/api/v3/"; const suffix = "?X-TBA-Auth-Key=" + APIKey; const MATCH_TYPES = ["qm", "qf", "sf", "f"]; const YEAR = 2019; -const DEFAULT_IMG = "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; +const BASE64_PREFIX = "data:image/png;base64, "; export class TBA { static eventID?: string; - static teams?: any[]; - static matches?: any[]; + static teams?: Team[]; + static matches?: Match[]; - static async downloadData(id?: string) + static async downloadData(id?: string, setDownloadStatus?: Function) { + // Check ID if (id) { TBA.eventID = id; @@ -23,39 +25,104 @@ export class TBA } // Teams + if (setDownloadStatus) + setDownloadStatus("Downloading Team Roster..."); let teams = await this.getTeams(); if (!(teams)) - return Alert.alert("Error","Could not connect to The Blue Alliance"); + { + if (setDownloadStatus) + setDownloadStatus(""); + return; + } + TBA.teams = teams; + + // Sort Teams + if (setDownloadStatus) + setDownloadStatus("Sorting Team Roster..."); TBA._sortTeams(); // Team Media - let imageCount = 0; + let teamCount = 0; for (let team of teams) { + teamCount++; + if (setDownloadStatus) + setDownloadStatus("Downloading Team Media... (" + teamCount + "/" + teams.length + ")"); + team.media = []; let media = await TBA.getTeamMedia(team.key); - team.thumb = DEFAULT_IMG; + + if (!(media)) + { + if (setDownloadStatus) + setDownloadStatus(""); + return; + } + if (media.length > 0) { if ("base64Image" in media[0].details) - { - team.thumb = "data:image/png;base64, " + media[0].details.base64Image; - imageCount++; - } + team.media.push(BASE64_PREFIX + media[0].details.base64Image); } } - await AsyncStorage.setItem('team_data', JSON.stringify(teams)); - // Matches + if (setDownloadStatus) + setDownloadStatus("Downloading Match List"); let matches = await this.getMatches(); if (!(matches)) - return Alert.alert("Error","Could not connect to The Blue Alliance"); + { + if (setDownloadStatus) + setDownloadStatus(""); + return; + } TBA.matches = matches; + + // Sort Matches + if (setDownloadStatus) + setDownloadStatus("Sorting Match List"); this._sortMatches(); + + // Match Data + for (let match of matches) + { + // Name + let matchName = match.comp_level + "-" + match.match_number; + switch (match.comp_level) + { + case "qm": + matchName = "Qualification " + match.match_number; + break; + case "qf": + matchName = "Quarter-Finals " + match.match_number; + break; + case "sf": + matchName = "Semi-Finals " + match.match_number; + break; + case "f": + matchName = "Finals " + match.match_number; + break; + } + match.name = matchName; + + // Description + let matchDesc = ""; + for (let team of match.alliances.blue.team_keys) + matchDesc += team.substring(3) + " "; + matchDesc += " - " + for (let team of match.alliances.red.team_keys) + matchDesc += team.substring(3) + " "; + + match.description = matchDesc; + } + + // Save Data await AsyncStorage.setItem('match_data', JSON.stringify(matches)); + await AsyncStorage.setItem('team_data', JSON.stringify(teams)); - Alert.alert("Success", "Successfully downloaded data from The Blue Alliance. (" + imageCount + " / " + teams.length + " images)"); + if (setDownloadStatus) + setDownloadStatus(""); + Alert.alert("Success", "Successfully downloaded data from The Blue Alliance."); } static getEvents() @@ -72,6 +139,27 @@ export class TBA { return TBA._fetch("team/" + teamKey + "/media/" + YEAR); } + + static getTeam(teamKey: string) + { + if (!(TBA.teams)) + return undefined; + return TBA.teams.find((value) => value.key === teamKey); + } + + static addTeamMedia(teamKey: string, media: string) + { + let team = this.getTeam(teamKey); + if (team) + team.media.push(BASE64_PREFIX + media); + } + + static getMatch(matchKey: string) + { + if (!(TBA.matches)) + return undefined; + return TBA.matches.find((value) => value.key === matchKey); + } static getMatches() { @@ -118,6 +206,12 @@ export class TBA static _fetch(path: string): Promise { - return fetch(prefix + path + suffix).then(response => response.json()); + return fetch(prefix + path + suffix).then(response => { + if (response.ok) + return response.json(); + }).catch((error) => { + Alert.alert("Error","Could not connect to The Blue Alliance"); + console.error(error); + }); } } \ No newline at end of file diff --git a/components/TBAModels.ts b/components/TBAModels.ts new file mode 100644 index 0000000..d1ff259 --- /dev/null +++ b/components/TBAModels.ts @@ -0,0 +1,29 @@ +export interface Team +{ + key: string; + nickname: string; + team_number: number; + media: string[]; +} + +export default interface Match +{ + alliances: { + blue: { + score: number; + team_keys: string[]; + }, + red: { + score: number; + team_keys: string[]; + } + }; + comp_level: string; + event_key: string; + key: string; + match_number: number; + videos: any[]; + winning_alliance: string; + name: string; + description: string; +} \ No newline at end of file diff --git a/package.json b/package.json index 7b8d4ef..7731f84 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "expo-asset": "~8.3.2", "expo-constants": "~11.0.1", "expo-font": "~9.2.1", + "expo-image-picker": "~10.2.2", "expo-linking": "~2.3.1", "expo-splash-screen": "~0.11.2", "expo-status-bar": "~1.0.4", diff --git a/screens/MatchModal.tsx b/screens/MatchModal.tsx new file mode 100644 index 0000000..4e96737 --- /dev/null +++ b/screens/MatchModal.tsx @@ -0,0 +1,92 @@ +import { FontAwesome } from "@expo/vector-icons"; +import React from "react"; +import { Alert, Dimensions, Image, Modal, ScrollView, StyleSheet, View } from "react-native"; +import { TextInput } from "react-native-gesture-handler"; +import DarkBackground from "../components/DarkBackground"; +import { TBA } from "../components/TBA"; +import { Button, Container, Text, Title } from "../components/Themed"; + +interface ModalProps +{ + matchID: string; + setMatch: Function; +} + +export default function MatchModal(props: ModalProps) +{ + // Default Behaviour + if (props.matchID === "") + return null; + + // Grab Match Data + let match = TBA.getMatch(props.matchID); + if (!(match)) + { + Alert.alert("Error", "There was an error grabbing the data from that match. Try re-downloading TBA data then try again."); + props.setMatch(""); + return null; + } + + // Return Modal + return ( + props.setMatch("")} > + + + + + + {match.name} + {match.description} + + Team Comments: + + + + + ); +} + +const styles = StyleSheet.create({ + media: { + flexDirection: "row", + marginBottom: 10 + }, + button: { + backgroundColor: "#deda04", + position: "absolute", + bottom: 80, + right: 20, + left: 20, + borderRadius: 10 + }, + buttonText: { + color: "#000" + }, + modal: { + backgroundColor: "#0b0b0b", + flex: 1, + borderRadius: 10, + marginTop: 30, + paddingTop: 30, + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 70, + marginBottom: 60, + }, + title: { + marginBottom: 0 + }, + subtitle: { + color: "#bbb", + fontSize: 15 + }, + header: { + fontSize: 24 + } +}); diff --git a/screens/MatchesScreen.tsx b/screens/MatchesScreen.tsx index 3ed2586..22b9bf8 100644 --- a/screens/MatchesScreen.tsx +++ b/screens/MatchesScreen.tsx @@ -3,8 +3,11 @@ import { StyleSheet } from 'react-native'; import { TBA } from '../components/TBA'; import { Text, Container, Title, Button } from '../components/Themed'; +import MatchModal from './MatchModal'; export default function MatchesScreen() { + const [matchID, setMatchID] = React.useState(""); + let matchDisplay: JSX.Element[] = []; if (TBA.matches) @@ -12,34 +15,15 @@ export default function MatchesScreen() { for (let match of TBA.matches) { let key = match.key; - let matchName = match.comp_level + "-" + match.match_number; - switch (match.comp_level) - { - case "qm": - matchName = "Qualification " + match.match_number; - break; - case "qf": - matchName = "Quarter-Finals " + match.match_number; - break; - case "sf": - matchName = "Semi-Finals " + match.match_number; - break; - case "f": - matchName = "Finals " + match.match_number; - break; - } - - let matchDesc = ""; - for (let team of match.alliances.blue.team_keys) - matchDesc += team.substring(3) + " "; - matchDesc += " - " - for (let team of match.alliances.red.team_keys) - matchDesc += team.substring(3) + " "; - + matchDisplay.push( - ); } @@ -55,6 +39,8 @@ export default function MatchesScreen() { Matches {matchDisplay} + + ); } diff --git a/screens/RegionalModal.tsx b/screens/RegionalModal.tsx index 25bd529..a8335f2 100644 --- a/screens/RegionalModal.tsx +++ b/screens/RegionalModal.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Alert, Modal, ScrollView, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; +import DownloadingModal from "../components/DownloadingModal"; import { TBA } from "../components/TBA"; import { Button, Container, Text, Title } from "../components/Themed"; @@ -14,10 +15,11 @@ export default function RegionalModal(props: ModalProps) { const [searchTerm, updateSearch] = React.useState(""); const [regionalList, updateRegionals] = React.useState([] as any[]); + const [downloadStatus, setDownloadStatus] = React.useState(""); // Generate List let regionalsDisplay: JSX.Element[] = []; - if (regionalList.length <= 0) + if (regionalList.length <= 0 && props.visible) { TBA.getEvents().then((events: any[]) => { updateRegionals(events); @@ -38,8 +40,15 @@ export default function RegionalModal(props: ModalProps) let key = regional.key; if (regional.name.toLowerCase().includes(searchTerm)) regionalsDisplay.push( - ) } @@ -70,6 +79,8 @@ export default function RegionalModal(props: ModalProps) + + ); } @@ -87,7 +98,11 @@ const styles = StyleSheet.create({ color: "#000" }, regionalButton: { - textAlign: "center" + padding: 6, + flexDirection: "row" + }, + regionalText:{ + fontSize: 16 }, textInput: { color: "#fff", diff --git a/screens/SettingsScreen.tsx b/screens/SettingsScreen.tsx index a141b3e..8aad6d1 100644 --- a/screens/SettingsScreen.tsx +++ b/screens/SettingsScreen.tsx @@ -2,6 +2,7 @@ import { Picker } from '@react-native-picker/picker'; import * as React from 'react'; import { Modal, ScrollView, StyleSheet } from 'react-native'; import { Switch, TextInput } from 'react-native-gesture-handler'; +import DownloadingModal from '../components/DownloadingModal'; import { TBA } from '../components/TBA'; import { Text, Container, Title, Button } from '../components/Themed'; import RegionalModal from './RegionalModal'; @@ -9,13 +10,14 @@ import RegionalModal from './RegionalModal'; export default function SettingsScreen() { const [modalVisible, setModalVisible] = React.useState(false); + const [downloadStatus, setDownloadStatus] = React.useState(""); return ( Settings @@ -24,7 +26,13 @@ export default function SettingsScreen() Change Regional + + + ); } diff --git a/screens/TeamModal.tsx b/screens/TeamModal.tsx new file mode 100644 index 0000000..dba5e1d --- /dev/null +++ b/screens/TeamModal.tsx @@ -0,0 +1,194 @@ +import { FontAwesome } from "@expo/vector-icons"; +import React from "react"; +import { Alert, Dimensions, Image, Modal, ScrollView, StyleSheet, View } from "react-native"; +import DarkBackground from "../components/DarkBackground"; +import { TBA } from "../components/TBA"; +import { Button, Container, Text, Title } from "../components/Themed"; +import * as ImagePicker from 'expo-image-picker'; +import PhotoModal from "../components/PhotoModal"; + +interface ModalProps +{ + teamID: string; + setTeam: Function; +} + +export default function TeamModal(props: ModalProps) +{ + const [previewData, setPreviewPhoto] = React.useState(""); + + // Default Behaviour + if (props.teamID === "") + return null; + + // Grab Team Data + let team = TBA.getTeam(props.teamID); + if (!(team)) + { + Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); + props.setTeam(""); + return null; + } + + // Grab Team Media + let mediaList: JSX.Element[] = []; + for (let imageData of team.media) + { + let preview = imageData; + mediaList.push( + + ); + } + + // Return Modal + return ( + props.setTeam("")} > + + + + + + {mediaList} + + + + + + + {team ? team.nickname : ""} + {team ? team.team_number : ""} + + Team Comments: + + + + + + + ); +} + +function addPhoto(teamID: string) +{ + return ImagePicker.launchCameraAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + + base64: true + }).then(result => { + if (result.cancelled) + return; + if (!result.base64) + return; + + TBA.addTeamMedia(teamID, result.base64); + }); +} + +function addFile(teamID: string) +{ + return ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + + base64: true + }).then(result => { + if (result.cancelled) + return; + if (!result.base64) + return; + + TBA.addTeamMedia(teamID, result.base64); + }); +} + +const styles = StyleSheet.create({ + media: { + marginBottom: 10, + height: 150, + width: "100%" + }, + button: { + backgroundColor: "#deda04", + position: "absolute", + bottom: 35, + right: 20, + left: 20, + borderRadius: 10 + }, + buttonText: { + color: "#000" + }, + modal: { + backgroundColor: "#0c0c0c", + flex: 1, + borderRadius: 10, + marginTop: 10, + marginBottom: 20, + marginLeft: 5, + marginRight: 5, + paddingTop: 30, + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 70, + }, + title: { + marginBottom: 0 + }, + subtitle: { + color: "#bbb", + fontSize: 30 + }, + header: { + fontSize: 24 + }, + thumbnail: { + height: 150, + width: 150, + margin: 5 + }, + imageButton: { + height: 150, + width: 150, + justifyContent: "center", + flexDirection: "row", + backgroundColor: "#333", + margin: 5 + } +}); diff --git a/screens/TeamsScreen.tsx b/screens/TeamsScreen.tsx index 85266a3..f915143 100644 --- a/screens/TeamsScreen.tsx +++ b/screens/TeamsScreen.tsx @@ -4,8 +4,11 @@ import { TBA } from '../components/TBA'; import { Text, Title, Container, Button } from '../components/Themed'; import { RootTabScreenProps } from '../types'; +import TeamModal from './TeamModal'; export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) { + const [teamID, setTeamID] = React.useState(""); + let teamDisplay: JSX.Element[] = []; if (TBA.teams) @@ -13,10 +16,14 @@ export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) for (let team of TBA.teams) { let key = team.key; - let image = team.thumb; teamDisplay.push( - @@ -60,7 +58,7 @@ const styles = StyleSheet.create({ button: { backgroundColor: "#deda04", position: "absolute", - bottom: 80, + bottom: 35, right: 20, left: 20, borderRadius: 10 @@ -72,12 +70,14 @@ const styles = StyleSheet.create({ backgroundColor: "#0b0b0b", flex: 1, borderRadius: 10, - marginTop: 30, + marginTop: 10, + marginBottom: 20, + marginLeft: 5, + marginRight: 5, paddingTop: 30, paddingLeft: 20, paddingRight: 20, paddingBottom: 70, - marginBottom: 60, }, title: { marginBottom: 0 diff --git a/screens/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx similarity index 71% rename from screens/MatchesScreen.tsx rename to screens/Matches/MatchesScreen.tsx index 22b9bf8..1f0cd5c 100644 --- a/screens/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; -import { TBA } from '../components/TBA'; +import { ScrollView } from 'react-native-gesture-handler'; +import { BlitzDB } from '../../components/Database/BlitzDB'; -import { Text, Container, Title, Button } from '../components/Themed'; +import { Text, Container, Title, Button, ScrollContainer } from '../../components/Themed'; import MatchModal from './MatchModal'; export default function MatchesScreen() { @@ -10,17 +11,15 @@ export default function MatchesScreen() { let matchDisplay: JSX.Element[] = []; - if (TBA.matches) + if (BlitzDB.event) { - for (let match of TBA.matches) + for (let match of BlitzDB.event.matches) { - let key = match.key; - matchDisplay.push( ); + return ( - + Settings - + {updateButton} - + ); } diff --git a/screens/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx similarity index 65% rename from screens/SharingScreen.tsx rename to screens/Sharing/SharingScreen.tsx index 242f938..2cc8ccf 100644 --- a/screens/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -1,26 +1,26 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import QRCode from "react-qr-code"; - -import { Container, Text } from '../components/Themed'; +import { Container } from '../../components/Themed'; export default function SharingScreen() { return ( - + - + + ); } const styles = StyleSheet.create({ container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', + marginTop: 150, + justifyContent: "center", + flexDirection: "row" } }); diff --git a/screens/TeamModal.tsx b/screens/Teams/TeamModal.tsx similarity index 83% rename from screens/TeamModal.tsx rename to screens/Teams/TeamModal.tsx index dba5e1d..d9f764f 100644 --- a/screens/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,16 +1,16 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Alert, Dimensions, Image, Modal, ScrollView, StyleSheet, View } from "react-native"; -import DarkBackground from "../components/DarkBackground"; -import { TBA } from "../components/TBA"; -import { Button, Container, Text, Title } from "../components/Themed"; +import { Alert, Image, Modal, ScrollView, StyleSheet } from "react-native"; +import DarkBackground from "../../components/DarkBackground"; +import { Button, Text, Title } from "../../components/Themed"; import * as ImagePicker from 'expo-image-picker'; -import PhotoModal from "../components/PhotoModal"; +import PhotoModal from "../../components/PhotoModal"; +import { BlitzDB } from "../../components/Database/BlitzDB"; interface ModalProps { teamID: string; - setTeam: Function; + setTeamID: Function; } export default function TeamModal(props: ModalProps) @@ -22,11 +22,11 @@ export default function TeamModal(props: ModalProps) return null; // Grab Team Data - let team = TBA.getTeam(props.teamID); + let team = BlitzDB.getTeam(props.teamID); if (!(team)) { Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); - props.setTeam(""); + props.setTeamID(""); return null; } @@ -36,7 +36,10 @@ export default function TeamModal(props: ModalProps) { let preview = imageData; mediaList.push( - ); @@ -48,7 +51,7 @@ export default function TeamModal(props: ModalProps) animationType="slide" transparent={true} visible={true} - onRequestClose={() => props.setTeam("")} > + onRequestClose={() => props.setTeamID("")} > @@ -85,13 +88,13 @@ export default function TeamModal(props: ModalProps) - {team ? team.nickname : ""} - {team ? team.team_number : ""} + {team ? team.name : ""} + {team ? team.number : ""} Team Comments: - @@ -115,7 +118,7 @@ function addPhoto(teamID: string) if (!result.base64) return; - TBA.addTeamMedia(teamID, result.base64); + BlitzDB.addTeamMedia(teamID, result.base64); }); } @@ -134,7 +137,7 @@ function addFile(teamID: string) if (!result.base64) return; - TBA.addTeamMedia(teamID, result.base64); + BlitzDB.addTeamMedia(teamID, result.base64); }); } diff --git a/screens/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx similarity index 61% rename from screens/TeamsScreen.tsx rename to screens/Teams/TeamsScreen.tsx index f915143..bf48e36 100644 --- a/screens/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { Image, StyleSheet, View } from 'react-native'; -import { TBA } from '../components/TBA'; - -import { Text, Title, Container, Button } from '../components/Themed'; -import { RootTabScreenProps } from '../types'; +import { BlitzDB } from '../../components/Database/BlitzDB'; +import { Text, Title, Button, ScrollContainer } from '../../components/Themed'; +import { RootTabScreenProps } from '../../types'; import TeamModal from './TeamModal'; export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) { @@ -11,22 +10,21 @@ export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) let teamDisplay: JSX.Element[] = []; - if (TBA.teams) + if (BlitzDB.currentTeams.length > 0) { - for (let team of TBA.teams) + for (let team of BlitzDB.currentTeams) { - let key = team.key; teamDisplay.push( @@ -41,12 +39,12 @@ export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) } return ( - - + + Teams {teamDisplay} - + ); } @@ -67,5 +65,5 @@ const styles = StyleSheet.create({ }, teamNumber: { color: "#bbb" - }, + } }); From b883fc947923b36ff0cf8c7ca4841736a74fe0a1 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Tue, 5 Oct 2021 05:04:06 -0500 Subject: [PATCH 05/38] Fixed Long Load Times in Bottom Tabs --- components/DarkBackground.tsx | 12 ++++--- components/PhotoModal.tsx | 2 +- components/Themed.tsx | 52 ++++++++++++++++++--------- navigation/LinkingConfiguration.ts | 45 ----------------------- navigation/index.tsx | 13 +------ screens/Matches/MatchModal.tsx | 2 +- screens/NotFoundScreen.tsx | 36 ------------------- screens/Settings/DownloadingModal.tsx | 2 +- screens/Settings/RegionalModal.tsx | 11 ++++-- screens/Sharing/SharingScreen.tsx | 24 +++++++------ screens/Teams/TeamModal.tsx | 35 ++++++++++++++++-- screens/Teams/TeamsScreen.tsx | 2 +- 12 files changed, 103 insertions(+), 133 deletions(-) delete mode 100644 navigation/LinkingConfiguration.ts delete mode 100644 screens/NotFoundScreen.tsx diff --git a/components/DarkBackground.tsx b/components/DarkBackground.tsx index 4e1db09..b3c3bd2 100644 --- a/components/DarkBackground.tsx +++ b/components/DarkBackground.tsx @@ -1,10 +1,15 @@ import React from "react"; import { StyleSheet, View } from "react-native"; -export default function DarkBackground() +interface DarkBackgroundProps +{ + isTransparent: boolean; +} + +export default function DarkBackground(props: DarkBackgroundProps) { return ( - + ); } @@ -15,7 +20,6 @@ const styles = StyleSheet.create({ left: 0, right: 0, bottom: 0, - backgroundColor: "black", - opacity: 0.6 + backgroundColor: "black" } }); diff --git a/components/PhotoModal.tsx b/components/PhotoModal.tsx index 9f0e193..f4153db 100644 --- a/components/PhotoModal.tsx +++ b/components/PhotoModal.tsx @@ -21,7 +21,7 @@ export default function PhotoModal(props: PhotoProps) onRequestClose={() => props.setImageData("")} > - + diff --git a/components/Themed.tsx b/components/Themed.tsx index c6e5dc8..1dd5b19 100644 --- a/components/Themed.tsx +++ b/components/Themed.tsx @@ -1,3 +1,5 @@ +import { FontAwesome } from '@expo/vector-icons'; +import { useFocusEffect, useIsFocused } from '@react-navigation/core'; import * as React from 'react'; import { ScrollView, StyleSheet, Text as DefaultText, View as DefaultView, TouchableOpacity as DefaultButton, Animated, View } from 'react-native'; @@ -56,31 +58,49 @@ export function Title(props: TextProps) { // Containers export function Container(props: ViewProps) { const { style, ...otherProps } = props; - const opacity = useFadeIn(); - return (); + return (); } export function ScrollContainer(props: ViewProps) { const { style, ...otherProps } = props; - const opacity = useFadeIn(); // BUG Gap at the end of each scroll view - return (); + return (); } // Fade Animation -function useFadeIn() { - const opacityRef = React.useRef(undefined); - if (opacityRef.current === undefined) - opacityRef.current = new Animated.Value(0); - - React.useEffect(() => { - Animated.timing(opacityRef.current as Animated.Value, { +function FadeIn(props: ViewProps) { + const fadeAnim = React.useRef(new Animated.Value(0)).current; + + useFocusEffect(() => { + Animated.timing(fadeAnim, { toValue: 1, duration: 300, - useNativeDriver: true, + useNativeDriver: true }).start(); - }, []); - - return opacityRef.current; - } + + return () => { + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true + }).start(); + } + }); + + return ( + {props.children} + + ); +} + +export function TabBarIcon(props: { + name: React.ComponentProps['name']; + color: string; +}) { + return ; +} diff --git a/navigation/LinkingConfiguration.ts b/navigation/LinkingConfiguration.ts deleted file mode 100644 index eb1470e..0000000 --- a/navigation/LinkingConfiguration.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Learn more about deep linking with React Navigation - * https://reactnavigation.org/docs/deep-linking - * https://reactnavigation.org/docs/configuring-links - */ - -import { LinkingOptions } from '@react-navigation/native'; -import * as Linking from 'expo-linking'; - -import { RootStackParamList } from '../types'; - -const linking: LinkingOptions = { - prefixes: [Linking.makeUrl('/')], - config: { - screens: { - Root: { - screens: { - Teams: { - screens: { - TeamsScreen: 'teams', - }, - }, - Matches: { - screens: { - MatchesScreen: 'matches', - }, - }, - Sharing: { - screens: { - SharingScreen: 'sharing', - }, - }, - Settings: { - screens: { - SettingsScreen: 'settings', - }, - }, - }, - }, - NotFound: '*', - }, - }, -}; - -export default linking; diff --git a/navigation/index.tsx b/navigation/index.tsx index 1e33879..77ab615 100644 --- a/navigation/index.tsx +++ b/navigation/index.tsx @@ -3,19 +3,17 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { NavigationContainer, DarkTheme } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import * as React from 'react'; -import NotFoundScreen from '../screens/NotFoundScreen'; import TeamsScreen from '../screens/Teams/TeamsScreen'; import MatchesScreen from '../screens/Matches/MatchesScreen'; import { RootStackParamList, RootTabParamList, RootTabScreenProps } from '../types'; -import LinkingConfiguration from './LinkingConfiguration'; import SharingScreen from '../screens/Sharing/SharingScreen'; import SettingsScreen from '../screens/Settings/SettingsScreen'; import { Animated } from 'react-native'; +import { TabBarIcon } from '../components/Themed'; export default function Navigation() { return ( @@ -28,7 +26,6 @@ function RootNavigator() { return ( - ); } @@ -41,7 +38,6 @@ function BottomTabNavigator() { initialRouteName="Teams" screenOptions={{ tabBarActiveTintColor: "#deda04", - unmountOnBlur: true, headerShown: false }}> ); -} - -function TabBarIcon(props: { - name: React.ComponentProps['name']; - color: string; -}) { - return ; } \ No newline at end of file diff --git a/screens/Matches/MatchModal.tsx b/screens/Matches/MatchModal.tsx index d2ba744..b735688 100644 --- a/screens/Matches/MatchModal.tsx +++ b/screens/Matches/MatchModal.tsx @@ -33,7 +33,7 @@ export default function MatchModal(props: ModalProps) visible={true} onRequestClose={() => props.setMatchID("")} > - + diff --git a/screens/NotFoundScreen.tsx b/screens/NotFoundScreen.tsx deleted file mode 100644 index a7032bd..0000000 --- a/screens/NotFoundScreen.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; - -import { RootStackScreenProps } from '../types'; - -export default function NotFoundScreen({ navigation }: RootStackScreenProps<'NotFound'>) { - return ( - - This screen doesn't exist. - navigation.replace('Root')} style={styles.link}> - Go to home screen! - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: 20, - }, - title: { - fontSize: 20, - fontWeight: 'bold', - }, - link: { - marginTop: 15, - paddingVertical: 15, - }, - linkText: { - fontSize: 14, - color: '#2e78b7', - }, -}); diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx index ec12a81..6e06116 100644 --- a/screens/Settings/DownloadingModal.tsx +++ b/screens/Settings/DownloadingModal.tsx @@ -18,7 +18,7 @@ export default function DownloadingModal(props: ModalProps) transparent={true} visible={props.status !== ""} > - + diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index d6a412d..e2e0e1d 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Alert, Modal, ScrollView, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; +import DarkBackground from "../../components/DarkBackground"; import { BlitzDB } from "../../components/Database/BlitzDB"; import { TBAEvent } from "../../components/Database/DBModels"; import { TBA } from "../../components/Database/TBA"; @@ -65,6 +66,8 @@ export default function RegionalModal(props: ModalProps) visible={props.visible} onRequestClose={() => props.setVisible(false)} > + + Regional: - - - - ); + let tempString = ""; + for (let i = 0; i < 500; i++) + tempString += Math.floor(Math.random() * 10).toString(); + return ( + + + + + + ); } const styles = StyleSheet.create({ diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index d9f764f..5edf521 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,6 +1,6 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Alert, Image, Modal, ScrollView, StyleSheet } from "react-native"; +import { Alert, Dimensions, Image, Modal, ScrollView, StyleSheet, TextInput, View } from "react-native"; import DarkBackground from "../../components/DarkBackground"; import { Button, Text, Title } from "../../components/Themed"; import * as ImagePicker from 'expo-image-picker'; @@ -53,7 +53,7 @@ export default function TeamModal(props: ModalProps) visible={true} onRequestClose={() => props.setTeamID("")} > - + @@ -91,7 +91,23 @@ export default function TeamModal(props: ModalProps) {team ? team.name : ""} {team ? team.number : ""} - Team Comments: + + + + + ); +} + +const styles = StyleSheet.create({ + matchButton: { + alignItems: "flex-start" + }, + matchName: { + fontSize: 18, + textAlign: "left" + }, + matchDesc: { + color: "#bbb" + }, +}); \ No newline at end of file diff --git a/screens/Matches/MatchModal.tsx b/screens/Matches/MatchModal.tsx index b735688..2741785 100644 --- a/screens/Matches/MatchModal.tsx +++ b/screens/Matches/MatchModal.tsx @@ -1,19 +1,21 @@ import React from "react"; import { Alert, Modal, ScrollView, StyleSheet } from "react-native"; -import DarkBackground from "../../components/DarkBackground"; -import { BlitzDB } from "../../components/Database/BlitzDB"; -import { Button, Text, Title } from "../../components/Themed"; +import { BlitzDB } from "../../api/BlitzDB"; +import DarkBackground from "../../components/common/DarkBackground"; +import { Button, HorizontalBar, Text, Title } from "../../components/Themed"; +import TeamBanner from "../Teams/TeamBanner"; interface ModalProps { matchID: string; - setMatchID: Function; + isVisible: boolean; + setVisible: (isVisible: boolean) => void; } export default function MatchModal(props: ModalProps) { // Default Behaviour - if (props.matchID === "") + if (!props.isVisible) return null; // Grab Match Data @@ -21,17 +23,25 @@ export default function MatchModal(props: ModalProps) if (!(match)) { Alert.alert("Error", "There was an error grabbing the data from that match. Try re-downloading TBA data then try again."); - props.setMatchID(""); + props.setVisible(false); return null; } + // Grab Team List + let redTeams = []; + let blueTeams = []; + for (let teamID of match.blueTeamIDs) + blueTeams.push(); + for (let teamID of match.redTeamIDs) + redTeams.push(); + // Return Modal return ( props.setMatchID("")} > + onRequestClose={() => props.setVisible(false)} > @@ -40,10 +50,16 @@ export default function MatchModal(props: ModalProps) {match.name} {match.description} - Team Comments: + + + Red Alliance: + {redTeams} + + Blue Alliance: + {blueTeams} - @@ -87,6 +103,7 @@ const styles = StyleSheet.create({ fontSize: 15 }, header: { - fontSize: 24 + fontSize: 24, + marginBottom: 5 } }); diff --git a/screens/Matches/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx index 1f0cd5c..055cef2 100644 --- a/screens/Matches/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -1,13 +1,20 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; -import { ScrollView } from 'react-native-gesture-handler'; -import { BlitzDB } from '../../components/Database/BlitzDB'; +import { BlitzDB } from '../../api/BlitzDB'; import { Text, Container, Title, Button, ScrollContainer } from '../../components/Themed'; +import MatchBanner from './MatchBanner'; import MatchModal from './MatchModal'; export default function MatchesScreen() { const [matchID, setMatchID] = React.useState(""); + const [version, setVersion] = React.useState(0); + + BlitzDB.eventEmitter.addListener("dataUpdate", () => { + BlitzDB.eventEmitter.removeCurrentListener(); + setVersion(version + 1); + }); + let matchDisplay: JSX.Element[] = []; @@ -16,14 +23,7 @@ export default function MatchesScreen() { for (let match of BlitzDB.event.matches) { matchDisplay.push( - + ); } } @@ -38,8 +38,6 @@ export default function MatchesScreen() { Matches {matchDisplay} - - ); } diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx index 6e06116..9b37a7e 100644 --- a/screens/Settings/DownloadingModal.tsx +++ b/screens/Settings/DownloadingModal.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Modal, StyleSheet, View } from "react-native"; -import DarkBackground from "../../components/DarkBackground"; +import DarkBackground from "../../components/common/DarkBackground"; import { Title } from "../../components/Themed"; interface ModalProps diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index e2e0e1d..2b53c1b 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Alert, Modal, ScrollView, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; -import DarkBackground from "../../components/DarkBackground"; -import { BlitzDB } from "../../components/Database/BlitzDB"; -import { TBAEvent } from "../../components/Database/DBModels"; -import { TBA } from "../../components/Database/TBA"; +import { BlitzDB } from "../../api/BlitzDB"; +import { TBAEvent } from "../../api/DBModels"; +import { TBA } from "../../api/TBA"; +import DarkBackground from "../../components/common/DarkBackground"; import { Button, Text, Title } from "../../components/Themed"; import DownloadingModal from "./DownloadingModal"; interface ModalProps diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 140a226..032e807 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { StyleSheet } from 'react-native'; +import { NativeEventEmitter, StyleSheet } from 'react-native'; import DownloadingModal from './DownloadingModal'; import { Text, Container, Title, Button, ScrollContainer } from '../../components/Themed'; import RegionalModal from './RegionalModal'; -import { BlitzDB } from '../../components/Database/BlitzDB'; +import { BlitzDB } from '../../api/BlitzDB'; // BUG "Update Data" is available after a data wipe // TODO More settings @@ -11,6 +11,14 @@ export default function SettingsScreen() { const [modalVisible, setModalVisible] = React.useState(false); const [downloadStatus, setDownloadStatus] = React.useState(""); + const [version, setVersion] = React.useState(0); + + BlitzDB.eventEmitter.addListener("dataUpdate", () => { + BlitzDB.eventEmitter.removeCurrentListener(); + setVersion(version + 1); + }); + + let updateButton; if (BlitzDB.event) @@ -29,7 +37,7 @@ export default function SettingsScreen() ); +} + +const styles = StyleSheet.create({ + teamButton: { + flexDirection: "row", + justifyContent: "flex-start" + }, + teamImage: { + width: 40, + height: 40, + marginRight: 10, + resizeMode: 'stretch' + }, + teamName: { + fontSize: 18, + textAlign: "left" + }, + teamNumber: { + color: "#bbb" + } +}); diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index 5edf521..cf11429 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,24 +1,31 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; import { Alert, Dimensions, Image, Modal, ScrollView, StyleSheet, TextInput, View } from "react-native"; -import DarkBackground from "../../components/DarkBackground"; -import { Button, Text, Title } from "../../components/Themed"; +import DarkBackground from "../../components/common/DarkBackground"; +import { Button, HorizontalBar, Text, Title } from "../../components/Themed"; import * as ImagePicker from 'expo-image-picker'; import PhotoModal from "../../components/PhotoModal"; -import { BlitzDB } from "../../components/Database/BlitzDB"; +import { BlitzDB } from "../../api/BlitzDB"; interface ModalProps { teamID: string; - setTeamID: Function; + isVisible: boolean; + setVisible: (isVisible: boolean) => void; } export default function TeamModal(props: ModalProps) { const [previewData, setPreviewPhoto] = React.useState(""); + const [version, setVersion] = React.useState(0); + + BlitzDB.eventEmitter.addListener("mediaUpdate", () => { + BlitzDB.eventEmitter.removeCurrentListener(); + setVersion(version + 1); + }); // Default Behaviour - if (props.teamID === "") + if (!props.isVisible) return null; // Grab Team Data @@ -26,7 +33,7 @@ export default function TeamModal(props: ModalProps) if (!(team)) { Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); - props.setTeamID(""); + props.setVisible(false); return null; } @@ -51,7 +58,7 @@ export default function TeamModal(props: ModalProps) animationType="slide" transparent={true} visible={true} - onRequestClose={() => props.setTeamID("")} > + onRequestClose={() => props.setVisible(false)} > @@ -91,7 +98,9 @@ export default function TeamModal(props: ModalProps) {team ? team.name : ""} {team ? team.number : ""} - + + + - @@ -191,7 +200,7 @@ const styles = StyleSheet.create({ color: "#fff", backgroundColor: "#222222", borderRadius: 10, - paddingLeft: 10, + padding: 10, marginBottom: 10, marginTop: -10, marginRight: 10, diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index 3bf5c14..0342624 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -1,69 +1,43 @@ import * as React from 'react'; -import { Image, StyleSheet, View } from 'react-native'; -import { BlitzDB } from '../../components/Database/BlitzDB'; -import { Text, Title, Button, ScrollContainer } from '../../components/Themed'; +import { StyleSheet } from 'react-native'; +import { BlitzDB } from '../../api/BlitzDB'; +import TeamBanner from './TeamBanner'; +import { Text, Title, ScrollContainer } from '../../components/Themed'; import { RootTabScreenProps } from '../../types'; -import TeamModal from './TeamModal'; export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) { - const [teamID, setTeamID] = React.useState(""); + const [version, setVersion] = React.useState(0); + + BlitzDB.eventEmitter.addListener("dataUpdate", () => { + BlitzDB.eventEmitter.removeCurrentListener(); + setVersion(version + 1); + }); + - let teamDisplay: JSX.Element[] = []; + let teamList: JSX.Element[] = []; - if (BlitzDB.currentTeams.length > 0) + if (BlitzDB.currentTeamIDs.length > 0) { - for (let team of BlitzDB.currentTeams) + for (let teamID of BlitzDB.currentTeamIDs) { - teamDisplay.push( - + teamList.push( + ); } } else { - teamDisplay.push( + teamList.push( Team data has not been downloaded from TBA yet. Download is available under settings ); } return ( - - Teams - {teamDisplay} + {teamList} ); } -const styles = StyleSheet.create({ - teamButton: { - flexDirection: "row", - justifyContent: "flex-start" - }, - teamImage: { - width: 40, - height: 40, - marginRight: 10, - resizeMode: 'stretch' - }, - teamName: { - fontSize: 18, - textAlign: "left" - }, - teamNumber: { - color: "#bbb" - } -}); +const styles = StyleSheet.create({}); diff --git a/yarn.lock b/yarn.lock index 8e72aeb..47d99bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,6 +2271,11 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/lz-string@^1.3.34": + version "1.3.34" + resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5" + integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow== + "@types/node@*": version "16.10.1" resolved "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz" @@ -4729,7 +4734,7 @@ hermes-profile-transformer@^0.0.6: dependencies: source-map "^0.7.3" -hoist-non-react-statics@^3.3.0: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -6055,6 +6060,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" @@ -7289,6 +7299,11 @@ react-is@^17.0.1: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-native-gesture-handler@~1.10.2: version "1.10.3" resolved "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz" @@ -7300,6 +7315,11 @@ react-native-gesture-handler@~1.10.2: invariant "^2.2.4" prop-types "^15.7.2" +react-native-iphone-x-helper@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" + integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== + react-native-qrcode-generator@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/react-native-qrcode-generator/-/react-native-qrcode-generator-1.2.2.tgz#5e475ba2bbe4496eb78bc37c2f5b0c1c8ff7f247" @@ -7339,6 +7359,11 @@ react-native-svg@^12.1.1: css-select "^2.1.0" css-tree "^1.0.0-alpha.39" +react-native-tab-view@^2.15.2: + version "2.16.0" + resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz#cae72c7084394bd328fac5fefb86cd966df37a86" + integrity sha512-ac2DmT7+l13wzIFqtbfXn4wwfgtPoKzWjjZyrK1t+T8sdemuUvD4zIt+UImg03fu3s3VD8Wh/fBrIdcqQyZJWg== + react-native-web@~0.13.12: version "0.13.18" resolved "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.18.tgz" @@ -7396,6 +7421,16 @@ react-native-webview@9.0.1: use-subscription "^1.0.0" whatwg-fetch "^3.0.0" +react-navigation-tabs@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-2.11.1.tgz#dd2ccb04c540b4439aadc4bc8f5a776dfc90439f" + integrity sha512-wq2wR3awu6PKimmVOycBf+iTPA9FWThbJwcaDBQEhQiiviXQzAtT3lw3nV9oqNIg4v65tdPhL1Dg8ptTJ03NjQ== + dependencies: + hoist-non-react-statics "^3.3.2" + react-lifecycles-compat "^3.0.4" + react-native-iphone-x-helper "^1.3.0" + react-native-tab-view "^2.15.2" + react-qr-code@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8" From 39ef5d346c1bad38cab28f79fcc4a62e7de8b906 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Fri, 8 Oct 2021 12:05:22 -0500 Subject: [PATCH 07/38] Bug Fixes, Comments, and Share Design --- api/BlitzDB.ts | 19 +- api/DBModels.ts | 2 +- components/Themed.tsx | 117 ------------ components/common/Button.tsx | 14 ++ components/common/DarkBackground.tsx | 2 +- components/common/FadeIn.tsx | 35 ++++ components/common/HorizontalBar.tsx | 19 ++ components/common/Modal.tsx | 75 ++++++++ components/common/TabBarIcon.tsx | 19 ++ components/common/Text.tsx | 10 + components/common/Title.tsx | 15 ++ components/containers/Container.tsx | 10 + components/{ => containers}/PhotoModal.tsx | 4 +- components/containers/ScrollContainer.tsx | 23 +++ navigation/index.tsx | 2 +- package.json | 4 +- screens/Matches/MatchBanner.tsx | 3 +- screens/Matches/MatchModal.tsx | 63 ++---- screens/Matches/MatchesScreen.tsx | 6 +- screens/Settings/DownloadingModal.tsx | 2 +- screens/Settings/RegionalModal.tsx | 79 +++----- screens/Settings/SettingsScreen.tsx | 7 +- screens/Sharing/ExportQRModal.tsx | 13 ++ screens/Sharing/ImportQRModal.tsx | 13 ++ screens/Sharing/SharingScreen.tsx | 124 +++++++++--- screens/Teams/TeamBanner.tsx | 6 +- screens/Teams/TeamModal.tsx | 212 +++++++++++---------- screens/Teams/TeamsScreen.tsx | 7 +- yarn.lock | 26 +-- 29 files changed, 543 insertions(+), 388 deletions(-) delete mode 100644 components/Themed.tsx create mode 100644 components/common/Button.tsx create mode 100644 components/common/FadeIn.tsx create mode 100644 components/common/HorizontalBar.tsx create mode 100644 components/common/Modal.tsx create mode 100644 components/common/TabBarIcon.tsx create mode 100644 components/common/Text.tsx create mode 100644 components/common/Title.tsx create mode 100644 components/containers/Container.tsx rename components/{ => containers}/PhotoModal.tsx (91%) create mode 100644 components/containers/ScrollContainer.tsx create mode 100644 screens/Sharing/ExportQRModal.tsx create mode 100644 screens/Sharing/ImportQRModal.tsx diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts index 45d825a..eeb632e 100644 --- a/api/BlitzDB.ts +++ b/api/BlitzDB.ts @@ -170,6 +170,23 @@ export class BlitzDB team.media.push(BASE64_PREFIX + imageData); BlitzDB.save(); BlitzDB.eventEmitter.emit("mediaUpdate"); + console.log("Added Media to " + teamID); + } + } + + static addTeamComment(teamID: string, commentText: string) + { + let team = BlitzDB.getTeam(teamID); + if (team) + { + team.comments.push({ + isScanned: false, + timestamp: (new Date()).getTime(), + text: commentText + }); + BlitzDB.save(); + BlitzDB.eventEmitter.emit("mediaUpdate"); + console.log("Added Comment to " + teamID); } } @@ -193,7 +210,7 @@ export class BlitzDB data += ";;" + team.id; for (let comment of team.comments) { - data += "::" + comment; + data += "::" + comment.text; } } diff --git a/api/DBModels.ts b/api/DBModels.ts index 0e8156f..74bbb69 100644 --- a/api/DBModels.ts +++ b/api/DBModels.ts @@ -45,7 +45,7 @@ export interface Team name: string; number: number; media: string[]; - comments: string[]; + comments: Comment[]; } export interface Match { diff --git a/components/Themed.tsx b/components/Themed.tsx deleted file mode 100644 index baf457e..0000000 --- a/components/Themed.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { FontAwesome } from '@expo/vector-icons'; -import { useFocusEffect, useIsFocused } from '@react-navigation/core'; -import * as React from 'react'; -import { ScrollView, StyleSheet, Text as DefaultText, View as DefaultView, TouchableOpacity as DefaultButton, Animated, View } from 'react-native'; - -export type TextProps = DefaultText['props']; -export type ViewProps = DefaultView['props']; -export type ButtonProps = DefaultButton['props']; - -const styles = StyleSheet.create({ - scrollContainer: { - marginTop: 40, - marginBottom: 0, - - paddingTop: 30, - paddingLeft: 20, - paddingRight: 20, - paddingBottom: 0, - - }, - title: { - color: "#fff", - fontSize: 40, - fontWeight: 'bold', - marginBottom: 20 - }, - text: { - color: "#fff" - }, - button: { - alignItems: "center", - padding: 10, - alignSelf: 'stretch', - } -}); - -// Button -export function Button(props: ButtonProps) { - const { style, ...otherProps } = props; - - return ; -} - -// Text -export function Text(props: TextProps) { - const { style, ...otherProps } = props; - - return ; -} - -// Title -export function Title(props: TextProps) { - const { style, ...otherProps } = props; - - return ; - } - -// Containers -export function Container(props: ViewProps) { - const { style, ...otherProps } = props; - - return (); -} -export function ScrollContainer(props: ViewProps) { - const { style, ...otherProps } = props; - - // BUG Gap at the end of each scroll view - return (); -} - -// Fade Animation -function FadeIn(props: ViewProps) { - const fadeAnim = React.useRef(new Animated.Value(0)).current; - - useFocusEffect(() => { - Animated.timing(fadeAnim, { - toValue: 1, - duration: 300, - useNativeDriver: true - }).start(); - - return () => { - Animated.timing(fadeAnim, { - toValue: 0, - duration: 300, - useNativeDriver: true - }).start(); - } - }); - - return ( - {props.children} - - ); -} - -export function TabBarIcon(props: { - name: React.ComponentProps['name']; - color: string; -}) { - return ; -} - -export function HorizontalBar(props: {}) -{ - return (); -} \ No newline at end of file diff --git a/components/common/Button.tsx b/components/common/Button.tsx new file mode 100644 index 0000000..678caba --- /dev/null +++ b/components/common/Button.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { TouchableOpacity } from "react-native"; + +export type ButtonProps = TouchableOpacity['props']; + +export default function Button(props: ButtonProps) { + const { style, ...otherProps } = props; + + return ; +} \ No newline at end of file diff --git a/components/common/DarkBackground.tsx b/components/common/DarkBackground.tsx index b3c3bd2..42e1805 100644 --- a/components/common/DarkBackground.tsx +++ b/components/common/DarkBackground.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import * as React from 'react'; import { StyleSheet, View } from "react-native"; interface DarkBackgroundProps diff --git a/components/common/FadeIn.tsx b/components/common/FadeIn.tsx new file mode 100644 index 0000000..59980a6 --- /dev/null +++ b/components/common/FadeIn.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { useFocusEffect } from "@react-navigation/core"; +import { Animated, View } from "react-native"; + +export type ViewProps = View['props']; + +export default function FadeIn(props: ViewProps) +{ + const { style, ...otherProps } = props; + const fadeAnim = React.useRef(new Animated.Value(0)).current; + + useFocusEffect(() => { + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true + }).start(); + + return () => { + Animated.timing(fadeAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true + }).start(); + } + }); + + return ( + ); +} \ No newline at end of file diff --git a/components/common/HorizontalBar.tsx b/components/common/HorizontalBar.tsx new file mode 100644 index 0000000..b50f18a --- /dev/null +++ b/components/common/HorizontalBar.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { View } from "react-native"; + +export type ViewProps = View['props']; + +export default function HorizontalBar(props: ViewProps) +{ + const { style, ...otherProps } = props; + + return (); +} \ No newline at end of file diff --git a/components/common/Modal.tsx b/components/common/Modal.tsx new file mode 100644 index 0000000..d78029c --- /dev/null +++ b/components/common/Modal.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { ReactNode } from "react"; +import { StyleSheet, Modal as DefaultModal, View, ScrollView } from "react-native"; +import Button from './Button'; +import DarkBackground from "./DarkBackground"; +import Text from './Text'; + +export interface ModalProps +{ + setVisible: (isVisible: boolean) => void; + children: ReactNode; +} + +export default function Modal(props: ModalProps) +{ + return ( + props.setVisible(false)} > + + + + + + + {props.children} + + + + + + + ); +} + +const styles = StyleSheet.create({ + scrollView: { + marginBottom: 60 + }, + outerView: { + backgroundColor: "#0c0c0c", + flex: 1, + borderRadius: 10, + marginTop: 10, + marginBottom: 20, + marginLeft: 5, + marginRight: 5 + }, + innerView: { + paddingTop: 30, + paddingLeft: 20, + paddingRight: 20 + }, + button: { + backgroundColor: "#deda04", + position: "absolute", + bottom: 35, + right: 20, + left: 20, + borderRadius: 10 + }, + buttonText: { + color: "#000" + }, +}); \ No newline at end of file diff --git a/components/common/TabBarIcon.tsx b/components/common/TabBarIcon.tsx new file mode 100644 index 0000000..b766e7c --- /dev/null +++ b/components/common/TabBarIcon.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { FontAwesome } from "@expo/vector-icons"; +import { View } from "react-native"; + +export type ViewProps = View['props']; +export interface TabBarIconProps +{ + name: React.ComponentProps['name'], + color: string +} + +export function TabBarIcon(props: TabBarIconProps) +{ + return (); +} \ No newline at end of file diff --git a/components/common/Text.tsx b/components/common/Text.tsx new file mode 100644 index 0000000..57b1379 --- /dev/null +++ b/components/common/Text.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { Text as DefaultText } from "react-native"; + +export type TextProps = DefaultText['props']; + +export default function Text(props: TextProps) { + const { style, ...otherProps } = props; + + return ; +} \ No newline at end of file diff --git a/components/common/Title.tsx b/components/common/Title.tsx new file mode 100644 index 0000000..cf68525 --- /dev/null +++ b/components/common/Title.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { Text } from "react-native"; + +export type TextProps = Text['props']; + +export default function Title(props: TextProps) { + const { style, ...otherProps } = props; + + return ; +} \ No newline at end of file diff --git a/components/containers/Container.tsx b/components/containers/Container.tsx new file mode 100644 index 0000000..331e50f --- /dev/null +++ b/components/containers/Container.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { View } from "react-native"; +import FadeIn from "../common/FadeIn"; + +export type ViewProps = View['props']; + +export default function Container(props: ViewProps) { + + return (); +} diff --git a/components/PhotoModal.tsx b/components/containers/PhotoModal.tsx similarity index 91% rename from components/PhotoModal.tsx rename to components/containers/PhotoModal.tsx index 372d375..1b42d04 100644 --- a/components/PhotoModal.tsx +++ b/components/containers/PhotoModal.tsx @@ -1,11 +1,11 @@ import React from "react"; import { Dimensions, Image, Modal, StyleSheet, View } from "react-native"; -import DarkBackground from "./common/DarkBackground"; +import DarkBackground from "../common/DarkBackground"; interface PhotoProps { imageData: string; - setImageData: Function; + setImageData: (imageData: string) => void; } // TODO Improve Photo Modal diff --git a/components/containers/ScrollContainer.tsx b/components/containers/ScrollContainer.tsx new file mode 100644 index 0000000..c50b6cc --- /dev/null +++ b/components/containers/ScrollContainer.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { ScrollView, View } from "react-native"; +import FadeIn from '../common/FadeIn'; + +export type ViewProps = View['props']; + +export default function ScrollContainer(props: ViewProps) { + const { style, ...otherProps } = props; + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/navigation/index.tsx b/navigation/index.tsx index 7fe8008..76f9b32 100644 --- a/navigation/index.tsx +++ b/navigation/index.tsx @@ -7,8 +7,8 @@ import MatchesScreen from '../screens/Matches/MatchesScreen'; import { RootStackParamList, RootTabParamList } from '../types'; import SharingScreen from '../screens/Sharing/SharingScreen'; import SettingsScreen from '../screens/Settings/SettingsScreen'; -import { TabBarIcon } from '../components/Themed'; import { BlitzDB } from '../api/BlitzDB'; +import { TabBarIcon } from '../components/common/TabBarIcon'; export default function Navigation() { return ( diff --git a/package.json b/package.json index 2321d55..18dbd00 100644 --- a/package.json +++ b/package.json @@ -33,15 +33,13 @@ "react-dom": "16.13.1", "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", "react-native-gesture-handler": "~1.10.2", - "react-native-qrcode-generator": "1.2.2", "react-native-reanimated": "~2.2.0", "react-native-safe-area-context": "3.2.0", "react-native-screens": "~3.4.0", "react-native-svg": "^12.1.1", "react-native-web": "~0.13.12", "react-native-webview": "9.0.1", - "react-navigation-tabs": "^2.11.1", - "react-qr-code": "^2.0.2" + "react-navigation-tabs": "^2.11.1" }, "devDependencies": { "@babel/core": "^7.9.0", diff --git a/screens/Matches/MatchBanner.tsx b/screens/Matches/MatchBanner.tsx index e09956f..8404cd6 100644 --- a/screens/Matches/MatchBanner.tsx +++ b/screens/Matches/MatchBanner.tsx @@ -1,7 +1,8 @@ import React from "react"; import { StyleSheet } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; -import { Button, Text } from "../../components/Themed"; +import Button from "../../components/common/Button"; +import Text from "../../components/common/Text"; import MatchModal from "./MatchModal"; interface MatchBannerProps diff --git a/screens/Matches/MatchModal.tsx b/screens/Matches/MatchModal.tsx index 2741785..652333d 100644 --- a/screens/Matches/MatchModal.tsx +++ b/screens/Matches/MatchModal.tsx @@ -1,8 +1,12 @@ import React from "react"; -import { Alert, Modal, ScrollView, StyleSheet } from "react-native"; +import { Alert, ScrollView, StyleSheet } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; +import Button from "../../components/common/Button"; import DarkBackground from "../../components/common/DarkBackground"; -import { Button, HorizontalBar, Text, Title } from "../../components/Themed"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import Modal from "../../components/common/Modal"; +import Text from "../../components/common/Text"; +import Title from "../../components/common/Title"; import TeamBanner from "../Teams/TeamBanner"; interface ModalProps @@ -37,31 +41,18 @@ export default function MatchModal(props: ModalProps) // Return Modal return ( - props.setVisible(false)} > - - - - + - {match.name} - {match.description} + {match.name} + {match.description} - - - Red Alliance: - {redTeams} - - Blue Alliance: - {blueTeams} - + - + Red Alliance: + {redTeams} + + Blue Alliance: + {blueTeams} ); } @@ -71,30 +62,6 @@ const styles = StyleSheet.create({ flexDirection: "row", marginBottom: 10 }, - button: { - backgroundColor: "#deda04", - position: "absolute", - bottom: 35, - right: 20, - left: 20, - borderRadius: 10 - }, - buttonText: { - color: "#000" - }, - modal: { - backgroundColor: "#0b0b0b", - flex: 1, - borderRadius: 10, - marginTop: 10, - marginBottom: 20, - marginLeft: 5, - marginRight: 5, - paddingTop: 30, - paddingLeft: 20, - paddingRight: 20, - paddingBottom: 70, - }, title: { marginBottom: 0 }, diff --git a/screens/Matches/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx index 055cef2..d6ec620 100644 --- a/screens/Matches/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; import { BlitzDB } from '../../api/BlitzDB'; - -import { Text, Container, Title, Button, ScrollContainer } from '../../components/Themed'; +import Text from '../../components/common/Text'; +import Title from '../../components/common/Title'; +import ScrollContainer from '../../components/containers/ScrollContainer'; import MatchBanner from './MatchBanner'; -import MatchModal from './MatchModal'; export default function MatchesScreen() { const [matchID, setMatchID] = React.useState(""); diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx index 9b37a7e..e6a51c9 100644 --- a/screens/Settings/DownloadingModal.tsx +++ b/screens/Settings/DownloadingModal.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Modal, StyleSheet, View } from "react-native"; import DarkBackground from "../../components/common/DarkBackground"; -import { Title } from "../../components/Themed"; +import Title from "../../components/common/Title"; interface ModalProps { diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index 2b53c1b..f38ead7 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -1,16 +1,20 @@ import React from "react"; -import { Alert, Modal, ScrollView, StyleSheet, View } from "react-native"; +import { Alert, ScrollView, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; import { BlitzDB } from "../../api/BlitzDB"; import { TBAEvent } from "../../api/DBModels"; import { TBA } from "../../api/TBA"; +import Button from "../../components/common/Button"; import DarkBackground from "../../components/common/DarkBackground"; -import { Button, Text, Title } from "../../components/Themed"; +import Modal from "../../components/common/Modal"; +import Text from "../../components/common/Text"; +import Title from "../../components/common/Title"; import DownloadingModal from "./DownloadingModal"; + interface ModalProps { - visible: boolean; - setVisible: Function; + isVisible: boolean; + setVisible: (isVisible: boolean) => void; } export default function RegionalModal(props: ModalProps) @@ -19,9 +23,13 @@ export default function RegionalModal(props: ModalProps) const [regionalList, updateRegionals] = React.useState([] as TBAEvent[]); const [downloadStatus, setDownloadStatus] = React.useState(""); + // Default Behaviour + if (!props.isVisible) + return null; + // Generate List let regionalsDisplay: JSX.Element[] = []; - if (regionalList.length <= 0 && props.visible) + if (regionalList.length <= 0 && props.isVisible) { TBA.getEvents().then((events) => { if (events) @@ -42,6 +50,7 @@ export default function RegionalModal(props: ModalProps) { let key = regional.key; if (regional.name.toLowerCase().includes(searchTerm)) + { regionalsDisplay.push( - ) + ); + } } } // Display Data return ( - props.setVisible(false)} > - - - - - Regional: - {updateSearch(text.toLowerCase())}} - /> - - {regionalsDisplay} - - - - + + Regional: + {updateSearch(text.toLowerCase())}} + /> + {regionalsDisplay} @@ -91,17 +86,6 @@ export default function RegionalModal(props: ModalProps) } const styles = StyleSheet.create({ - button: { - backgroundColor: "#deda04", - position: "absolute", - bottom: 35, - right: 20, - left: 20, - borderRadius: 10 - }, - buttonText: { - color: "#000" - }, regionalButton: { padding: 8, marginLeft: 4, @@ -118,19 +102,6 @@ const styles = StyleSheet.create({ marginBottom: 10, marginTop: -10 }, - modal: { - backgroundColor: "#0b0b0b", - flex: 1, - borderRadius: 10, - marginTop: 10, - marginBottom: 20, - marginLeft: 5, - marginRight: 5, - paddingTop: 30, - paddingLeft: 20, - paddingRight: 20, - paddingBottom: 70, - }, loadingText: { textAlign: "center", fontStyle: "italic" diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 032e807..5965014 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import { NativeEventEmitter, StyleSheet } from 'react-native'; import DownloadingModal from './DownloadingModal'; -import { Text, Container, Title, Button, ScrollContainer } from '../../components/Themed'; import RegionalModal from './RegionalModal'; import { BlitzDB } from '../../api/BlitzDB'; +import Button from '../../components/common/Button'; +import Title from '../../components/common/Title'; +import Text from '../../components/common/Text'; +import ScrollContainer from '../../components/containers/ScrollContainer'; // BUG "Update Data" is available after a data wipe // TODO More settings @@ -45,7 +48,7 @@ export default function SettingsScreen() Clear All Data - + ); diff --git a/screens/Sharing/ExportQRModal.tsx b/screens/Sharing/ExportQRModal.tsx new file mode 100644 index 0000000..17ab8bd --- /dev/null +++ b/screens/Sharing/ExportQRModal.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +export interface ModalProps +{ + isVisible: boolean; + setVisible: (isVisible: boolean) => void; +} + +export default function ExportQRModal(props: ModalProps) +{ + // TODO: Generate and display QRCode + return null; +} \ No newline at end of file diff --git a/screens/Sharing/ImportQRModal.tsx b/screens/Sharing/ImportQRModal.tsx new file mode 100644 index 0000000..0a5e0fd --- /dev/null +++ b/screens/Sharing/ImportQRModal.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +export interface ModalProps +{ + isVisible: boolean; + setVisible: (isVisible: boolean) => void; +} + +export default function ImportQRModal(props: ModalProps) +{ + // TODO: View and import QRCode + return null; +} \ No newline at end of file diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index c8e4bcf..3fe2a78 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -1,36 +1,114 @@ -import LZString from 'lz-string'; +import { FontAwesome } from '@expo/vector-icons'; import * as React from 'react'; -import { Dimensions, StyleSheet, View } from 'react-native'; -import QRCode from "react-qr-code"; -import { BlitzDB } from '../../api/BlitzDB'; -import { Container, HorizontalBar, Text } from '../../components/Themed'; - -const textData = "Pigeons in holes. Here there are n = 10 pigeons in m = 9 holes. Since 10 is greater than 9, the pigeonhole principle says that at least one hole has more than one pigeon. (The top left hole has 2 pigeons.) In mathematics, the pigeonhole principle states that if n items are put into m containers, with n > m, then at least one container must contain more than one item. For example, if one has three gloves (and none is ambidextrous/reversible), then there must be at least two right-handed gloves, or at least two left-handed gloves, because there are three objects, but only two categories of handedness to put them into."; +import { StyleSheet, View } from 'react-native'; +import Button from '../../components/common/Button'; +import ScrollContainer from '../../components/containers/ScrollContainer'; +import Text from '../../components/common/Text'; +import Title from '../../components/common/Title'; +import ExportQRModal from './ExportQRModal'; +import ImportQRModal from './ImportQRModal'; export default function SharingScreen() { + const [isExportQRVisible, setExportQRVisible] = React.useState(false); + const [isImportQRVisible, setImportQRVisible] = React.useState(false); - const deviceWidth = Dimensions.get("window").width; - - const commentData = BlitzDB.exportComments(); - const compressedData = LZString.compress(commentData); - return ( - + - + Sharing + + {/* Export QRCode */} + + + + {/* Import QRCode */} + + + + {/* Export CSV */} + + + {/* Export CSV */} + + + {/* Export Cloud */} + + + {/* Import Cloud */} + - + ); } const styles = StyleSheet.create({ - container: { - marginTop: 150, - justifyContent: "center", - flexDirection: "column" + sharingButton: { + flexDirection: "row", + justifyContent: "flex-start" + }, + buttonIcon: { + color: "#fff", + marginRight: 10 + }, + buttonTitle: { + fontSize: 18, + textAlign: "left" + }, + buttonSubtitle: { + color: "#bbb" } }); diff --git a/screens/Teams/TeamBanner.tsx b/screens/Teams/TeamBanner.tsx index 406ff83..ac128f3 100644 --- a/screens/Teams/TeamBanner.tsx +++ b/screens/Teams/TeamBanner.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Image, StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; -import { Team } from "../../api/DBModels"; +import Button from "../../components/common/Button"; +import Text from "../../components/common/Text"; import TeamModal from "./TeamModal"; -import { Button, Text } from "../../components/Themed"; interface TeamBannerProps { @@ -17,7 +17,7 @@ export default function TeamBanner(props: TeamBannerProps) let team = BlitzDB.getTeam(props.teamID); if (!team) { - console.log("Could not find (TeamBanner ID=" + props.teamID + ")"); + console.log("Could not find Team ID " + props.teamID); return null; } diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index cf11429..20a8487 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,11 +1,14 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Alert, Dimensions, Image, Modal, ScrollView, StyleSheet, TextInput, View } from "react-native"; -import DarkBackground from "../../components/common/DarkBackground"; -import { Button, HorizontalBar, Text, Title } from "../../components/Themed"; +import { Alert, Dimensions, Image, ScrollView, StyleSheet, TextInput, View } from "react-native"; import * as ImagePicker from 'expo-image-picker'; -import PhotoModal from "../../components/PhotoModal"; +import PhotoModal from "../../components/containers/PhotoModal"; import { BlitzDB } from "../../api/BlitzDB"; +import Button from "../../components/common/Button"; +import Title from "../../components/common/Title"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import Text from "../../components/common/Text"; +import Modal from "../../components/common/Modal"; interface ModalProps { @@ -19,14 +22,15 @@ export default function TeamModal(props: ModalProps) const [previewData, setPreviewPhoto] = React.useState(""); const [version, setVersion] = React.useState(0); + // Default Behaviour + if (!props.isVisible) + return null; + + // Re-render on New Media BlitzDB.eventEmitter.addListener("mediaUpdate", () => { BlitzDB.eventEmitter.removeCurrentListener(); setVersion(version + 1); }); - - // Default Behaviour - if (!props.isVisible) - return null; // Grab Team Data let team = BlitzDB.getTeam(props.teamID); @@ -52,78 +56,106 @@ export default function TeamModal(props: ModalProps) ); } + // Grab Team Comments + let commentList: JSX.Element[] = []; + if (team.comments.length > 0) + { + for (let comment of team.comments) + { + let commentTime = new Date(comment.timestamp); + commentList.push( + + {comment.text} + {commentTime.getHours() + ":" + commentTime.getMinutes()} + + ); + } + } + else + { + commentList.push( + There are no comments yet... + ); + } + + // Comment Data + let commentText: string; + let commentInput: TextInput | null; + // Return Modal return ( - props.setVisible(false)} > - - - - - - {mediaList} - - - - - - - {team ? team.name : ""} - {team ? team.number : ""} - - - - - - - - + + + + {mediaList} + + + + + + {team ? team.name : ""} + {team ? team.number : ""} + + + + + { commentText = text }} + ref={input => {commentInput = input}} + /> + + + + {commentList} - - + + ); } @@ -172,30 +204,6 @@ const styles = StyleSheet.create({ height: 150, width: "100%" }, - button: { - backgroundColor: "#deda04", - position: "absolute", - bottom: 35, - right: 20, - left: 20, - borderRadius: 10 - }, - buttonText: { - color: "#000" - }, - modal: { - backgroundColor: "#0c0c0c", - flex: 1, - borderRadius: 10, - marginTop: 10, - marginBottom: 20, - marginLeft: 5, - marginRight: 5, - paddingTop: 30, - paddingLeft: 20, - paddingRight: 20, - paddingBottom: 70, - }, textInput: { color: "#fff", backgroundColor: "#222222", @@ -231,5 +239,11 @@ const styles = StyleSheet.create({ flexDirection: "row", backgroundColor: "#333", margin: 5 + }, + comment: { + backgroundColor: "#111", + borderRadius: 10, + marginBottom: 5, + padding: 10 } }); diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index 0342624..152b1c0 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, Text } from 'react-native'; import { BlitzDB } from '../../api/BlitzDB'; import TeamBanner from './TeamBanner'; -import { Text, Title, ScrollContainer } from '../../components/Themed'; import { RootTabScreenProps } from '../../types'; +import Title from '../../components/common/Title'; +import ScrollContainer from '../../components/containers/ScrollContainer'; export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) { const [version, setVersion] = React.useState(0); @@ -39,5 +40,3 @@ export default function TeamsScreen({ navigation }: RootTabScreenProps<'Teams'>) ); } - -const styles = StyleSheet.create({}); diff --git a/yarn.lock b/yarn.lock index 47d99bf..79fc9f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3447,7 +3447,7 @@ cosmiconfig@^5.0.5, cosmiconfig@^5.1.0: js-yaml "^3.13.1" parse-json "^4.0.0" -create-react-class@^15.6.2, create-react-class@^15.6.3: +create-react-class@^15.6.2: version "15.7.0" resolved "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz" integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== @@ -7188,7 +7188,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7220,11 +7220,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qr.js@0.0.0, qr.js@^0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" - integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= - qs@^6.5.0: version "6.10.1" resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz" @@ -7320,15 +7315,6 @@ react-native-iphone-x-helper@^1.3.0: resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== -react-native-qrcode-generator@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/react-native-qrcode-generator/-/react-native-qrcode-generator-1.2.2.tgz#5e475ba2bbe4496eb78bc37c2f5b0c1c8ff7f247" - integrity sha512-M75uyU3zL8yuL1ppL6OJsKBhL+VDZE3jNI5PAvDv+BCKa3MiSTTpXbGEqu7Y4xlhvFUozes/C0Ia7aS3mQ5w6Q== - dependencies: - create-react-class "^15.6.3" - prop-types "^15.5.10" - qr.js "^0.0.0" - react-native-reanimated@~2.2.0: version "2.2.2" resolved "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.2.2.tgz" @@ -7431,14 +7417,6 @@ react-navigation-tabs@^2.11.1: react-native-iphone-x-helper "^1.3.0" react-native-tab-view "^2.15.2" -react-qr-code@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8" - integrity sha512-73VGe81MgeE5FJNFgdY42ez/wPPJTHuooU3iE4CX+6F8M88O1Gg4zNA0L4bKEpoySQ0QjqreJgyXjFrG/QfsdA== - dependencies: - prop-types "^15.7.2" - qr.js "0.0.0" - react-refresh@^0.4.0: version "0.4.3" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz" From feb89373804b5f4600ff4e01de010a5e8fe1c5f9 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Mon, 11 Oct 2021 18:19:00 -0500 Subject: [PATCH 08/38] Logo & QRCodes --- assets/images/adaptive-icon.png | Bin 22996 -> 24444 bytes assets/images/favicon.png | Bin 22996 -> 24444 bytes assets/images/icon.png | Bin 22996 -> 24444 bytes components/containers/PhotoModal.tsx | 1 - package.json | 3 +- screens/Sharing/ExportQRModal.tsx | 46 +++++++++++++++++++++++++-- screens/Sharing/SharingScreen.tsx | 11 ++++--- yarn.lock | 13 ++++++++ 8 files changed, 64 insertions(+), 10 deletions(-) diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png index 20bfaba609425295abbeb34d585fdfb95baf97a9..e5a3733d7e7e3408c17c8ad53f5e916555ff85cd 100644 GIT binary patch literal 24444 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 kPuV)dVh!X*1-dS9TZ%8K_{~?`!tWh0G% props.setImageData("")} > diff --git a/package.json b/package.json index 18dbd00..67a46c5 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "react-native-svg": "^12.1.1", "react-native-web": "~0.13.12", "react-native-webview": "9.0.1", - "react-navigation-tabs": "^2.11.1" + "react-navigation-tabs": "^2.11.1", + "react-qr-code": "^2.0.2" }, "devDependencies": { "@babel/core": "^7.9.0", diff --git a/screens/Sharing/ExportQRModal.tsx b/screens/Sharing/ExportQRModal.tsx index 17ab8bd..57cddbb 100644 --- a/screens/Sharing/ExportQRModal.tsx +++ b/screens/Sharing/ExportQRModal.tsx @@ -1,4 +1,9 @@ +import LZString from 'lz-string'; import * as React from 'react'; +import { Dimensions, Image, Modal, StyleSheet, View } from 'react-native'; +import QRCode from 'react-qr-code'; +import { BlitzDB } from '../../api/BlitzDB'; +import DarkBackground from '../../components/common/DarkBackground'; export interface ModalProps { @@ -8,6 +13,41 @@ export interface ModalProps export default function ExportQRModal(props: ModalProps) { - // TODO: Generate and display QRCode - return null; -} \ No newline at end of file + // Default Behaviour + if (!props.isVisible) + return null; + + const deviceWidth = Dimensions.get("window").width; + const commentData = BlitzDB.exportComments(); + const compressedData = LZString.compress(commentData); + + return ( + props.setVisible(false)} > + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: "absolute", + justifyContent: "center", + top: 0, + left: 0, + bottom: 0, + right: 0 + } +}); diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index 3fe2a78..f69f830 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -19,7 +19,7 @@ export default function SharingScreen() { {/* Export QRCode */} - - {/* Export CSV */} + {/* Export ZIP */} + ); +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 10, + flexDirection: "row", + height: 150 + }, + subContainer: { + width: "100%" + }, + background: { + position: "absolute", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "black" + }, + thumbnail: { + height: 150, + width: 150, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#444", + marginRight: 15, + }, + teamName: { + fontSize: 18, + fontWeight: "bold", + textAlign: "left", + width: 150 + }, + teamDesc: { + color: "#bbb", + fontWeight: "bold" + }, +}); diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx index e6a51c9..9bee54f 100644 --- a/screens/Settings/DownloadingModal.tsx +++ b/screens/Settings/DownloadingModal.tsx @@ -1,7 +1,8 @@ import React from "react"; import { Modal, StyleSheet, View } from "react-native"; import DarkBackground from "../../components/common/DarkBackground"; -import Title from "../../components/common/Title"; +import Subtitle from "../../components/text/Subtitle"; +import Title from "../../components/text/Title"; interface ModalProps { @@ -22,8 +23,8 @@ export default function DownloadingModal(props: ModalProps) - Downloading, Please Wait - {props.status} + Downloading, Please Wait + {props.status} @@ -42,13 +43,5 @@ const styles = StyleSheet.create({ paddingLeft: 20, paddingRight: 20, paddingBottom: 80, - }, - title: { - fontSize: 24, - marginBottom: 0 - }, - subtitle: { - color: "#bbb", - fontSize: 18 } }); diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index f38ead7..e81325d 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -7,8 +7,8 @@ import { TBA } from "../../api/TBA"; import Button from "../../components/common/Button"; import DarkBackground from "../../components/common/DarkBackground"; import Modal from "../../components/common/Modal"; -import Text from "../../components/common/Text"; -import Title from "../../components/common/Title"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; import DownloadingModal from "./DownloadingModal"; interface ModalProps diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index e904be9..62a9afd 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -4,9 +4,8 @@ import DownloadingModal from './DownloadingModal'; import RegionalModal from './RegionalModal'; import { BlitzDB } from '../../api/BlitzDB'; import Button from '../../components/common/Button'; -import Title from '../../components/common/Title'; -import Text from '../../components/common/Text'; import ScrollContainer from '../../components/containers/ScrollContainer'; +import Text from '../../components/text/Text'; // BUG "Update Data" is available after a data wipe // TODO More settings @@ -14,15 +13,8 @@ export default function SettingsScreen() { const [modalVisible, setModalVisible] = React.useState(false); const [downloadStatus, setDownloadStatus] = React.useState(""); - const [version, setVersion] = React.useState(0); - - BlitzDB.eventEmitter.addListener("dataUpdate", () => { - BlitzDB.eventEmitter.removeCurrentListener(); - setVersion(version + 1); - }); - - + // Update Button let updateButton; if (BlitzDB.event) updateButton = ( diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index 47bb4a3..5a61662 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -3,12 +3,12 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; import Button from '../../components/common/Button'; import ScrollContainer from '../../components/containers/ScrollContainer'; -import Text from '../../components/common/Text'; -import Title from '../../components/common/Title'; +import Text from '../../components/text/Text'; import ExportQRModal from './ExportQRModal'; import ImportQRModal from './ImportQRModal'; -export default function SharingScreen() { +export default function SharingScreen() +{ const [isExportQRVisible, setExportQRVisible] = React.useState(false); const [isImportQRVisible, setImportQRVisible] = React.useState(false); @@ -24,7 +24,7 @@ export default function SharingScreen() { style={styles.buttonIcon}/> Show QRCode - Export Comments + Export Scouting Data @@ -37,7 +37,7 @@ export default function SharingScreen() { style={styles.buttonIcon}/> Scan QRCode - Import Comments + Import Scouting Data @@ -49,7 +49,7 @@ export default function SharingScreen() { style={styles.buttonIcon}/> Save to CSV - Export Teams & Comments + Export Scouting Data @@ -73,7 +73,7 @@ export default function SharingScreen() { style={styles.buttonIcon}/> Upload to Cloud - Export Images, Teams, & Comments + Export Everything @@ -85,7 +85,19 @@ export default function SharingScreen() { style={styles.buttonIcon}/> Download from Cloud - Import Images, Teams, & Comments + Import Everything + + + + {/* Sync USB */} + diff --git a/screens/Teams/TeamBanner.tsx b/screens/Teams/TeamBanner.tsx index ac128f3..24c4a49 100644 --- a/screens/Teams/TeamBanner.tsx +++ b/screens/Teams/TeamBanner.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Image, StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; import Button from "../../components/common/Button"; -import Text from "../../components/common/Text"; +import Text from "../../components/text/Text"; import TeamModal from "./TeamModal"; interface TeamBannerProps @@ -25,9 +25,15 @@ export default function TeamBanner(props: TeamBannerProps) style={styles.teamButton} onPress={() => setVisible(true)}> - + - 0 ? {uri:team.media[0]} : {}} /> + 0 ? {uri:team.media[0]} : {}} /> {team.name} diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index 20a8487..7084b5f 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -5,10 +5,11 @@ import * as ImagePicker from 'expo-image-picker'; import PhotoModal from "../../components/containers/PhotoModal"; import { BlitzDB } from "../../api/BlitzDB"; import Button from "../../components/common/Button"; -import Title from "../../components/common/Title"; import HorizontalBar from "../../components/common/HorizontalBar"; -import Text from "../../components/common/Text"; import Modal from "../../components/common/Modal"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; +import Subtitle from "../../components/text/Subtitle"; interface ModalProps { @@ -56,32 +57,6 @@ export default function TeamModal(props: ModalProps) ); } - // Grab Team Comments - let commentList: JSX.Element[] = []; - if (team.comments.length > 0) - { - for (let comment of team.comments) - { - let commentTime = new Date(comment.timestamp); - commentList.push( - - {comment.text} - {commentTime.getHours() + ":" + commentTime.getMinutes()} - - ); - } - } - else - { - commentList.push( - There are no comments yet... - ); - } - - // Comment Data - let commentText: string; - let commentInput: TextInput | null; - // Return Modal return ( @@ -118,40 +93,11 @@ export default function TeamModal(props: ModalProps) - {team ? team.name : ""} - {team ? team.number : ""} + {team.name} + {team.number} - - { commentText = text }} - ref={input => {commentInput = input}} - /> - - - - {commentList} - - - ) { - const [version, setVersion] = React.useState(0); - - BlitzDB.eventEmitter.addListener("dataUpdate", () => { - BlitzDB.eventEmitter.removeCurrentListener(); - setVersion(version + 1); - }); - - +export default function TeamsScreen() +{ let teamList: JSX.Element[] = []; - + if (BlitzDB.currentTeamIDs.length > 0) - { for (let teamID of BlitzDB.currentTeamIDs) - { - teamList.push( - - ); - } - } + teamList.push( ); else - { - teamList.push( - Team data has not been downloaded from TBA yet. Download is available under settings - ); - } + teamList.push( Team data has not been downloaded from TBA yet. Download is available under settings ); return ( From f9864a7871181010d97de123ba6fc7e93157c28b Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Thu, 14 Oct 2021 23:09:38 -0500 Subject: [PATCH 11/38] StandardButton and Scouting Templates --- api/BlitzDB.ts | 14 +- api/DBModels.ts | 21 +++ app.json | 6 +- components/common/StandardButton.tsx | 90 +++++++++++ components/containers/PhotoModal.tsx | 2 +- navigation/index.tsx | 9 +- screens/Matches/MatchBanner.tsx | 47 ++++-- screens/Matches/MatchModal.tsx | 9 ++ screens/Settings/DownloadingModal.tsx | 2 +- screens/Settings/RegionalModal.tsx | 7 +- screens/Settings/SettingsScreen.tsx | 104 ++++++------- screens/Settings/Template/ElementChooser.tsx | 1 + screens/Settings/Template/TemplateModal.tsx | 41 +++++ screens/Sharing/ExportQRModal.tsx | 5 +- screens/Sharing/SharingScreen.tsx | 154 +++++++------------ screens/Teams/TeamBanner.tsx | 38 ++--- screens/Teams/TeamModal.tsx | 18 ++- types.tsx | 2 - 18 files changed, 351 insertions(+), 219 deletions(-) create mode 100644 components/common/StandardButton.tsx create mode 100644 screens/Settings/Template/ElementChooser.tsx create mode 100644 screens/Settings/Template/TemplateModal.tsx diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts index 7b964d9..2806a46 100644 --- a/api/BlitzDB.ts +++ b/api/BlitzDB.ts @@ -224,9 +224,7 @@ export class BlitzDB if (alert) { - Alert.alert( - "Are you sure?", - "This will delete all scouting data from your device. Are you sure you want to continue?", + Alert.alert( "Are you sure?", "This will delete all scouting data from your device. Are you sure you want to continue?", [ { text: "Confirm", @@ -236,14 +234,8 @@ export class BlitzDB }); } }, - { - text: "Cancel", - style: "cancel" - } - ], - { - cancelable: true - } + { text: "Cancel", style: "cancel" } + ], { cancelable: true } ); } else diff --git a/api/DBModels.ts b/api/DBModels.ts index 74bbb69..d33e98f 100644 --- a/api/DBModels.ts +++ b/api/DBModels.ts @@ -69,4 +69,25 @@ export interface Comment isScanned: boolean; timestamp: number; text: string; +} + +/* Template */ +export enum TemplateType +{ + None, + Pit, + Match +} + +export enum ScoutingElement +{ + counter, + checkbox, + textbox, + rating +} + +interface Template +{ + elements: ScoutingElement[]; } \ No newline at end of file diff --git a/app.json b/app.json index cbcf3ae..522166f 100644 --- a/app.json +++ b/app.json @@ -1,12 +1,12 @@ { "expo": { - "name": "BlitzScouter", + "name": "Blitz Scouter", "slug": "BlitzScouter", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/images/icon.png", - "scheme": "myapp", - "userInterfaceStyle": "automatic", + "scheme": "blitz", + "userInterfaceStyle": "light", "splash": { "image": "./assets/images/splash.png", "resizeMode": "contain", diff --git a/components/common/StandardButton.tsx b/components/common/StandardButton.tsx new file mode 100644 index 0000000..e67bc01 --- /dev/null +++ b/components/common/StandardButton.tsx @@ -0,0 +1,90 @@ +import { FontAwesome } from '@expo/vector-icons'; +import * as React from 'react'; +import { ReactNode } from "react"; +import { StyleSheet, Modal as DefaultModal, View, ScrollView, TouchableOpacity, GestureResponderEvent, Image } from "react-native"; +import Text from '../text/Text'; +import DarkBackground from "./DarkBackground"; + +export interface ButtonProps +{ + iconType?: React.ComponentProps['name']; + iconText?: string; + iconData?: string; + + title: string; + subtitle: string; + onPress: (event: GestureResponderEvent) => void; +} + +export default function StandardButton(props: ButtonProps) +{ + + return ( + + + {/* Text Icon */} + {props.iconText ? + {props.iconText} + : null} + + {/* FA Icon */} + {props.iconType ? + + + + : null} + + {/* SVG Icon */} + {props.iconData ? + + : null} + + {/* Titles */} + + {props.title} + {props.subtitle} + + + + ); +} + +const styles = StyleSheet.create({ + button: { + flexDirection: "row", + justifyContent: "flex-start", + alignItems: "center", + alignSelf: 'stretch', + padding: 10, + }, + buttonTitle: { + fontSize: 18 + }, + buttonSubtitle: { + color: "#bbb" + }, + buttonIconFA: { + marginRight: 10, + width: 40, + alignItems: 'center', + justifyContent: 'center' + }, + buttonIconSVG: { + width: 40, + height: 40, + marginRight: 10, + resizeMode: 'stretch' + }, + buttonIconTXT: { + fontSize: 20, + fontWeight: "bold", + textAlign: "center", + paddingTop: 5, + marginRight: 10, + marginLeft: -10, + width: 35, + height: 35, + } +}); \ No newline at end of file diff --git a/components/containers/PhotoModal.tsx b/components/containers/PhotoModal.tsx index d8832d2..6a23db7 100644 --- a/components/containers/PhotoModal.tsx +++ b/components/containers/PhotoModal.tsx @@ -8,7 +8,7 @@ interface PhotoProps setImageData: (imageData: string) => void; } -// TODO Improve Photo Modal +// TODO Photo Zoom, Delete, and Re-Arrange export default function PhotoModal(props: PhotoProps) { if (props.imageData === "") diff --git a/navigation/index.tsx b/navigation/index.tsx index e018199..fbc6702 100644 --- a/navigation/index.tsx +++ b/navigation/index.tsx @@ -23,7 +23,10 @@ const Stack = createNativeStackNavigator(); function RootNavigator() { return ( - + ); } @@ -48,18 +51,22 @@ function BottomTabNavigator() {return ()}}} /> {return ()}}} /> {return ()}}} /> {return ()}}} /> ); diff --git a/screens/Matches/MatchBanner.tsx b/screens/Matches/MatchBanner.tsx index 1d3963a..14f47ac 100644 --- a/screens/Matches/MatchBanner.tsx +++ b/screens/Matches/MatchBanner.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { StyleSheet } from "react-native"; +import { StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; -import Button from "../../components/common/Button"; -import Text from "../../components/text/Text"; +import StandardButton from "../../components/common/StandardButton"; import MatchModal from "./MatchModal"; interface MatchBannerProps @@ -18,26 +17,40 @@ export default function MatchBanner(props: MatchBannerProps) if (!match) return null; - return (); + + + ); } const styles = StyleSheet.create({ - matchButton: { - alignItems: "flex-start" + button: { + flexDirection: "row", + justifyContent: "flex-start" }, - matchName: { - fontSize: 18, - textAlign: "left" + buttonTitle: { + fontSize: 18 }, - matchDesc: { + buttonSubtitle: { color: "#bbb" }, + matchThumbnail: { + fontSize: 20, + fontWeight: "bold", + marginRight: 10, + width: 35, + height: 35, + paddingTop: 5, + textAlign: "center" + } }); \ No newline at end of file diff --git a/screens/Matches/MatchModal.tsx b/screens/Matches/MatchModal.tsx index f78ed85..d10ca9e 100644 --- a/screens/Matches/MatchModal.tsx +++ b/screens/Matches/MatchModal.tsx @@ -3,6 +3,7 @@ import { Alert, ScrollView, StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; +import StandardButton from "../../components/common/StandardButton"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; import TeamPreview from "./TeamPreview"; @@ -46,6 +47,14 @@ export default function MatchModal(props: ModalProps) + {}} /> + + + Red Alliance diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx index 9bee54f..585b5dc 100644 --- a/screens/Settings/DownloadingModal.tsx +++ b/screens/Settings/DownloadingModal.tsx @@ -23,7 +23,7 @@ export default function DownloadingModal(props: ModalProps) - Downloading, Please Wait + Downloading {props.status} diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index e81325d..61e16ea 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -6,7 +6,9 @@ import { TBAEvent } from "../../api/DBModels"; import { TBA } from "../../api/TBA"; import Button from "../../components/common/Button"; import DarkBackground from "../../components/common/DarkBackground"; +import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; +import Subtitle from "../../components/text/Subtitle"; import Text from "../../components/text/Text"; import Title from "../../components/text/Title"; import DownloadingModal from "./DownloadingModal"; @@ -71,7 +73,8 @@ export default function RegionalModal(props: ModalProps) return ( - Regional: + Set Regional + Select the upcoming regional: {BlitzDB.download(BlitzDB.event ? BlitzDB.event.id : "", setDownloadStatus)}}> - Update Data - ); - return ( - {updateButton} - + {/* Data Buttons */} + {BlitzDB.event ? + { BlitzDB.download(BlitzDB.event ? BlitzDB.event.id : "", setDownloadStatus); }} + /> + : null} + + { setRegionalModalVisible(true); }} /> + { BlitzDB.deleteAll(true); }} /> - + - + {/* Scouting Buttons */} + { setTemplateModalType(TemplateType.Pit); }} /> + { setTemplateModalType(TemplateType.Match); }} /> + {}} /> + + + {/* Modals */} + + ); -} - -const styles = StyleSheet.create({ - button: { - backgroundColor: "#c89f00", - marginBottom: 10, - borderRadius: 5 - }, - buttonText: { - color: "#000", - fontSize: 16 - }, - textInput: { - color: "#fff", - borderBottomColor: "#fff", - borderBottomWidth: 1, - marginBottom: 10 - }, - modal: { - backgroundColor: "#111", - margin: 20, - marginBottom: 100 - }, - loadingText: { - textAlign: "center", - fontStyle: "italic" - } -}); +} \ No newline at end of file diff --git a/screens/Settings/Template/ElementChooser.tsx b/screens/Settings/Template/ElementChooser.tsx new file mode 100644 index 0000000..83a0230 --- /dev/null +++ b/screens/Settings/Template/ElementChooser.tsx @@ -0,0 +1 @@ +// TODO Design & Implement Element Chooser \ No newline at end of file diff --git a/screens/Settings/Template/TemplateModal.tsx b/screens/Settings/Template/TemplateModal.tsx new file mode 100644 index 0000000..4ccbdb1 --- /dev/null +++ b/screens/Settings/Template/TemplateModal.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { TemplateType } from '../../../api/DBModels'; +import HorizontalBar from '../../../components/common/HorizontalBar'; +import Modal from '../../../components/common/Modal'; +import StandardButton from '../../../components/common/StandardButton'; +import Subtitle from '../../../components/text/Subtitle'; +import Title from '../../../components/text/Title'; + +interface ModalProps +{ + setType: (type: TemplateType) => void; + type: TemplateType +} + +const StringTypes = [ + "NULL", + "Pit", + "Match" +]; + +export default function TemplateModal(props: ModalProps) +{ + // Default Behaviour + if (props.type === TemplateType.None) + return null; + + let stringType = StringTypes[props.type]; + + return ( + { props.setType(TemplateType.None); }} > + Edit Template + {stringType} Scouting + + {}}/> + + ); +} \ No newline at end of file diff --git a/screens/Sharing/ExportQRModal.tsx b/screens/Sharing/ExportQRModal.tsx index 57cddbb..86b4afd 100644 --- a/screens/Sharing/ExportQRModal.tsx +++ b/screens/Sharing/ExportQRModal.tsx @@ -17,7 +17,8 @@ export default function ExportQRModal(props: ModalProps) if (!props.isVisible) return null; - const deviceWidth = Dimensions.get("window").width; + const windowSize = Dimensions.get("window"); + const qrSize = Math.min(windowSize.width, windowSize.height); const commentData = BlitzDB.exportComments(); const compressedData = LZString.compress(commentData); @@ -31,7 +32,7 @@ export default function ExportQRModal(props: ModalProps) diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index 5a61662..9db0f50 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -1,9 +1,8 @@ -import { FontAwesome } from '@expo/vector-icons'; import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; -import Button from '../../components/common/Button'; +import { Vibration } from 'react-native'; +import HorizontalBar from '../../components/common/HorizontalBar'; +import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; -import Text from '../../components/text/Text'; import ExportQRModal from './ExportQRModal'; import ImportQRModal from './ImportQRModal'; @@ -15,111 +14,62 @@ export default function SharingScreen() return ( - {/* Export QRCode */} + {/* QR Codes */} - + { setExportQRVisible(true); }} /> - {/* Import QRCode */} - + { setImportQRVisible(true); }} /> + - {/* Export CSV */} - - - {/* Export ZIP */} - - - {/* Export Cloud */} - + {/* File Formats */} + {}} /> + + {}} /> + - {/* Import Cloud */} - + {/* Cloud Save */} + {}} /> + + {}} /> + - {/* Sync USB */} - + {/* Hardware Sync */} + {}} /> + { Vibration.vibrate([200, 200, 200, 200, 200, 600], false); }} /> ); } - -const styles = StyleSheet.create({ - sharingButton: { - flexDirection: "row", - justifyContent: "flex-start" - }, - buttonIcon: { - color: "#fff", - marginRight: 10, - width: 35 - }, - buttonTitle: { - fontSize: 18, - textAlign: "left" - }, - buttonSubtitle: { - color: "#bbb" - } -}); diff --git a/screens/Teams/TeamBanner.tsx b/screens/Teams/TeamBanner.tsx index 24c4a49..0b01b54 100644 --- a/screens/Teams/TeamBanner.tsx +++ b/screens/Teams/TeamBanner.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { Image, StyleSheet, View } from "react-native"; +import { StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; -import Button from "../../components/common/Button"; -import Text from "../../components/text/Text"; +import StandardButton from "../../components/common/StandardButton"; import TeamModal from "./TeamModal"; interface TeamBannerProps @@ -21,26 +20,23 @@ export default function TeamBanner(props: TeamBannerProps) return null; } - return (); + + + 0 ? team.media[0] : undefined} + iconType={team.media.length > 0 ? undefined : "ban"} + title={team.name} + subtitle={team.number.toString()} + onPress={() => { setVisible(true); }} /> + + + ); } const styles = StyleSheet.create({ diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index 7084b5f..fbb519b 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,15 +1,15 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Alert, Dimensions, Image, ScrollView, StyleSheet, TextInput, View } from "react-native"; +import { Alert, Image, ScrollView, StyleSheet } from "react-native"; import * as ImagePicker from 'expo-image-picker'; import PhotoModal from "../../components/containers/PhotoModal"; import { BlitzDB } from "../../api/BlitzDB"; import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; -import Text from "../../components/text/Text"; import Title from "../../components/text/Title"; import Subtitle from "../../components/text/Subtitle"; +import StandardButton from "../../components/common/StandardButton"; interface ModalProps { @@ -98,6 +98,20 @@ export default function TeamModal(props: ModalProps) + {}} /> + + {}} /> + + + | undefined; - Modal: undefined; - NotFound: undefined; }; export type RootStackScreenProps = NativeStackScreenProps< From d1f6a1c418a03ade9ce9c12db7f2858d8571f5b5 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Mon, 18 Oct 2021 04:01:35 -0500 Subject: [PATCH 12/38] Photo Modal and TBA Buttons --- api/BlitzDB.ts | 43 ++++- api/DBModels.ts | 21 ++- api/TBA.tsx | 14 +- app.json | 4 +- assets/images/adaptive-icon.png | Bin 24444 -> 0 bytes assets/images/favicon.png | Bin 24444 -> 0 bytes components/common/StandardButton.tsx | 1 + components/containers/PhotoModal.tsx | 167 ++++++++++++++++-- components/elements/HRElement.tsx | 10 ++ components/elements/ScoutingElement.tsx | 21 +++ components/elements/SubtitleElement.tsx | 10 ++ components/elements/TextElement.tsx | 10 ++ components/elements/TitleElement.tsx | 10 ++ navigation/index.tsx | 2 +- package.json | 1 + screens/Matches/MatchModal.tsx | 7 + screens/Settings/SettingsScreen.tsx | 12 +- screens/Settings/Template/ElementChooser.tsx | 1 - .../Settings/Template/ElementChooserModal.tsx | 90 ++++++++++ screens/Settings/Template/TemplateModal.tsx | 71 +++++++- screens/Sharing/SharingScreen.tsx | 11 +- screens/Teams/TeamMatchesModal.tsx | 60 +++++++ screens/Teams/TeamModal.tsx | 32 +++- yarn.lock | 7 + 24 files changed, 548 insertions(+), 57 deletions(-) delete mode 100644 assets/images/adaptive-icon.png delete mode 100644 assets/images/favicon.png create mode 100644 components/elements/HRElement.tsx create mode 100644 components/elements/ScoutingElement.tsx create mode 100644 components/elements/SubtitleElement.tsx create mode 100644 components/elements/TextElement.tsx create mode 100644 components/elements/TitleElement.tsx delete mode 100644 screens/Settings/Template/ElementChooser.tsx create mode 100644 screens/Settings/Template/ElementChooserModal.tsx create mode 100644 screens/Teams/TeamMatchesModal.tsx diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts index 2806a46..a06b3a1 100644 --- a/api/BlitzDB.ts +++ b/api/BlitzDB.ts @@ -1,6 +1,6 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { Alert, NativeEventEmitter } from "react-native"; -import { Event, Match, Team } from "./DBModels"; +import { Event, Match, ScoutingTemplate, Team, TemplateType } from "./DBModels"; import { TBA } from "./TBA"; const BASE64_PREFIX = "data:image/png;base64, "; @@ -11,6 +11,7 @@ export class BlitzDB static event?: Event; static currentTeamIDs: string[] = []; static teams: Team[] = []; + static templates: Record = [[], []]; static eventEmitter = new NativeEventEmitter(); static async download(eventID: string, setDownloadStatus: Function) @@ -170,6 +171,34 @@ export class BlitzDB } } + static removeTeamMedia(teamID: string, mediaIndex: number) + { + let team = BlitzDB.getTeam(teamID); + if (team) + { + team.media.splice(mediaIndex, 1); + BlitzDB.save(); + BlitzDB.eventEmitter.emit("mediaUpdate"); + console.log("Removed Media " + mediaIndex + " from " + teamID); + } + } + + static swapTeamMedia(teamID: string, mediaIndex1: number, mediaIndex2?: number) + { + let team = BlitzDB.getTeam(teamID); + if (team) + { + if (mediaIndex2 === undefined) + mediaIndex2 = team.media.length - 1; + let tempMedia = team.media[mediaIndex1]; + team.media[mediaIndex1] = team.media[mediaIndex2]; + team.media[mediaIndex2] = tempMedia; + BlitzDB.save(); + BlitzDB.eventEmitter.emit("mediaUpdate"); + console.log("Swapped Media " + mediaIndex1 + " <-> " + mediaIndex2 + " from " + teamID); + } + } + static getTeam(teamID: string): Team | undefined { return BlitzDB.teams.find(team => team.id === teamID); @@ -181,6 +210,18 @@ export class BlitzDB return BlitzDB.event.matches.find(match => match.id === matchID); } + static getTeamMatches(teamID: string): Match[] + { + if (!(this.event)) + return []; + + let matchList: Match[] = []; + for (let match of this.event.matches) + if (match.blueTeamIDs.includes(teamID) || match.redTeamIDs.includes(teamID)) + matchList.push(match); + return matchList; + } + static exportComments(): string { let data = ""; diff --git a/api/DBModels.ts b/api/DBModels.ts index d33e98f..fe65ac1 100644 --- a/api/DBModels.ts +++ b/api/DBModels.ts @@ -74,20 +74,23 @@ export interface Comment /* Template */ export enum TemplateType { - None, Pit, Match } -export enum ScoutingElement +export enum ElementType { - counter, - checkbox, - textbox, - rating + title, + subtitle, + text, + hr } -interface Template +export interface ElementData { - elements: ScoutingElement[]; -} \ No newline at end of file + type: ElementType; + label: string; + options: any; +} + +export type ScoutingTemplate = ElementData[]; \ No newline at end of file diff --git a/api/TBA.tsx b/api/TBA.tsx index b667b1b..8d63ad0 100644 --- a/api/TBA.tsx +++ b/api/TBA.tsx @@ -1,10 +1,10 @@ -import { Alert } from 'react-native'; +import { Alert, Linking } from 'react-native'; import { TBAEvent, TBAMatch, TBAMedia, TBATeam } from './DBModels'; const API_KEY = "i90dAcKHXvQ9havypHJKeGY8O1tfymFpaW1Po3RGYpvoMTRVwtiUsUFaLmstCDp3"; const URL_PREFIX = "https://www.thebluealliance.com/api/v3/"; const URL_SUFFIX = "?X-TBA-Auth-Key=" + API_KEY; -const YEAR = 2019; +const YEAR = 2020; export class TBA { @@ -28,6 +28,16 @@ export class TBA return TBA._fetch("team/" + teamID + "/media/" + YEAR); } + static openTeam(teamNumber: number) + { + Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + YEAR); + } + + static openMatch(matchID: string) + { + Linking.openURL("https://www.thebluealliance.com/match/" + matchID); + } + static async _fetch(path: string): Promise { try diff --git a/app.json b/app.json index 522166f..6e00f37 100644 --- a/app.json +++ b/app.json @@ -25,14 +25,14 @@ }, "android": { "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", + "foregroundImage": "./assets/images/icon.png", "backgroundColor": "#1e1e1e" }, "package": "org.team5148.blitzscouter", "versionCode":1 }, "web": { - "favicon": "./assets/images/favicon.png" + "favicon": "./assets/images/icon.png" } } } diff --git a/assets/images/adaptive-icon.png b/assets/images/adaptive-icon.png deleted file mode 100644 index e5a3733d7e7e3408c17c8ad53f5e916555ff85cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24444 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 : null} + {/* Titles */} diff --git a/components/containers/PhotoModal.tsx b/components/containers/PhotoModal.tsx index 6a23db7..1d4ff49 100644 --- a/components/containers/PhotoModal.tsx +++ b/components/containers/PhotoModal.tsx @@ -1,31 +1,149 @@ +import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Dimensions, Image, Modal, StyleSheet, View } from "react-native"; +import { Alert, Dimensions, Image, Modal, StyleSheet, ToastAndroid, View } from "react-native"; +import { BlitzDB } from "../../api/BlitzDB"; +import Button from "../common/Button"; import DarkBackground from "../common/DarkBackground"; +import Text from "../text/Text"; +import * as Sharing from 'expo-sharing'; interface PhotoProps { - imageData: string; - setImageData: (imageData: string) => void; + teamID: string; + imageIndex: number; + setImageIndex: (imageIndex: number) => void; } // TODO Photo Zoom, Delete, and Re-Arrange export default function PhotoModal(props: PhotoProps) { - if (props.imageData === "") + if (props.imageIndex < 0) return null; - else - return ( - props.setImageData("")} > - - - - - + + // Team + let team = BlitzDB.getTeam(props.teamID); + if (!(team)) + { + Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); + props.setImageIndex(-1); + return null; + } + + // Media + if (props.imageIndex >= team.media.length) + return null; + let mediaData = team.media[props.imageIndex] + + return ( + props.setImageIndex(-1)} > + + + + + + + + + + + + + - - ); + + + + ); +} + +function trashImage(teamID: string, imageIndex: number) +{ + Alert.alert( "Are you sure?", "Are you sure you want to delete this image?", + [ + { + text: "Confirm", + onPress: () => { BlitzDB.removeTeamMedia(teamID, imageIndex); } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true } + ); +} + +function setThumbnail(teamID: string, imageIndex: number) +{ + Alert.alert( "Are you sure?", "Are you sure you want to set this as Team Thumbnail?", + [ + { + text: "Confirm", + onPress: () => { BlitzDB.swapTeamMedia(teamID, imageIndex, 0); } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true } + ); +} + +function setRobot(teamID: string, imageIndex: number) +{ + Alert.alert( "Are you sure?", "Are you sure you want to set this as the Robot Image?", + [ + { + text: "Confirm", + onPress: () => { BlitzDB.swapTeamMedia(teamID, imageIndex); } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true } + ); +} + +function shareImage(mediaData: string) +{ + // TODO Convert the storage medium for image data from sqlite to local device storage + //Sharing.shareAsync(mediaData); // <-- Using the Expo Sharing Library + + ToastAndroid.show("To be implemented!", ToastAndroid.SHORT); } const styles = StyleSheet.create({ @@ -35,11 +153,26 @@ const styles = StyleSheet.create({ top: 0, left: 0, bottom: 0, - right: 0, + right: 0 }, image: { width: Dimensions.get('window').width, height: Dimensions.get('window').width + }, + button: { + minWidth: 80 + }, + buttonText: { + fontSize: 12, + marginTop: 1 + }, + buttonBar: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + flexDirection: "row", + justifyContent: "space-evenly" } }); diff --git a/components/elements/HRElement.tsx b/components/elements/HRElement.tsx new file mode 100644 index 0000000..35e822a --- /dev/null +++ b/components/elements/HRElement.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { ElementData } from "../../api/DBModels"; +import HorizontalBar from "../common/HorizontalBar"; + +export default function HRElement(props: {data: ElementData}) +{ + return ( + + ); +} \ No newline at end of file diff --git a/components/elements/ScoutingElement.tsx b/components/elements/ScoutingElement.tsx new file mode 100644 index 0000000..54bc838 --- /dev/null +++ b/components/elements/ScoutingElement.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ElementData, ElementType } from "../../api/DBModels"; +import HRElement from "./HRElement"; +import SubtitleElement from "./SubtitleElement"; +import TextElement from "./TextElement"; +import TitleElement from "./TitleElement"; + +export default function ScoutingElement(props: {data: ElementData}): JSX.Element +{ + switch (props.data.type) + { + case ElementType.title: + return (); + case ElementType.subtitle: + return (); + case ElementType.text: + return (); + case ElementType.hr: + return (); + } +} \ No newline at end of file diff --git a/components/elements/SubtitleElement.tsx b/components/elements/SubtitleElement.tsx new file mode 100644 index 0000000..f1c8576 --- /dev/null +++ b/components/elements/SubtitleElement.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { ElementData } from "../../api/DBModels"; +import Subtitle from "../text/Subtitle"; + +export default function SubtitleElement(props: {data: ElementData}) +{ + return ( + {props.data.label} + ); +} \ No newline at end of file diff --git a/components/elements/TextElement.tsx b/components/elements/TextElement.tsx new file mode 100644 index 0000000..8d7e24d --- /dev/null +++ b/components/elements/TextElement.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { ElementData } from "../../api/DBModels"; +import Text from "../text/Text"; + +export default function TextElement(props: {data: ElementData}) +{ + return ( + {props.data.label} + ); +} \ No newline at end of file diff --git a/components/elements/TitleElement.tsx b/components/elements/TitleElement.tsx new file mode 100644 index 0000000..933dd98 --- /dev/null +++ b/components/elements/TitleElement.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { ElementData } from "../../api/DBModels"; +import Title from "../text/Title"; + +export default function TitleElement(props: {data: ElementData}) +{ + return ( + {props.data.label} + ); +} \ No newline at end of file diff --git a/navigation/index.tsx b/navigation/index.tsx index fbc6702..a7d6c15 100644 --- a/navigation/index.tsx +++ b/navigation/index.tsx @@ -41,7 +41,7 @@ function BottomTabNavigator() screenOptions={{ headerTitleStyle: { color: "#fff" }, - headerStyle: {backgroundColor: "#111111" }, + headerStyle: {backgroundColor: "#151515" }, drawerActiveBackgroundColor: "#c89f00", drawerActiveTintColor: "#000", headerTintColor: "white", diff --git a/package.json b/package.json index 4ba8c27..19a4e15 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "expo-font": "~9.2.1", "expo-image-picker": "~10.2.2", "expo-linking": "~2.3.1", + "expo-sharing": "~9.2.1", "expo-splash-screen": "~0.11.2", "expo-status-bar": "~1.0.4", "expo-web-browser": "~9.2.0", diff --git a/screens/Matches/MatchModal.tsx b/screens/Matches/MatchModal.tsx index d10ca9e..2b14096 100644 --- a/screens/Matches/MatchModal.tsx +++ b/screens/Matches/MatchModal.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Alert, ScrollView, StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; +import { TBA } from "../../api/TBA"; import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; import StandardButton from "../../components/common/StandardButton"; @@ -53,6 +54,12 @@ export default function MatchModal(props: ModalProps) subtitle={"Scout this match"} onPress={() => {}} /> + { match ? TBA.openMatch(match.id) : null }} /> + Red Alliance diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 58cf594..df8c4c1 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -12,7 +12,8 @@ import StandardButton from '../../components/common/StandardButton'; export default function SettingsScreen() { const [regionalModalVisible, setRegionalModalVisible] = React.useState(false); - const [templateModalType, setTemplateModalType] = React.useState(TemplateType.None); + const [pitTemplateModalVisible, setPitTemplateModalVisible] = React.useState(false); + const [matchTemplateModalVisible, setMatchTemplateModalVisible] = React.useState(false); const [downloadStatus, setDownloadStatus] = React.useState(""); return ( @@ -30,7 +31,7 @@ export default function SettingsScreen() { setRegionalModalVisible(true); }} /> { setTemplateModalType(TemplateType.Pit); }} /> + onPress={() => { setPitTemplateModalVisible(true); }} /> { setTemplateModalType(TemplateType.Match); }} /> + onPress={() => { setMatchTemplateModalVisible(true); }} /> - + + ); diff --git a/screens/Settings/Template/ElementChooser.tsx b/screens/Settings/Template/ElementChooser.tsx deleted file mode 100644 index 83a0230..0000000 --- a/screens/Settings/Template/ElementChooser.tsx +++ /dev/null @@ -1 +0,0 @@ -// TODO Design & Implement Element Chooser \ No newline at end of file diff --git a/screens/Settings/Template/ElementChooserModal.tsx b/screens/Settings/Template/ElementChooserModal.tsx new file mode 100644 index 0000000..26ec8b7 --- /dev/null +++ b/screens/Settings/Template/ElementChooserModal.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { BlitzDB } from '../../../api/BlitzDB'; +import { ElementType, TemplateType } from '../../../api/DBModels'; +import HorizontalBar from '../../../components/common/HorizontalBar'; +import Modal from '../../../components/common/Modal'; +import StandardButton from '../../../components/common/StandardButton'; +import Subtitle from '../../../components/text/Subtitle'; +import Title from '../../../components/text/Title'; + +interface ModalProps +{ + isVisible: boolean; + setVisible: (isVisible: boolean) => void; + type: TemplateType; +} + +export default function ElementChooserModal(props: ModalProps) +{ + // Default Behaviour + if (!props.isVisible) + return null; + + const chooseElement = (type: ElementType) => { + BlitzDB.templates[props.type].push({ + type: type, + label: "Element", + options: {} + }); + + props.setVisible(false); + } + + return ( + + Add Element + Choose an element to add: + + + {}} /> + + {}} /> + + {}} /> + + {}} /> + + + + { chooseElement(ElementType.title); }} /> + + { chooseElement(ElementType.subtitle); }} /> + + { chooseElement(ElementType.text); }} /> + + { chooseElement(ElementType.hr); }} /> + + ); +} \ No newline at end of file diff --git a/screens/Settings/Template/TemplateModal.tsx b/screens/Settings/Template/TemplateModal.tsx index 4ccbdb1..fcb353d 100644 --- a/screens/Settings/Template/TemplateModal.tsx +++ b/screens/Settings/Template/TemplateModal.tsx @@ -1,41 +1,96 @@ import * as React from 'react'; -import { TemplateType } from '../../../api/DBModels'; +import { Alert } from 'react-native'; +import { BlitzDB } from '../../../api/BlitzDB'; +import { ElementData, TemplateType } from '../../../api/DBModels'; import HorizontalBar from '../../../components/common/HorizontalBar'; import Modal from '../../../components/common/Modal'; import StandardButton from '../../../components/common/StandardButton'; +import ScoutingElement from '../../../components/elements/ScoutingElement'; +import TextElement from '../../../components/elements/TextElement'; import Subtitle from '../../../components/text/Subtitle'; +import Text from '../../../components/text/Text'; import Title from '../../../components/text/Title'; +import ElementChooserModal from './ElementChooserModal'; interface ModalProps { - setType: (type: TemplateType) => void; - type: TemplateType + isVisible: boolean; + setVisible: (isVisible: boolean) => void; + type: TemplateType; } const StringTypes = [ - "NULL", "Pit", "Match" ]; export default function TemplateModal(props: ModalProps) { + const [isVisible, setVisible] = React.useState(false); + const [version, setVersion] = React.useState(0); + // Default Behaviour - if (props.type === TemplateType.None) + if (!props.isVisible) return null; - let stringType = StringTypes[props.type]; + // Element Preview + const stringType = StringTypes[props.type]; + const template = BlitzDB.templates[props.type]; + let elementList: JSX.Element[] = []; + if (template.length > 0) + { + for (let elementData of template) + { + elementList.push( + + ); + } + } + else + { + elementList.push( There are no elements yet. Add an element to scout below ); + } + + // Clear Behaviour + const clearTemplate = () => { + Alert.alert( "Are you sure?", "This will delete all elements in this template. Are you sure you want to continue?", + [ + { + text: "Confirm", + onPress: () => { + BlitzDB.templates[props.type] = []; + setVersion(version + 1); + } + }, + { text: "Cancel", style: "cancel" } + ], { cancelable: true } + ); + } return ( - { props.setType(TemplateType.None); }} > + + Edit Template {stringType} Scouting + + + + {elementList} + {}}/> + onPress={() => { setVisible(true); }} /> + + { clearTemplate(); }} /> + + ); } \ No newline at end of file diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index 9db0f50..c43b2b0 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -45,6 +45,7 @@ export default function SharingScreen() {/* Cloud Save */} + {/* {}} /> + */} {/* Hardware Sync */} {}} /> - { Vibration.vibrate([200, 200, 200, 200, 200, 600], false); }} /> + onPress={() => {}} /> + {}} /> ); diff --git a/screens/Teams/TeamMatchesModal.tsx b/screens/Teams/TeamMatchesModal.tsx new file mode 100644 index 0000000..86c0f45 --- /dev/null +++ b/screens/Teams/TeamMatchesModal.tsx @@ -0,0 +1,60 @@ +import { FontAwesome } from "@expo/vector-icons"; +import React from "react"; +import { Alert, Image, ScrollView, StyleSheet } from "react-native"; +import * as ImagePicker from 'expo-image-picker'; +import PhotoModal from "../../components/containers/PhotoModal"; +import { BlitzDB } from "../../api/BlitzDB"; +import Button from "../../components/common/Button"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import Modal from "../../components/common/Modal"; +import Title from "../../components/text/Title"; +import Subtitle from "../../components/text/Subtitle"; +import StandardButton from "../../components/common/StandardButton"; +import MatchBanner from "../Matches/MatchBanner"; +import Text from "../../components/text/Text"; + +interface ModalProps +{ + teamID: string; + isVisible: boolean; + setVisible: (isVisible: boolean) => void; +} + +export default function TeamMatchesModal(props: ModalProps) +{ + // Default Behaviour + if (!props.isVisible) + return null; + + // Grab Team Data + let team = BlitzDB.getTeam(props.teamID); + if (!(team) || !(BlitzDB.event)) + { + Alert.alert("Error", "There was an error grabbing team or event data. Try re-downloading TBA data then try again."); + props.setVisible(false); + return null; + } + + // Matches + let matchList = BlitzDB.getTeamMatches(props.teamID); + let matchDisplay: JSX.Element[] = []; + if (matchList.length > 0) + for (let match of matchList) + matchDisplay.push( ); + else + matchDisplay.push( Match data has not been downloaded from TBA yet. Download is available under settings ); + + // Return Modal + return ( + + {team.name} + Team {team.number}'s Match List + + + {matchDisplay} + + + + + ); +} \ No newline at end of file diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index fbb519b..dd398c1 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,6 +1,6 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Alert, Image, ScrollView, StyleSheet } from "react-native"; +import { Alert, Image, Linking, ScrollView, StyleSheet } from "react-native"; import * as ImagePicker from 'expo-image-picker'; import PhotoModal from "../../components/containers/PhotoModal"; import { BlitzDB } from "../../api/BlitzDB"; @@ -10,6 +10,8 @@ import Modal from "../../components/common/Modal"; import Title from "../../components/text/Title"; import Subtitle from "../../components/text/Subtitle"; import StandardButton from "../../components/common/StandardButton"; +import TeamMatchesModal from "./TeamMatchesModal"; +import { TBA } from "../../api/TBA"; interface ModalProps { @@ -20,7 +22,8 @@ interface ModalProps export default function TeamModal(props: ModalProps) { - const [previewData, setPreviewPhoto] = React.useState(""); + const [previewIndex, setPreviewIndex] = React.useState(-1); + const [isTeamMatchesVisible, setTeamMatchesVisible] = React.useState(false); const [version, setVersion] = React.useState(0); // Default Behaviour @@ -31,6 +34,7 @@ export default function TeamModal(props: ModalProps) BlitzDB.eventEmitter.addListener("mediaUpdate", () => { BlitzDB.eventEmitter.removeCurrentListener(); setVersion(version + 1); + setPreviewIndex(-1); }); // Grab Team Data @@ -44,13 +48,13 @@ export default function TeamModal(props: ModalProps) // Grab Team Media let mediaList: JSX.Element[] = []; - for (let imageData of team.media) + for (let i = 0; i < team.media.length; i++) { - let preview = imageData; + let imageData = team.media[i]; mediaList.push( @@ -108,14 +112,24 @@ export default function TeamModal(props: ModalProps) iconType={"list"} title={"List Matches"} subtitle={"List the matches Team " + team.number + " is in"} - onPress={() => {}} /> + onPress={() => { setTeamMatchesVisible(true); }} /> + + { team ? TBA.openTeam(team.number) : null }} /> + teamID={props.teamID} + imageIndex={previewIndex} + setImageIndex={setPreviewIndex} /> + ); } diff --git a/yarn.lock b/yarn.lock index eca176f..f8c6d08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4082,6 +4082,13 @@ expo-modules-core@~0.2.0: resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-0.2.0.tgz" integrity sha512-inpfZ5X/BaTtbj2wG9PA9AC0MN8VyId6KSRlVuEg7+ziurHBy/kKDFxpOddUokhwiln2uhoYPSStJjR/tKypdw== +expo-sharing@~9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-9.2.1.tgz#50267cd66f54d29f0ab3f185a05d92b197e5b60c" + integrity sha512-L0OR7qq8GJRKEFDMPrStc9UxVdOr3XRGMuK3vO2qgQgU3CCSb5QE5lHRdp4DFyJd22ILZqwL+3ghiUtFQD0eig== + dependencies: + expo-modules-core "~0.2.0" + expo-splash-screen@~0.11.2: version "0.11.4" resolved "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.11.4.tgz" From aa9027ef3034242e25bf840a3e85d36499d20478 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Tue, 19 Oct 2021 02:17:57 -0500 Subject: [PATCH 13/38] Fetch Timeout & Match Refresh --- api/BlitzDB.ts | 184 ++++++++++++---------- api/TBA.ts | 83 ++++++++++ api/TBA.tsx | 54 ------- components/containers/ScrollContainer.tsx | 32 +++- screens/Matches/MatchesScreen.tsx | 14 +- screens/Settings/RegionalModal.tsx | 2 +- screens/Settings/SettingsScreen.tsx | 8 +- 7 files changed, 228 insertions(+), 149 deletions(-) create mode 100644 api/TBA.ts delete mode 100644 api/TBA.tsx diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts index a06b3a1..b36247f 100644 --- a/api/BlitzDB.ts +++ b/api/BlitzDB.ts @@ -14,7 +14,7 @@ export class BlitzDB static templates: Record = [[], []]; static eventEmitter = new NativeEventEmitter(); - static async download(eventID: string, setDownloadStatus: Function) + static async downloadAll(eventID: string, setDownloadStatus: Function) { BlitzDB.event = { id: eventID, @@ -24,33 +24,13 @@ export class BlitzDB // Teams setDownloadStatus("Downloading Team Roster..."); - let tbaTeams = await TBA.getTeams(eventID); - if (tbaTeams) - { - setDownloadStatus("Sorting Team Roster..."); - tbaTeams.sort((a, b) => a.team_number - b.team_number); - for (let tbaTeam of tbaTeams) - { - let existingTeam = BlitzDB.getTeam(tbaTeam.key); - if (!(existingTeam)) - { - BlitzDB.teams.push({ - id: tbaTeam.key, - name: tbaTeam.nickname, - number: tbaTeam.team_number, - media: [], - comments: [] - }); - } - - BlitzDB.event.teams.push(tbaTeam.key); - } - } - else + let success1 = await BlitzDB.downloadTeams(); + if (!success1) { setDownloadStatus(""); return; } + // Team Media let teamCount = 0; @@ -58,7 +38,7 @@ export class BlitzDB for (let teamID of BlitzDB.currentTeamIDs) { teamCount++; - setDownloadStatus("Downloading Team Media... (" + teamCount + "/" + tbaTeams.length + ")"); + setDownloadStatus("Downloading Team Media... (" + teamCount + "/" + BlitzDB.event.teams.length + ")"); let mediaList = await TBA.getTeamMedia(teamID); if (mediaList) @@ -88,63 +68,8 @@ export class BlitzDB // Matches setDownloadStatus("Downloading Match List..."); - let tbaMatches = await TBA.getMatches(eventID); - if (tbaMatches) - { - setDownloadStatus("Sorting Match List"); - for (let tbaMatch of tbaMatches) - { - // Match Name - let matchName = tbaMatch.comp_level + "-" + tbaMatch.match_number; - switch (tbaMatch.comp_level) - { - case "qm": - matchName = "Qualification " + tbaMatch.match_number; - break; - case "qf": - matchName = "Quarter-Finals " + tbaMatch.match_number; - break; - case "sf": - matchName = "Semi-Finals " + tbaMatch.match_number; - break; - case "f": - matchName = "Finals " + tbaMatch.match_number; - break; - } - - // Match Description / Teams - let matchDesc = ""; - for (let teamKey of tbaMatch.alliances.blue.team_keys) - { - let teamNumber = parseInt(teamKey.substring(3)); - matchDesc += teamNumber + " "; - } - matchDesc += " - " - for (let teamKey of tbaMatch.alliances.red.team_keys) - { - let teamNumber = parseInt(teamKey.substring(3)); - matchDesc += teamNumber + " "; - } - - - BlitzDB.event.matches.push({ - id: tbaMatch.key, - name: matchName, - description: matchDesc, - number: tbaMatch.match_number, - compLevel: tbaMatch.comp_level, - blueTeamIDs: tbaMatch.alliances.blue.team_keys, - redTeamIDs: tbaMatch.alliances.red.team_keys, - comment: "" - }); - } - - // Sort - BlitzDB.event.matches.sort((a, b) => - (a.number + MATCH_TYPES.indexOf(a.compLevel) * 500) - (b.number + MATCH_TYPES.indexOf(b.compLevel) * 500) - ); - } - else + let success2 = await BlitzDB.downloadMatches(); + if (!success2) { setDownloadStatus(""); return; @@ -159,6 +84,101 @@ export class BlitzDB Alert.alert("Success", "Successfully downloaded data from The Blue Alliance."); } + static async downloadTeams(): Promise + { + if (!BlitzDB.event) + return false; + + let tbaTeams = await TBA.getTeams(BlitzDB.event.id); + if (!tbaTeams) + return false; + + tbaTeams.sort((a, b) => a.team_number - b.team_number); + for (let tbaTeam of tbaTeams) + { + let existingTeam = BlitzDB.getTeam(tbaTeam.key); + if (!(existingTeam)) + { + BlitzDB.teams.push({ + id: tbaTeam.key, + name: tbaTeam.nickname, + number: tbaTeam.team_number, + media: [], + comments: [] + }); + } + BlitzDB.event.teams.push(tbaTeam.key); + } + return true; + } + + static async downloadMatches(): Promise + { + if (!BlitzDB.event) + return false; + + // Get Matches + let tbaMatches = await TBA.getMatches(BlitzDB.event.id) + if (!tbaMatches) + return false; + + // Parse Matches + BlitzDB.event.matches = []; + for (let tbaMatch of tbaMatches) + { + // Match Name + let matchName = tbaMatch.comp_level + "-" + tbaMatch.match_number; + switch (tbaMatch.comp_level) + { + case "qm": + matchName = "Qualification " + tbaMatch.match_number; + break; + case "qf": + matchName = "Quarter-Finals " + tbaMatch.match_number; + break; + case "sf": + matchName = "Semi-Finals " + tbaMatch.match_number; + break; + case "f": + matchName = "Finals " + tbaMatch.match_number; + break; + } + + // Match Description / Teams + let matchDesc = ""; + for (let teamKey of tbaMatch.alliances.blue.team_keys) + { + let teamNumber = parseInt(teamKey.substring(3)); + matchDesc += teamNumber + " "; + } + matchDesc += " - " + for (let teamKey of tbaMatch.alliances.red.team_keys) + { + let teamNumber = parseInt(teamKey.substring(3)); + matchDesc += teamNumber + " "; + } + + // Add to DB + BlitzDB.event.matches.push({ + id: tbaMatch.key, + name: matchName, + description: matchDesc, + number: tbaMatch.match_number, + compLevel: tbaMatch.comp_level, + blueTeamIDs: tbaMatch.alliances.blue.team_keys, + redTeamIDs: tbaMatch.alliances.red.team_keys, + comment: "" + }); + } + + // Sort + BlitzDB.event.matches.sort((a, b) => + (a.number + MATCH_TYPES.indexOf(a.compLevel) * 500) - (b.number + MATCH_TYPES.indexOf(b.compLevel) * 500) + ); + + return true; + } + static addTeamMedia(teamID: string, imageData: string) { let team = BlitzDB.getTeam(teamID); diff --git a/api/TBA.ts b/api/TBA.ts new file mode 100644 index 0000000..40554aa --- /dev/null +++ b/api/TBA.ts @@ -0,0 +1,83 @@ +import { Alert, Linking } from 'react-native'; +import { TBAEvent, TBAMatch, TBAMedia, TBATeam } from './DBModels'; + +const API_KEY = "i90dAcKHXvQ9havypHJKeGY8O1tfymFpaW1Po3RGYpvoMTRVwtiUsUFaLmstCDp3"; +const URL_PREFIX = "https://www.thebluealliance.com/api/v3/"; +const URL_SUFFIX = "?X-TBA-Auth-Key=" + API_KEY; +const YEAR = 2020; + +export class TBA +{ + static getMatches(eventID: string) + { + return TBA._fetch("event/" + eventID + "/matches/simple"); + } + + static async getEvents() + { + return TBA._fetch("events/" + YEAR); + } + + static async getTeams(eventID: string) + { + return TBA._fetch("event/" + eventID + "/teams/simple"); + } + + static getTeamMedia(teamID: string) + { + return TBA._fetch("team/" + teamID + "/media/" + YEAR); + } + + static openTeam(teamNumber: number) + { + Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + YEAR); + } + + static openMatch(matchID: string) + { + Linking.openURL("https://www.thebluealliance.com/match/" + matchID); + } + + static _fetch(path: string): Promise + { + /* + https://github.com/whatwg/fetch/issues/180 + + TL;DR; fetch doesn't include a timeout by default. + While this solution does introduce memory leaks, there + is no other option until a better solution is implemented. + */ + + // Fetch Promise + const URL = URL_PREFIX + path + URL_SUFFIX; + let fetchPromise = new Promise((resolve, reject) => { + let headers = new Headers(); + headers.append("pragma", "no-cache"); + headers.append("cache-control", "no-cache"); + + const REQUEST_DATA = { + method: "GET", + headers: headers + }; + + fetch(URL, REQUEST_DATA).then((result) => { + result.json().then((json) => { + resolve(json); + }).catch(() => { + resolve(undefined); + }); + }).catch(() => { + resolve(undefined); + }); + }); + + // Timeout Promise + let timeoutPromise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve(undefined); + }, 5000); + }) + + return Promise.race>([fetchPromise, timeoutPromise]); + } +} \ No newline at end of file diff --git a/api/TBA.tsx b/api/TBA.tsx deleted file mode 100644 index 8d63ad0..0000000 --- a/api/TBA.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Alert, Linking } from 'react-native'; -import { TBAEvent, TBAMatch, TBAMedia, TBATeam } from './DBModels'; - -const API_KEY = "i90dAcKHXvQ9havypHJKeGY8O1tfymFpaW1Po3RGYpvoMTRVwtiUsUFaLmstCDp3"; -const URL_PREFIX = "https://www.thebluealliance.com/api/v3/"; -const URL_SUFFIX = "?X-TBA-Auth-Key=" + API_KEY; -const YEAR = 2020; - -export class TBA -{ - static getMatches(eventID: string) - { - return TBA._fetch("event/" + eventID + "/matches/simple"); - } - - static async getEvents() - { - return TBA._fetch("events/" + YEAR); - } - - static async getTeams(eventID: string) - { - return TBA._fetch("event/" + eventID + "/teams/simple"); - } - - static getTeamMedia(teamID: string) - { - return TBA._fetch("team/" + teamID + "/media/" + YEAR); - } - - static openTeam(teamNumber: number) - { - Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + YEAR); - } - - static openMatch(matchID: string) - { - Linking.openURL("https://www.thebluealliance.com/match/" + matchID); - } - - static async _fetch(path: string): Promise - { - try - { - let result = await fetch(URL_PREFIX + path + URL_SUFFIX); - if (result.ok) - return (await result.json()) as Type; - } - catch - { - Alert.alert("Error","Could not connect to The Blue Alliance"); - } - } -} \ No newline at end of file diff --git a/components/containers/ScrollContainer.tsx b/components/containers/ScrollContainer.tsx index 1e10ef7..341ca89 100644 --- a/components/containers/ScrollContainer.tsx +++ b/components/containers/ScrollContainer.tsx @@ -1,11 +1,26 @@ import * as React from 'react'; -import { ScrollView, View } from "react-native"; +import { RefreshControl, ScrollView, StyleProp, ToastAndroid, View, ViewStyle } from "react-native"; import FadeIn from '../common/FadeIn'; export type ViewProps = View['props']; +interface ScrollContainerProps +{ + children: React.ReactNode + onRefresh?: () => Promise; +} -export default function ScrollContainer(props: ViewProps) { - const { style, ...otherProps } = props; +export default function ScrollContainer(props: ScrollContainerProps) +{ + const [isRefreshing, setRefreshing] = React.useState(false); + + const onRefresh = React.useCallback(() => { + setRefreshing(true); + + if (props.onRefresh !== undefined) + props.onRefresh().then(() => setRefreshing(false)); + else + setRefreshing(false); + }, []); return ( @@ -15,9 +30,14 @@ export default function ScrollContainer(props: ViewProps) { paddingLeft: 20, paddingRight: 20, backgroundColor: "#0a0a0a" - }}> - - + }} + refreshControl={ + props.onRefresh ? : undefined + } + > + + {props.children} + ); diff --git a/screens/Matches/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx index 944e393..ace22fc 100644 --- a/screens/Matches/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, ToastAndroid } from 'react-native'; import { BlitzDB } from '../../api/BlitzDB'; import ScrollContainer from '../../components/containers/ScrollContainer'; import Text from '../../components/text/Text'; @@ -7,8 +7,18 @@ import MatchBanner from './MatchBanner'; export default function MatchesScreen() { + const [version, setVersion] = React.useState(0); let matchDisplay: JSX.Element[] = []; + const onRefresh = async () => { + let success = await BlitzDB.downloadMatches(); + if (!success) + ToastAndroid.show("Failed to connect to TBA", 1000); + else + ToastAndroid.show("Updated match data!", 1000); + setVersion(version + 1); + }; + if (BlitzDB.event) for (let match of BlitzDB.event.matches) matchDisplay.push( ); @@ -16,7 +26,7 @@ export default function MatchesScreen() matchDisplay.push( Match data has not been downloaded from TBA yet. Download is available under settings ); return ( - + {matchDisplay} ); diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index 61e16ea..0d16fac 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -56,7 +56,7 @@ export default function RegionalModal(props: ModalProps) regionalsDisplay.push( ); diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx index 585b5dc..8e9be80 100644 --- a/screens/Settings/DownloadingModal.tsx +++ b/screens/Settings/DownloadingModal.tsx @@ -4,13 +4,11 @@ import DarkBackground from "../../components/common/DarkBackground"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; -interface ModalProps -{ +interface ModalProps { status: string; } -export default function DownloadingModal(props: ModalProps) -{ +export default function DownloadingModal(props: ModalProps) { // Return Modal return ( @@ -18,11 +16,11 @@ export default function DownloadingModal(props: ModalProps) animationType="slide" transparent={true} visible={props.status !== ""} > - + - + - + Downloading {props.status} diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index 0d16fac..9ead72f 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -1,26 +1,22 @@ import React from "react"; -import { Alert, ScrollView, StyleSheet, View } from "react-native"; +import { Alert, StyleSheet } from "react-native"; import { TextInput } from "react-native-gesture-handler"; import { BlitzDB } from "../../api/BlitzDB"; import { TBAEvent } from "../../api/DBModels"; import { TBA } from "../../api/TBA"; import Button from "../../components/common/Button"; -import DarkBackground from "../../components/common/DarkBackground"; -import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; import Subtitle from "../../components/text/Subtitle"; import Text from "../../components/text/Text"; import Title from "../../components/text/Title"; import DownloadingModal from "./DownloadingModal"; -interface ModalProps -{ +interface ModalProps { isVisible: boolean; setVisible: (isVisible: boolean) => void; } -export default function RegionalModal(props: ModalProps) -{ +export default function RegionalModal(props: ModalProps) { const [searchTerm, updateSearch] = React.useState(""); const [regionalList, updateRegionals] = React.useState([] as TBAEvent[]); const [downloadStatus, setDownloadStatus] = React.useState(""); @@ -31,13 +27,12 @@ export default function RegionalModal(props: ModalProps) // Generate List let regionalsDisplay: JSX.Element[] = []; - if (regionalList.length <= 0 && props.isVisible) - { + if (regionalList.length <= 0 && props.isVisible) { TBA.getEvents().then((events) => { if (events) updateRegionals(events); }).catch(() => { - Alert.alert("Error","Could not connect to The Blue Alliance"); + Alert.alert("Error", "Could not connect to The Blue Alliance"); }); regionalsDisplay.push( @@ -46,13 +41,10 @@ export default function RegionalModal(props: ModalProps) ); } - else - { - for (let regional of regionalList) - { + else { + for (let regional of regionalList) { let key = regional.key; - if (regional.name.toLowerCase().includes(searchTerm)) - { + if (regional.name.toLowerCase().includes(searchTerm)) { regionalsDisplay.push( + ); + } + } + + // Display Data + + return ( + + Set Year + Select the current season / year: + + {yearsDisplay} + + + ); +} + +const styles = StyleSheet.create({ + regionalButton: { + padding: 8, + marginLeft: 4, + textAlign: "center" + }, + regionalText: { + fontSize: 16 + }, + loadingText: { + textAlign: "center", + fontStyle: "italic" + } +}); diff --git a/screens/Sharing/ExportQRModal.tsx b/screens/Sharing/ExportQRModal.tsx index 86b4afd..b8af8d2 100644 --- a/screens/Sharing/ExportQRModal.tsx +++ b/screens/Sharing/ExportQRModal.tsx @@ -1,18 +1,16 @@ import LZString from 'lz-string'; import * as React from 'react'; -import { Dimensions, Image, Modal, StyleSheet, View } from 'react-native'; +import { Dimensions, Modal, StyleSheet, View } from 'react-native'; import QRCode from 'react-qr-code'; import { BlitzDB } from '../../api/BlitzDB'; import DarkBackground from '../../components/common/DarkBackground'; -export interface ModalProps -{ +export interface ModalProps { isVisible: boolean; setVisible: (isVisible: boolean) => void; } -export default function ExportQRModal(props: ModalProps) -{ +export default function ExportQRModal(props: ModalProps) { // Default Behaviour if (!props.isVisible) return null; @@ -29,7 +27,7 @@ export default function ExportQRModal(props: ModalProps) - + void; } -export default function ImportQRModal(props: ModalProps) -{ +export default function ImportQRModal(props: ModalProps) { // TODO: View and import QRCode return null; } \ No newline at end of file diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index c43b2b0..dee7056 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -1,32 +1,30 @@ import * as React from 'react'; -import { Vibration } from 'react-native'; import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import ExportQRModal from './ExportQRModal'; import ImportQRModal from './ImportQRModal'; -export default function SharingScreen() -{ +export default function SharingScreen() { const [isExportQRVisible, setExportQRVisible] = React.useState(false); const [isImportQRVisible, setImportQRVisible] = React.useState(false); return ( - + {/* QR Codes */} { setExportQRVisible(true); }} /> { setImportQRVisible(true); }} /> @@ -34,14 +32,14 @@ export default function SharingScreen() {}} /> - + subtitle={"Export Scouting Data"} + onPress={() => { }} /> + {}} /> + subtitle={"Export Images"} + onPress={() => { }} /> {/* Cloud Save */} @@ -64,18 +62,18 @@ export default function SharingScreen() {}} /> + subtitle={"Import & Export Everything"} + onPress={() => { }} /> {}} /> + subtitle={"Import & Export Everything"} + onPress={() => { }} /> {}} /> + subtitle={"Import & Export Everything"} + onPress={() => { }} /> ); diff --git a/screens/Teams/TeamBanner.tsx b/screens/Teams/TeamBanner.tsx index 0b01b54..059beb0 100644 --- a/screens/Teams/TeamBanner.tsx +++ b/screens/Teams/TeamBanner.tsx @@ -4,37 +4,34 @@ import { BlitzDB } from "../../api/BlitzDB"; import StandardButton from "../../components/common/StandardButton"; import TeamModal from "./TeamModal"; -interface TeamBannerProps -{ +interface TeamBannerProps { teamID: string; } -export default function TeamBanner(props: TeamBannerProps) -{ +export default function TeamBanner(props: TeamBannerProps) { const [isVisible, setVisible] = React.useState(false); let team = BlitzDB.getTeam(props.teamID); - if (!team) - { + if (!team) { console.log("Could not find Team ID " + props.teamID); return null; } return ( - + - + 0 ? team.media[0] : undefined} iconType={team.media.length > 0 ? undefined : "ban"} title={team.name} subtitle={team.number.toString()} onPress={() => { setVisible(true); }} /> - + ); } diff --git a/screens/Teams/TeamMatchesModal.tsx b/screens/Teams/TeamMatchesModal.tsx index 86c0f45..28a0c38 100644 --- a/screens/Teams/TeamMatchesModal.tsx +++ b/screens/Teams/TeamMatchesModal.tsx @@ -1,35 +1,27 @@ -import { FontAwesome } from "@expo/vector-icons"; import React from "react"; -import { Alert, Image, ScrollView, StyleSheet } from "react-native"; -import * as ImagePicker from 'expo-image-picker'; -import PhotoModal from "../../components/containers/PhotoModal"; +import { Alert } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; -import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; -import Title from "../../components/text/Title"; import Subtitle from "../../components/text/Subtitle"; -import StandardButton from "../../components/common/StandardButton"; -import MatchBanner from "../Matches/MatchBanner"; import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; +import MatchBanner from "../Matches/MatchBanner"; -interface ModalProps -{ +interface ModalProps { teamID: string; isVisible: boolean; setVisible: (isVisible: boolean) => void; } -export default function TeamMatchesModal(props: ModalProps) -{ +export default function TeamMatchesModal(props: ModalProps) { // Default Behaviour if (!props.isVisible) return null; // Grab Team Data let team = BlitzDB.getTeam(props.teamID); - if (!(team) || !(BlitzDB.event)) - { + if (!(team) || !(BlitzDB.event)) { Alert.alert("Error", "There was an error grabbing team or event data. Try re-downloading TBA data then try again."); props.setVisible(false); return null; @@ -40,10 +32,10 @@ export default function TeamMatchesModal(props: ModalProps) let matchDisplay: JSX.Element[] = []; if (matchList.length > 0) for (let match of matchList) - matchDisplay.push( ); + matchDisplay.push(); else - matchDisplay.push( Match data has not been downloaded from TBA yet. Download is available under settings ); - + matchDisplay.push(Match data has not been downloaded from TBA yet. Download is available under settings); + // Return Modal return ( diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx index dd398c1..4ebec2a 100644 --- a/screens/Teams/TeamModal.tsx +++ b/screens/Teams/TeamModal.tsx @@ -1,27 +1,25 @@ import { FontAwesome } from "@expo/vector-icons"; -import React from "react"; -import { Alert, Image, Linking, ScrollView, StyleSheet } from "react-native"; import * as ImagePicker from 'expo-image-picker'; -import PhotoModal from "../../components/containers/PhotoModal"; +import React from "react"; +import { Alert, Image, ScrollView, StyleSheet } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; +import { TBA } from "../../api/TBA"; import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import Modal from "../../components/common/Modal"; -import Title from "../../components/text/Title"; -import Subtitle from "../../components/text/Subtitle"; import StandardButton from "../../components/common/StandardButton"; +import PhotoModal from "../../components/containers/PhotoModal"; +import Subtitle from "../../components/text/Subtitle"; +import Title from "../../components/text/Title"; import TeamMatchesModal from "./TeamMatchesModal"; -import { TBA } from "../../api/TBA"; -interface ModalProps -{ +interface ModalProps { teamID: string; isVisible: boolean; setVisible: (isVisible: boolean) => void; } -export default function TeamModal(props: ModalProps) -{ +export default function TeamModal(props: ModalProps) { const [previewIndex, setPreviewIndex] = React.useState(-1); const [isTeamMatchesVisible, setTeamMatchesVisible] = React.useState(false); const [version, setVersion] = React.useState(0); @@ -36,11 +34,10 @@ export default function TeamModal(props: ModalProps) setVersion(version + 1); setPreviewIndex(-1); }); - + // Grab Team Data let team = BlitzDB.getTeam(props.teamID); - if (!(team)) - { + if (!(team)) { Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); props.setVisible(false); return null; @@ -48,15 +45,14 @@ export default function TeamModal(props: ModalProps) // Grab Team Media let mediaList: JSX.Element[] = []; - for (let i = 0; i < team.media.length; i++) - { + for (let i = 0; i < team.media.length; i++) { let imageData = team.media[i]; mediaList.push( ); } @@ -70,33 +66,33 @@ export default function TeamModal(props: ModalProps) - + {team.name} {team.number} @@ -106,7 +102,7 @@ export default function TeamModal(props: ModalProps) iconType={"binoculars"} title={"Scout Team"} subtitle={"Pit scout this team"} - onPress={() => {}} /> + onPress={() => { }} /> { if (result.cancelled) return; @@ -153,15 +148,14 @@ function addPhoto(teamID: string) }); } -function addFile(teamID: string) -{ +function addFile(teamID: string) { return ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: .5, - - base64: true + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + + base64: true }).then(result => { if (result.cancelled) return; diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index fe439b3..aa081b0 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -1,21 +1,34 @@ import * as React from 'react'; +import { ToastAndroid } from 'react-native'; import { BlitzDB } from '../../api/BlitzDB'; -import TeamBanner from './TeamBanner'; import ScrollContainer from '../../components/containers/ScrollContainer'; import Text from '../../components/text/Text'; +import TeamBanner from './TeamBanner'; -export default function TeamsScreen() -{ +export default function TeamsScreen() { + const [version, setVersion] = React.useState(0); let teamList: JSX.Element[] = []; - - if (BlitzDB.currentTeamIDs.length > 0) - for (let teamID of BlitzDB.currentTeamIDs) - teamList.push( ); + + const onRefresh = async () => { + let success = await BlitzDB.downloadTeams(); + if (!success) + ToastAndroid.show("Failed to connect to TBA", 1000); + else + ToastAndroid.show("Updated team data", 1000); + setVersion(version + 1); + }; + + if (BlitzDB.event) + if (BlitzDB.currentTeamIDs.length > 0) + for (let teamID of BlitzDB.currentTeamIDs) + teamList.push(); + else + teamList.push(This event has no team data posted yet! You can try refreshing by pulling down.); else - teamList.push( Team data has not been downloaded from TBA yet. Download is available under settings ); + teamList.push(Team data has not been downloaded from TBA yet. Download is available under settings); return ( - + {teamList} ); From f882b7b7360169bbdaf5930da2a9d633577c9051 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 20 Oct 2021 07:47:22 -0500 Subject: [PATCH 15/38] Ported From Modals => Navigation Looks better in every way except for the ways it doesn't --- app.json | 72 ++++----- navigation/index.tsx | 20 ++- package.json | 1 + screens/Match/MatchScreen.tsx | 88 ++++++++++ screens/Matches/MatchBanner.tsx | 13 +- screens/Matches/MatchModal.tsx | 89 ---------- screens/Matches/TeamPreview.tsx | 19 +-- screens/Settings/SettingsScreen.tsx | 3 + screens/Team/TeamScreen.tsx | 181 +++++++++++++++++++++ screens/TeamMatches/TeamMatchesScreen.tsx | 40 +++++ screens/Teams/TeamBanner.tsx | 37 +---- screens/Teams/TeamMatchesModal.tsx | 52 ------ screens/Teams/TeamModal.tsx | 188 ---------------------- types.tsx | 38 +++-- 14 files changed, 408 insertions(+), 433 deletions(-) create mode 100644 screens/Match/MatchScreen.tsx delete mode 100644 screens/Matches/MatchModal.tsx create mode 100644 screens/Team/TeamScreen.tsx create mode 100644 screens/TeamMatches/TeamMatchesScreen.tsx delete mode 100644 screens/Teams/TeamMatchesModal.tsx delete mode 100644 screens/Teams/TeamModal.tsx diff --git a/app.json b/app.json index 6e00f37..9598ec1 100644 --- a/app.json +++ b/app.json @@ -1,38 +1,38 @@ { - "expo": { - "name": "Blitz Scouter", - "slug": "BlitzScouter", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/images/icon.png", - "scheme": "blitz", - "userInterfaceStyle": "light", - "splash": { - "image": "./assets/images/splash.png", - "resizeMode": "contain", - "backgroundColor": "#1e1e1e" - }, - "updates": { - "fallbackToCacheTimeout": 0 - }, - "assetBundlePatterns": [ - "**/*" - ], - "ios": { - "supportsTablet": true, - "bundleIdentifier": "org.team5148.blitzscouter", - "buildNumber": "1.0.0" - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/images/icon.png", - "backgroundColor": "#1e1e1e" - }, - "package": "org.team5148.blitzscouter", - "versionCode":1 - }, - "web": { - "favicon": "./assets/images/icon.png" + "expo": { + "name": "Blitz Scouter", + "slug": "BlitzScouter", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "blitz", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/images/splash.png", + "resizeMode": "contain", + "backgroundColor": "#1e1e1e" + }, + "updates": { + "fallbackToCacheTimeout": 0 + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.nbblitz.blitzscouter", + "buildNumber": "1.0.0" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/icon.png", + "backgroundColor": "#1e1e1e" + }, + "package": "com.nbblitz.blitzscouter", + "versionCode": 1 + }, + "web": { + "favicon": "./assets/images/icon.png" + } } - } -} +} \ No newline at end of file diff --git a/navigation/index.tsx b/navigation/index.tsx index 12e45f9..7e9d27b 100644 --- a/navigation/index.tsx +++ b/navigation/index.tsx @@ -3,9 +3,12 @@ import { createDrawerNavigator } from '@react-navigation/drawer'; import { DarkTheme, NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import * as React from 'react'; +import MatchScreen from '../screens/Match/MatchScreen'; import MatchesScreen from '../screens/Matches/MatchesScreen'; import SettingsScreen from '../screens/Settings/SettingsScreen'; import SharingScreen from '../screens/Sharing/SharingScreen'; +import TeamScreen from '../screens/Team/TeamScreen'; +import TeamMatchesScreen from '../screens/TeamMatches/TeamMatchesScreen'; import TeamsScreen from '../screens/Teams/TeamsScreen'; import { RootStackParamList, RootTabParamList } from '../types'; @@ -22,7 +25,22 @@ const Stack = createNativeStackNavigator(); function RootNavigator() { return ( - + + + + + + + ); + for (let teamID of match.redTeamIDs) + redTeams.push(); + + // Return Modal + return ( + + + {match.name} + {match.description} + + + + { }} /> + + { match ? TBA.openMatch(match.id) : null }} /> + + + + Red Alliance + + + + {redTeams} + + + + + + Blue Alliance + + + + {blueTeams} + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20 + }, + allianceHeader: { + marginBottom: 15 + } +}); diff --git a/screens/Matches/MatchBanner.tsx b/screens/Matches/MatchBanner.tsx index 12c7f35..800d2f4 100644 --- a/screens/Matches/MatchBanner.tsx +++ b/screens/Matches/MatchBanner.tsx @@ -1,17 +1,17 @@ +import { useNavigation } from "@react-navigation/core"; import React from "react"; import { StyleSheet, View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; import StandardButton from "../../components/common/StandardButton"; -import MatchModal from "./MatchModal"; interface MatchBannerProps { matchID: string; } export default function MatchBanner(props: MatchBannerProps) { - const [isVisible, setVisible] = React.useState(false); + const navigator = useNavigation(); - let match = BlitzDB.getMatch(props.matchID); + const match = BlitzDB.getMatch(props.matchID); if (!match) return null; @@ -21,12 +21,7 @@ export default function MatchBanner(props: MatchBannerProps) { iconText={match.number.toString()} title={match.name} subtitle={match.description} - onPress={() => { setVisible(true); }} /> - - + onPress={() => { navigator.navigate("Match", { matchID: match.id }) }} /> ); } diff --git a/screens/Matches/MatchModal.tsx b/screens/Matches/MatchModal.tsx deleted file mode 100644 index f2c8d36..0000000 --- a/screens/Matches/MatchModal.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from "react"; -import { Alert, ScrollView, StyleSheet, View } from "react-native"; -import { BlitzDB } from "../../api/BlitzDB"; -import { TBA } from "../../api/TBA"; -import HorizontalBar from "../../components/common/HorizontalBar"; -import Modal from "../../components/common/Modal"; -import StandardButton from "../../components/common/StandardButton"; -import Subtitle from "../../components/text/Subtitle"; -import Title from "../../components/text/Title"; -import TeamPreview from "./TeamPreview"; - -interface ModalProps { - matchID: string; - isVisible: boolean; - setVisible: (isVisible: boolean) => void; -} - -export default function MatchModal(props: ModalProps) { - // Default Behaviour - if (!props.isVisible) - return null; - - // Grab Match Data - let match = BlitzDB.getMatch(props.matchID); - if (!(match)) { - Alert.alert("Error", "There was an error grabbing the data from that match. Try re-downloading TBA data then try again."); - props.setVisible(false); - return null; - } - - // Grab Team List - let redTeams = []; - let blueTeams = []; - for (let teamID of match.blueTeamIDs) - blueTeams.push(); - for (let teamID of match.redTeamIDs) - redTeams.push(); - - // Return Modal - return ( - - - {match.name} - {match.description} - - - - { }} /> - - { match ? TBA.openMatch(match.id) : null }} /> - - - - Red Alliance - - - - {redTeams} - - - - - - Blue Alliance - - - - {blueTeams} - - - - - - ); -} - -const styles = StyleSheet.create({ - allianceHeader: { - marginBottom: 15 - } -}); diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index a93157e..de4dce8 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -1,17 +1,17 @@ import { FontAwesome } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/core'; import * as React from 'react'; import { Alert, Image, StyleSheet, View } from "react-native"; import { BlitzDB } from '../../api/BlitzDB'; import Button from '../../components/common/Button'; import Text from '../../components/text/Text'; -import TeamModal from '../Teams/TeamModal'; interface TeamThumbnailProps { teamID: string } export default function TeamPreview(props: TeamThumbnailProps) { - const [isVisible, setVisible] = React.useState(false); + const navigator = useNavigation(); // Grab Team Data let team = BlitzDB.getTeam(props.teamID); @@ -46,12 +46,7 @@ export default function TeamPreview(props: TeamThumbnailProps) { return ( + ); + } + + // Return Modal + return ( + + + + {mediaList} + + + + + + + {team.name} + {team.number} + + + + { }} /> + + { navigator.navigate("TeamMatches", { teamID }) }} /> + + { team ? TBA.openTeam(team.number) : null }} /> + + + + + + + ); +} + +function addPhoto(teamID: string) { + return ImagePicker.launchCameraAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + + base64: true + }).then(result => { + if (result.cancelled) + return; + if (!result.base64) + return; + + BlitzDB.addTeamMedia(teamID, result.base64); + }); +} + +function addFile(teamID: string) { + return ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + + base64: true + }).then(result => { + if (result.cancelled) + return; + if (!result.base64) + return; + + BlitzDB.addTeamMedia(teamID, result.base64); + }); +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20 + }, + media: { + marginBottom: 10, + marginLeft: -20, + height: 200, + width: Dimensions.get("window").width + }, + thumbnail: { + height: 200, + width: 200 + }, + imageButton: { + height: 200, + width: 200, + justifyContent: "center", + flexDirection: "row", + backgroundColor: "#444" + }, +}); diff --git a/screens/TeamMatches/TeamMatchesScreen.tsx b/screens/TeamMatches/TeamMatchesScreen.tsx new file mode 100644 index 0000000..b0e98e0 --- /dev/null +++ b/screens/TeamMatches/TeamMatchesScreen.tsx @@ -0,0 +1,40 @@ +import { useNavigation } from "@react-navigation/core"; +import React from "react"; +import { ScrollView, StyleSheet, View } from "react-native"; +import { BlitzDB } from "../../api/BlitzDB"; +import Text from "../../components/text/Text"; +import MatchBanner from "../Matches/MatchBanner"; + +export interface TeamMatchesProps { + teamID: string; +} + +export default function TeamMatchesScreen({ route }: any) { + const navigator = useNavigation(); + const teamID = route.params.teamID; + + // Matches + let matchList = BlitzDB.getTeamMatches(teamID); + let matchDisplay: JSX.Element[] = []; + if (matchList.length > 0) + for (let match of matchList) + matchDisplay.push(); + else + matchDisplay.push(Match data has not been downloaded from TBA yet. Download is available under settings); + + // Return Modal + return ( + + + {matchDisplay} + + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20 + } +}); diff --git a/screens/Teams/TeamBanner.tsx b/screens/Teams/TeamBanner.tsx index 059beb0..4235500 100644 --- a/screens/Teams/TeamBanner.tsx +++ b/screens/Teams/TeamBanner.tsx @@ -1,17 +1,17 @@ +import { useNavigation } from "@react-navigation/core"; import React from "react"; -import { StyleSheet, View } from "react-native"; +import { View } from "react-native"; import { BlitzDB } from "../../api/BlitzDB"; import StandardButton from "../../components/common/StandardButton"; -import TeamModal from "./TeamModal"; interface TeamBannerProps { teamID: string; } export default function TeamBanner(props: TeamBannerProps) { - const [isVisible, setVisible] = React.useState(false); + const navigator = useNavigation(); - let team = BlitzDB.getTeam(props.teamID); + const team = BlitzDB.getTeam(props.teamID); if (!team) { console.log("Could not find Team ID " + props.teamID); return null; @@ -20,38 +20,13 @@ export default function TeamBanner(props: TeamBannerProps) { return ( - - 0 ? team.media[0] : undefined} iconType={team.media.length > 0 ? undefined : "ban"} title={team.name} subtitle={team.number.toString()} - onPress={() => { setVisible(true); }} /> + onPress={() => { navigator.navigate("Team", { teamID: team.id }) }} /> ); -} - -const styles = StyleSheet.create({ - teamButton: { - flexDirection: "row", - justifyContent: "flex-start" - }, - teamImage: { - width: 40, - height: 40, - marginRight: 10, - resizeMode: 'stretch' - }, - teamName: { - fontSize: 18, - textAlign: "left" - }, - teamNumber: { - color: "#bbb" - } -}); +} \ No newline at end of file diff --git a/screens/Teams/TeamMatchesModal.tsx b/screens/Teams/TeamMatchesModal.tsx deleted file mode 100644 index 28a0c38..0000000 --- a/screens/Teams/TeamMatchesModal.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import { Alert } from "react-native"; -import { BlitzDB } from "../../api/BlitzDB"; -import HorizontalBar from "../../components/common/HorizontalBar"; -import Modal from "../../components/common/Modal"; -import Subtitle from "../../components/text/Subtitle"; -import Text from "../../components/text/Text"; -import Title from "../../components/text/Title"; -import MatchBanner from "../Matches/MatchBanner"; - -interface ModalProps { - teamID: string; - isVisible: boolean; - setVisible: (isVisible: boolean) => void; -} - -export default function TeamMatchesModal(props: ModalProps) { - // Default Behaviour - if (!props.isVisible) - return null; - - // Grab Team Data - let team = BlitzDB.getTeam(props.teamID); - if (!(team) || !(BlitzDB.event)) { - Alert.alert("Error", "There was an error grabbing team or event data. Try re-downloading TBA data then try again."); - props.setVisible(false); - return null; - } - - // Matches - let matchList = BlitzDB.getTeamMatches(props.teamID); - let matchDisplay: JSX.Element[] = []; - if (matchList.length > 0) - for (let match of matchList) - matchDisplay.push(); - else - matchDisplay.push(Match data has not been downloaded from TBA yet. Download is available under settings); - - // Return Modal - return ( - - {team.name} - Team {team.number}'s Match List - - - {matchDisplay} - - - - - ); -} \ No newline at end of file diff --git a/screens/Teams/TeamModal.tsx b/screens/Teams/TeamModal.tsx deleted file mode 100644 index 4ebec2a..0000000 --- a/screens/Teams/TeamModal.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { FontAwesome } from "@expo/vector-icons"; -import * as ImagePicker from 'expo-image-picker'; -import React from "react"; -import { Alert, Image, ScrollView, StyleSheet } from "react-native"; -import { BlitzDB } from "../../api/BlitzDB"; -import { TBA } from "../../api/TBA"; -import Button from "../../components/common/Button"; -import HorizontalBar from "../../components/common/HorizontalBar"; -import Modal from "../../components/common/Modal"; -import StandardButton from "../../components/common/StandardButton"; -import PhotoModal from "../../components/containers/PhotoModal"; -import Subtitle from "../../components/text/Subtitle"; -import Title from "../../components/text/Title"; -import TeamMatchesModal from "./TeamMatchesModal"; - -interface ModalProps { - teamID: string; - isVisible: boolean; - setVisible: (isVisible: boolean) => void; -} - -export default function TeamModal(props: ModalProps) { - const [previewIndex, setPreviewIndex] = React.useState(-1); - const [isTeamMatchesVisible, setTeamMatchesVisible] = React.useState(false); - const [version, setVersion] = React.useState(0); - - // Default Behaviour - if (!props.isVisible) - return null; - - // Re-render on New Media - BlitzDB.eventEmitter.addListener("mediaUpdate", () => { - BlitzDB.eventEmitter.removeCurrentListener(); - setVersion(version + 1); - setPreviewIndex(-1); - }); - - // Grab Team Data - let team = BlitzDB.getTeam(props.teamID); - if (!(team)) { - Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); - props.setVisible(false); - return null; - } - - // Grab Team Media - let mediaList: JSX.Element[] = []; - for (let i = 0; i < team.media.length; i++) { - let imageData = team.media[i]; - mediaList.push( - - ); - } - - // Return Modal - return ( - - - - {mediaList} - - - - - - - {team.name} - {team.number} - - - - { }} /> - - { setTeamMatchesVisible(true); }} /> - - { team ? TBA.openTeam(team.number) : null }} /> - - - - - - - ); -} - -function addPhoto(teamID: string) { - return ImagePicker.launchCameraAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: .5, - - base64: true - }).then(result => { - if (result.cancelled) - return; - if (!result.base64) - return; - - BlitzDB.addTeamMedia(teamID, result.base64); - }); -} - -function addFile(teamID: string) { - return ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [1, 1], - quality: .5, - - base64: true - }).then(result => { - if (result.cancelled) - return; - if (!result.base64) - return; - - BlitzDB.addTeamMedia(teamID, result.base64); - }); -} - -const styles = StyleSheet.create({ - media: { - marginBottom: 10, - height: 150, - width: "100%" - }, - thumbnail: { - height: 150, - width: 150, - margin: 5 - }, - imageButton: { - height: 150, - width: 150, - justifyContent: "center", - flexDirection: "row", - backgroundColor: "#444", - margin: 5 - }, -}); diff --git a/types.tsx b/types.tsx index 5161642..f227834 100644 --- a/types.tsx +++ b/types.tsx @@ -3,33 +3,41 @@ * https://reactnavigation.org/docs/typescript/ */ -import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; -import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native'; -import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { NavigatorScreenParams } from '@react-navigation/native'; +import { MatchProps } from './screens/Match/MatchScreen'; +import { TeamProps } from './screens/Team/TeamScreen'; +import { TeamMatchesProps } from './screens/TeamMatches/TeamMatchesScreen'; declare global { - namespace ReactNavigation { - interface RootParamList extends RootStackParamList {} - } + namespace ReactNavigation { + interface RootParamList extends RootStackParamList { } + } } export type RootStackParamList = { - Root: NavigatorScreenParams | undefined; + Root: NavigatorScreenParams | undefined; + Team: TeamProps; + Match: MatchProps; + TeamMatches: TeamMatchesProps; }; +/* export type RootStackScreenProps = NativeStackScreenProps< - RootStackParamList, - Screen + RootStackParamList, + Screen >; +*/ export type RootTabParamList = { - Teams: undefined; - Matches: undefined; - Sharing: undefined; - Settings: undefined; + Teams: undefined; + Matches: undefined; + Sharing: undefined; + Settings: undefined; }; +/* export type RootTabScreenProps = CompositeScreenProps< - BottomTabScreenProps, - NativeStackScreenProps + BottomTabScreenProps, + NativeStackScreenProps >; +*/ From d809531215a74ee17693379f11c3c9c4b2cd7271 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 10 Nov 2021 15:10:12 -0600 Subject: [PATCH 16/38] Database Re-write --- api/BlitzDB.ts | 327 ++++-------------- api/DBModels.ts | 91 ----- api/EventDB.ts | 94 +++++ api/MatchDB.ts | 135 ++++++++ api/TBA.ts | 60 +++- api/TeamDB.ts | 140 ++++++++ api/models/Match.ts | 13 + api/models/TBAModels.ts | 40 +++ api/models/Team.ts | 10 + api/models/TemplateModels.ts | 19 + components/common/Button.tsx | 10 +- components/common/DarkBackground.tsx | 8 +- components/common/FadeIn.tsx | 15 +- components/common/HorizontalBar.tsx | 5 +- components/common/Modal.tsx | 12 +- components/common/StandardButton.tsx | 26 +- components/containers/PhotoModal.tsx | 48 ++- components/containers/ScrollContainer.tsx | 8 +- components/elements/HRElement.tsx | 5 +- components/elements/ScoutingElement.tsx | 8 +- components/elements/SubtitleElement.tsx | 5 +- components/elements/TextElement.tsx | 5 +- components/elements/TitleElement.tsx | 5 +- components/text/Header.tsx | 10 +- components/text/Subtitle.tsx | 10 +- components/text/Title.tsx | 10 +- hooks/useCachedResources.ts | 5 +- screens/Match/MatchScreen.tsx | 6 +- screens/Matches/MatchBanner.tsx | 4 +- screens/Matches/MatchesScreen.tsx | 17 +- screens/Matches/TeamPreview.tsx | 4 +- screens/Settings/RegionalModal.tsx | 8 +- screens/Settings/SettingsScreen.tsx | 6 +- .../Settings/Template/ElementChooserModal.tsx | 7 +- screens/Settings/Template/TemplateModal.tsx | 7 +- screens/Settings/YearModal.tsx | 6 +- screens/Sharing/ExportQRModal.tsx | 2 +- screens/Team/TeamScreen.tsx | 16 +- screens/TeamMatches/TeamMatchesScreen.tsx | 2 +- screens/Teams/TeamBanner.tsx | 4 +- screens/Teams/TeamsScreen.tsx | 16 +- 41 files changed, 716 insertions(+), 513 deletions(-) delete mode 100644 api/DBModels.ts create mode 100644 api/EventDB.ts create mode 100644 api/MatchDB.ts create mode 100644 api/TeamDB.ts create mode 100644 api/models/Match.ts create mode 100644 api/models/TBAModels.ts create mode 100644 api/models/Team.ts create mode 100644 api/models/TemplateModels.ts diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts index 8eff684..dbe7576 100644 --- a/api/BlitzDB.ts +++ b/api/BlitzDB.ts @@ -1,295 +1,116 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import { Alert, NativeEventEmitter } from "react-native"; -import { Event, Match, ScoutingTemplate, Team, TemplateType } from "./DBModels"; -import { TBA } from "./TBA"; - -const BASE64_PREFIX = "data:image/png;base64, "; -const MATCH_TYPES = ["qm", "qf", "sf", "f"]; - -export class BlitzDB { - static event?: Event; - static currentTeamIDs: string[] = []; - static teams: Team[] = []; - static templates: Record = [[], []]; - static eventEmitter = new NativeEventEmitter(); - - static async downloadAll(eventID: string, setDownloadStatus: Function) { - BlitzDB.event = { - id: eventID, - matches: [], - teams: [] - }; - +import { Alert } from "react-native"; +import EventDB from "./EventDB"; +import MatchDB from "./MatchDB"; +import Match from "./models/Match"; +import TeamDB from "./TeamDB"; +export default class BlitzDB { + static teams: TeamDB = new TeamDB(); + static matches: MatchDB = new MatchDB(); + static event: EventDB = new EventDB(); + + /** + * Downloads all event data from the Blue Alliance + * @param eventID - ID of event to download + * @param setDownloadStatus - Sets the download status of the event + * @returns + */ + static async downloadEvent(eventID: string, setDownloadStatus: (status: string) => void) { // Teams - setDownloadStatus("Downloading Team Roster..."); - let success1 = await BlitzDB.downloadTeams(); - if (!success1) { - setDownloadStatus(""); - return; - } - - - // Team Media - let teamCount = 0; - BlitzDB._loadCurrentTeams(); - for (let teamID of BlitzDB.currentTeamIDs) { - teamCount++; - setDownloadStatus("Downloading Team Media... (" + teamCount + "/" + BlitzDB.event.teams.length + ")"); - - let mediaList = await TBA.getTeamMedia(teamID); - if (mediaList) { - for (let media of mediaList) { - // Get Image Data - let imageData: string | undefined; - if (media.details.base64Image) - imageData = BASE64_PREFIX + media.details.base64Image; - else if (media.details.model_image) - imageData = media.details.model_image; - else if (media.direct_url) - imageData = media.direct_url; - - // Prevent Duplicate Images - if (imageData) { - let team = BlitzDB.getTeam(teamID); - if (team) - if (!team.media.find(media => media === imageData)) - team.media.push(imageData); - } - } - } - } + setDownloadStatus("Downloading Teams..."); + let teamSuccess = await this.teams.downloadEvent(eventID, (teamNumber) => { + setDownloadStatus("Downloading Team " + teamNumber + "..."); + }); + if (!teamSuccess) + return setDownloadStatus(""); // Matches - setDownloadStatus("Downloading Match List..."); - let success2 = await BlitzDB.downloadMatches(); - if (!success2) { - setDownloadStatus(""); - return; - } + let matchSuccess = await this.matches.downloadEvent(eventID, (matchNumber) => { + setDownloadStatus("Downloading Match " + matchNumber + "..."); + }); + if (!matchSuccess) + return setDownloadStatus(""); + + // Event + this.event.setLoaded(true, eventID); // Save setDownloadStatus("Saving..."); - await BlitzDB.save(); + await this.saveAll(); // Exit setDownloadStatus(""); Alert.alert("Success", "Successfully downloaded data from The Blue Alliance."); } - static async downloadTeams(): Promise { - if (!BlitzDB.event) - return false; - - let tbaTeams = await TBA.getTeams(BlitzDB.event.id); - if (!tbaTeams) - return false; - - tbaTeams.sort((a, b) => a.team_number - b.team_number); - for (let tbaTeam of tbaTeams) { - let existingTeam = BlitzDB.getTeam(tbaTeam.key); - if (!(existingTeam)) { - BlitzDB.teams.push({ - id: tbaTeam.key, - name: tbaTeam.nickname, - number: tbaTeam.team_number, - media: [], - comments: [] - }); - } - BlitzDB.event.teams.push(tbaTeam.key); - } - return true; - } - - static async downloadMatches(): Promise { - if (!BlitzDB.event) - return false; - - // Get Matches - let tbaMatches = await TBA.getMatches(BlitzDB.event.id) - if (!tbaMatches) - return false; - - // Parse Matches - BlitzDB.event.matches = []; - for (let tbaMatch of tbaMatches) { - // Match Name - let matchName = tbaMatch.comp_level + "-" + tbaMatch.match_number; - switch (tbaMatch.comp_level) { - case "qm": - matchName = "Qualification " + tbaMatch.match_number; - break; - case "qf": - matchName = "Quarter-Finals " + tbaMatch.match_number; - break; - case "sf": - matchName = "Semi-Finals " + tbaMatch.match_number; - break; - case "f": - matchName = "Finals " + tbaMatch.match_number; - break; - } - - // Match Description / Teams - let matchDesc = ""; - for (let teamKey of tbaMatch.alliances.blue.team_keys) { - let teamNumber = parseInt(teamKey.substring(3)); - matchDesc += teamNumber + " "; - } - matchDesc += " - " - for (let teamKey of tbaMatch.alliances.red.team_keys) { - let teamNumber = parseInt(teamKey.substring(3)); - matchDesc += teamNumber + " "; - } - - // Add to DB - BlitzDB.event.matches.push({ - id: tbaMatch.key, - name: matchName, - description: matchDesc, - number: tbaMatch.match_number, - compLevel: tbaMatch.comp_level, - blueTeamIDs: tbaMatch.alliances.blue.team_keys, - redTeamIDs: tbaMatch.alliances.red.team_keys, - comment: "" - }); - } - - // Sort - BlitzDB.event.matches.sort((a, b) => - (a.number + MATCH_TYPES.indexOf(a.compLevel) * 500) - (b.number + MATCH_TYPES.indexOf(b.compLevel) * 500) - ); - - return true; - } - - static addTeamMedia(teamID: string, imageData: string) { - let team = BlitzDB.getTeam(teamID); - if (team) { - team.media.push(BASE64_PREFIX + imageData); - BlitzDB.save(); - BlitzDB.eventEmitter.emit("mediaUpdate"); - console.log("Added Media to " + teamID); - } - } - - static removeTeamMedia(teamID: string, mediaIndex: number) { - let team = BlitzDB.getTeam(teamID); - if (team) { - team.media.splice(mediaIndex, 1); - BlitzDB.save(); - BlitzDB.eventEmitter.emit("mediaUpdate"); - console.log("Removed Media " + mediaIndex + " from " + teamID); - } - } - - static swapTeamMedia(teamID: string, mediaIndex1: number, mediaIndex2?: number) { - let team = BlitzDB.getTeam(teamID); - if (team) { - if (mediaIndex2 === undefined) - mediaIndex2 = team.media.length - 1; - let tempMedia = team.media[mediaIndex1]; - team.media[mediaIndex1] = team.media[mediaIndex2]; - team.media[mediaIndex2] = tempMedia; - BlitzDB.save(); - BlitzDB.eventEmitter.emit("mediaUpdate"); - console.log("Swapped Media " + mediaIndex1 + " <-> " + mediaIndex2 + " from " + teamID); - } - } - - static getTeam(teamID: string): Team | undefined { - return BlitzDB.teams.find(team => team.id === teamID); - } - - static getMatch(matchID: string): Match | undefined { - if (BlitzDB.event) - return BlitzDB.event.matches.find(match => match.id === matchID); - } - - static setYear(year: number) { - TBA.year = year; + /** + * Saves all DB data to storage + */ + static async saveAll() { + await this.teams.save(); + await this.matches.save(); + await this.event.save(); } + /** + * Gets all of the matches from a team + * @param teamID - ID of team to get matches from + * @returns an array of matches + */ static getTeamMatches(teamID: string): Match[] { - if (!(this.event)) - return []; - let matchList: Match[] = []; - for (let match of this.event.matches) + for (let match of this.matches.matchCache) if (match.blueTeamIDs.includes(teamID) || match.redTeamIDs.includes(teamID)) matchList.push(match); return matchList; } + /** + * Exports all comments in a compressed format + * @returns compressed string of comments + */ static exportComments(): string { let data = ""; - for (let team of BlitzDB.teams) { + for (let team of this.teams.teamCache) { if (team.comments.length > 0) data += ";;" + team.id; for (let comment of team.comments) { - data += "::" + comment.text; + data += "::" + comment; } } return data; } - static async loadSave() { - let eventData = await AsyncStorage.getItem('event_data'); - if (eventData) - BlitzDB.event = JSON.parse(eventData); - - let teamData = await AsyncStorage.getItem('team_data'); - if (teamData) - BlitzDB.teams = JSON.parse(teamData); - - BlitzDB._loadCurrentTeams(); - } - - static async save() { - if (BlitzDB.event) - await AsyncStorage.setItem('event_data', JSON.stringify(BlitzDB.event)); - else - await AsyncStorage.removeItem('event_data'); - await AsyncStorage.setItem('team_data', JSON.stringify(BlitzDB.teams)); + /** + * Loads all DB data from storage + */ + static async loadAll() { + await this.teams.load(); + await this.matches.load(); + await this.event.load(); } + /** + * Deletes all data from the database + * @param alert - Whether or not to alert the user + */ static async deleteAll(alert: boolean) { if (alert) { - Alert.alert("Are you sure?", "This will delete all scouting data from your device. Are you sure you want to continue?", - [ - { - text: "Confirm", - onPress: () => { - BlitzDB._deleteAll().then(() => { - Alert.alert("Done", "All data has been cleared"); - }); - } - }, - { text: "Cancel", style: "cancel" } - ], { cancelable: true } - ); + Alert.alert("Are you sure?", "This will delete all scouting data from your device. Are you sure you want to continue?", [{ + text: "Confirm", + onPress: () => { + BlitzDB.deleteAll(false).then(() => { + Alert.alert("Done", "All data has been cleared"); + }); + } + }, + { text: "Cancel", style: "cancel" }], { cancelable: true }); } else { - await this._deleteAll(); - } - } - - static async _deleteAll() { - BlitzDB.event = undefined; - BlitzDB.currentTeamIDs = []; - BlitzDB.teams = []; - await BlitzDB.save(); - } - - static _loadCurrentTeams() { - BlitzDB.currentTeamIDs = []; - - if (BlitzDB.event) { - for (let teamID of BlitzDB.event.teams) { - BlitzDB.currentTeamIDs.push(teamID); - } + this.teams.deleteAll(); + this.matches.deleteAll(); + this.event.deleteAll(); } } } \ No newline at end of file diff --git a/api/DBModels.ts b/api/DBModels.ts deleted file mode 100644 index 96fe2d5..0000000 --- a/api/DBModels.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* The Blue Alliance */ -export interface TBATeam { - key: string; - nickname: string; - team_number: number; -} -export interface TBAMatch { - alliances: { - blue: { - score: number; - team_keys: string[]; - }, - red: { - score: number; - team_keys: string[]; - } - }; - comp_level: string; - event_key: string; - key: string; - match_number: number; - videos: any[]; - winning_alliance: string; -} -export interface TBAEvent { - key: string; - name: string; -} -export interface TBAMedia { - details: { - base64Image?: string; - model_image?: string; - }, - direct_url?: string; -} - -export interface TBAStatus { - is_datafeed_down: boolean; - max_season: number; - current_season: number; -} - -/* Database */ -export interface Team { - id: string; - name: string; - number: number; - media: string[]; - comments: Comment[]; -} -export interface Match { - id: string; - name: string; - description: string; - number: number; - compLevel: string; - blueTeamIDs: string[]; - redTeamIDs: string[]; - comment: string; -} -export interface Event { - id: string; - matches: Match[]; - teams: string[]; -} -export interface Comment { - isScanned: boolean; - timestamp: number; - text: string; -} - -/* Template */ -export enum TemplateType { - Pit, - Match -} - -export enum ElementType { - title, - subtitle, - text, - hr -} - -export interface ElementData { - type: ElementType; - label: string; - options: any; -} - -export type ScoutingTemplate = ElementData[]; \ No newline at end of file diff --git a/api/EventDB.ts b/api/EventDB.ts new file mode 100644 index 0000000..729e98d --- /dev/null +++ b/api/EventDB.ts @@ -0,0 +1,94 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import TBA from "./TBA"; + +const SAVE_KEY = "event-data"; + +/** + * Handles the currently loaded event + */ +export default class EventDB { + isLoaded: boolean = false; + id: string = ""; + year: number = 0; + matchIDs: string[] = []; + teamIDs: string[] = []; + + /** + * Updates the list of matches at an event + * @param matchIDs - ID of each match in an event + */ + setMatches(matchIDs: string[]) { + this.matchIDs = matchIDs; + } + + /** + * Updates the list of teams at an event + * @param teamIDs - ID of each team at an event + */ + setTeams(teamIDs: string[]) { + this.teamIDs = teamIDs; + } + + /** + * Sets the current event year + * @param year - Year to set to + */ + setYear(year: number) { + this.year = year; + TBA.setYear(year); + } + + /** + * Sets whether or not the event is loaded + * @param isLoaded - True if the event is loaded + * @param eventID - ID of the event + */ + setLoaded(isLoaded: boolean, eventID?: string) { + this.isLoaded = isLoaded; + if (eventID) + this.id = eventID; + } + + /** + * Deletes all event data + */ + async deleteAll() { + this.isLoaded = false; + this.id = ""; + this.year = 0; + this.matchIDs = []; + this.teamIDs = []; + AsyncStorage.removeItem(SAVE_KEY); + } + + /** + * Saves all event data + */ + async save() { + let eventData = { + matchIDs: this.matchIDs, + teamIDs: this.teamIDs, + year: this.year, + id: this.id + }; + let jsonEvent = JSON.stringify(eventData); + await AsyncStorage.setItem(SAVE_KEY, jsonEvent); + } + + /** + * Loads all event data + */ + async load() { + let jsonEvent = await AsyncStorage.getItem(SAVE_KEY); + if (!jsonEvent) + return; + let event = JSON.parse(jsonEvent); + this.matchIDs = event.matchIDs as string[]; + this.teamIDs = event.teamIDs as string[]; + this.year = event.year as number; + this.id = event.id as string; + this.isLoaded = true; + + TBA.setYear(this.year); + } +} \ No newline at end of file diff --git a/api/MatchDB.ts b/api/MatchDB.ts new file mode 100644 index 0000000..96eb908 --- /dev/null +++ b/api/MatchDB.ts @@ -0,0 +1,135 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import BlitzDB from "./BlitzDB"; +import Match from "./models/Match"; +import { TBAMatch } from "./models/TBAModels"; +import TBA from "./TBA"; + +const SAVE_KEY = "match-data"; +const MATCH_TYPES = ["qm", "qf", "sf", "f"]; + +/** + * Represents a database of event matches + */ +export default class MatchDB { + matchCache: Match[] = []; + + /** + * Downloads all matches at an event + * @param eventID - ID of the event + */ + async downloadEvent(eventID: string, matchCallback: (matchNumber: number) => void): Promise { + + // Download + let tbaMatches = await TBA.getMatches(eventID); + if (!tbaMatches) + return false; + + // Sort + tbaMatches.sort((a, b) => + (a.match_number + MATCH_TYPES.indexOf(a.comp_level) * 1000) - + (b.match_number + MATCH_TYPES.indexOf(b.comp_level) * 1000) + ); + + // Add + let matchIDs: string[] = []; + for (let match of tbaMatches) { + matchCallback(match.match_number); + matchIDs.push(match.key); + let existingMatch = this.get(match.key); + if (!existingMatch) { + this.matchCache.push({ + id: match.key, + name: this._generateName(match), + description: this._generateDesc(match), + number: match.match_number, + compLevel: match.comp_level, + blueTeamIDs: match.alliances.blue.team_keys, + redTeamIDs: match.alliances.red.team_keys, + comment: "" + }); + } + } + BlitzDB.event.setMatches(matchIDs); + + return true; + } + + /** + * Gets the a match by its id + * @param matchID - The ID of the match + */ + get(matchID: string): Match | undefined { + let match = this.matchCache.find(match => match.id === matchID); + return match; + } + + /** + * Generates a name for a match + * (Ex: "Qualifications 12") + * @param match - Match to generate name + */ + _generateName(match: TBAMatch): string { + let matchName = match.comp_level + "-" + match.match_number; + switch (match.comp_level) { + case "qm": + matchName = "Qualification " + match.match_number; + break; + case "qf": + matchName = "Quarter-Finals " + match.match_number; + break; + case "sf": + matchName = "Semi-Finals " + match.match_number; + break; + case "f": + matchName = "Finals " + match.match_number; + break; + } + return matchName; + } + + /** + * Generates a description for a match + * (Ex: "5141 5142 5143 - 1231 1232 1233") + * @param match - Match to generate name + */ + _generateDesc(match: TBAMatch): string { + let matchDesc = ""; + match.alliances.blue.team_keys.forEach(teamKey => { + let teamNumber = parseInt(teamKey.substring(3)); + matchDesc += teamNumber + " "; + }); + matchDesc += " - " + match.alliances.red.team_keys.forEach(teamKey => { + let teamNumber = parseInt(teamKey.substring(3)); + matchDesc += teamNumber + " "; + }); + return matchDesc; + } + + /** + * Deletes all matches from the database + */ + async deleteAll() { + this.matchCache = []; + await AsyncStorage.removeItem(SAVE_KEY); + } + + /** + * Saves data from the cache to the database + */ + async save() { + let jsonMatches = JSON.stringify(this.matchCache); + await AsyncStorage.setItem(SAVE_KEY, jsonMatches); + } + + /** + * Loads the match cache + */ + async load() { + let jsonMatches = await AsyncStorage.getItem(SAVE_KEY); + if (!jsonMatches) + return; + let matches = JSON.parse(jsonMatches); + this.matchCache = matches; + } +} \ No newline at end of file diff --git a/api/TBA.ts b/api/TBA.ts index 0ab732f..8168e01 100644 --- a/api/TBA.ts +++ b/api/TBA.ts @@ -1,47 +1,91 @@ import { Linking } from 'react-native'; -import { TBAEvent, TBAMatch, TBAMedia, TBAStatus, TBATeam } from './DBModels'; - +import { TBAEvent, TBAMatch, TBAMedia, TBAStatus, TBATeam } from './models/TBAModels'; const API_KEY = "i90dAcKHXvQ9havypHJKeGY8O1tfymFpaW1Po3RGYpvoMTRVwtiUsUFaLmstCDp3"; const URL_PREFIX = "https://www.thebluealliance.com/api/v3/"; const URL_SUFFIX = "?X-TBA-Auth-Key=" + API_KEY; const DEFAULT_YEAR = 2020; -export class TBA { - static year = DEFAULT_YEAR; +export default class TBA { + static currentYear = DEFAULT_YEAR; + + /** + * Sets the current year + * @param year - Year to set to + */ + static setYear(year: number) { + TBA.currentYear = year; + } + /** + * Fetches all matches at a given event + * @param eventID - ID of the event + * @returns An array of TBAMatch + */ static getMatches(eventID: string) { return TBA._fetch("event/" + eventID + "/matches/simple"); } + /** + * Fetches all events in a current year + * @returns An array of TBAEvent + */ static async getEvents() { - return TBA._fetch("events/" + TBA.year); + return TBA._fetch("events/" + TBA.currentYear); } + /** + * Fetches all teams at a given event + * @param eventID - ID of the event + * @returns An array of TBATeam + */ static async getTeams(eventID: string) { return TBA._fetch("event/" + eventID + "/teams/simple"); } + + /** + * Fetches all media of a given team + * @param teamID - ID of the team + * @returns An array of TBAMedia + */ static getTeamMedia(teamID: string) { - return TBA._fetch("team/" + teamID + "/media/" + TBA.year); + return TBA._fetch("team/" + teamID + "/media/" + TBA.currentYear); } + /** + * Fetches the server status of The Blue Alliance + * @returns Status in the form of TBAStatus + */ static getServerStatus() { return TBA._fetch("status"); } + /** + * Opens a team in a new browser window + * @param teamNumber - Number of the team + */ static openTeam(teamNumber: number) { - Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + TBA.year); + Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + TBA.currentYear); } + /** + * Opens a match in a new browser window + * @param matchID - ID of the match + */ static openMatch(matchID: string) { Linking.openURL("https://www.thebluealliance.com/match/" + matchID); } + /** + * Fetches a url from the TBA API + * @param path - API route + * @returns The parsed response from the API + */ static _fetch(path: string): Promise { /* https://github.com/whatwg/fetch/issues/180 - TL;DR; fetch doesn't include a timeout by default. + TL;DR; fetch doesn't include a request timeout by default. While this solution does introduce memory leaks, there is no other option until a better solution is implemented. */ diff --git a/api/TeamDB.ts b/api/TeamDB.ts new file mode 100644 index 0000000..cb87d7c --- /dev/null +++ b/api/TeamDB.ts @@ -0,0 +1,140 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import BlitzDB from "./BlitzDB"; +import Team from "./models/Team"; +import TBA from "./TBA"; + +const SAVE_KEY = "teams-data"; +const BASE64_PREFIX = "data:image/png;base64, "; + +/** + * Represents a database of FRC teams + */ +export default class TeamDB { + teamCache: Team[] = []; + + /** + * Downloads all the teams attending an event + * @param event - Event to reference teams + * @returns true if successful + */ + async downloadEvent(eventID: string, teamCallback: (teamNumber: number) => void): Promise { + + // Download + let tbaTeams = await TBA.getTeams(eventID); + if (!tbaTeams) + return false; + tbaTeams.sort((a, b) => a.team_number - b.team_number); + + // Parse + let teamIDs: string[] = []; + for (let team of tbaTeams) { + teamCallback(team.team_number); + teamIDs.push(team.key); + let existingTeam = this.get(team.key); + if (!existingTeam) { + this.teamCache.push({ + id: team.key, + name: team.nickname, + number: team.team_number, + media: [], + comments: [] + }); + } + await this.downloadMedia(team.key); + } + BlitzDB.event.setTeams(teamIDs); + + return true; + } + + /** + * Downloads all media from an FRC team + * @param teamID - ID of the team + * @returns true if successful + */ + async downloadMedia(teamID: string) { + let mediaList = await TBA.getTeamMedia(teamID); + if (!mediaList) + return false; + mediaList.forEach(media => { + let imageData: string | undefined; + if (media.details.base64Image) + imageData = BASE64_PREFIX + media.details.base64Image; + else if (media.details.model_image) + imageData = media.details.model_image; + else if (media.direct_url) + imageData = media.direct_url; + if (imageData) + this.addMedia(teamID, imageData, true); + }); + } + + /** + * Gets a team by it's id + * @param teamID - TBA ID of the team + */ + get(teamID: string): Team | undefined { + let team = this.teamCache.find(team => team.id === teamID); + return team; + } + + /** + * Adds a team to the database + * @param team - Team to add + */ + add(team: Team) { + this.teamCache.push(team); + } + + /** + * Adds media onto the team + * @param teamID - ID of the team + * @param media - Media in Base64 format + */ + addMedia(teamID: string, media: string, removePrefix?: boolean) { + + // TODO: Store media outside of database + let team = this.get(teamID); + let b64 = removePrefix ? media : BASE64_PREFIX + media; + if (team) + if (team.media.indexOf(b64) === -1) + team.media.push(b64); + } + + /** + * Adds comment onto the team + * @param teamID - ID of the team + * @param comment - Text of the comment + */ + addComment(teamID: string, comment: string) { + let team = this.get(teamID); + if (team) + team.comments.push(comment); + } + + /** + * Deletes all teams from the database + */ + async deleteAll() { + this.teamCache = []; + await AsyncStorage.removeItem(SAVE_KEY); + } + + /** + * Saves data from the cache to the database + */ + async save() { + let jsonTeams = JSON.stringify(this.teamCache); + await AsyncStorage.setItem(SAVE_KEY, jsonTeams); + } + + /** + * Loads the team cache + */ + async load() { + let jsonTeams = await AsyncStorage.getItem(SAVE_KEY); + if (!jsonTeams) + return; + this.teamCache = JSON.parse(jsonTeams) as Team[]; + } +} \ No newline at end of file diff --git a/api/models/Match.ts b/api/models/Match.ts new file mode 100644 index 0000000..1a9c889 --- /dev/null +++ b/api/models/Match.ts @@ -0,0 +1,13 @@ +/** + * Represents a game match + */ +export default interface Match { + id: string; + name: string; + description: string; + number: number; + compLevel: string; + blueTeamIDs: string[]; + redTeamIDs: string[]; + comment: string; +} \ No newline at end of file diff --git a/api/models/TBAModels.ts b/api/models/TBAModels.ts new file mode 100644 index 0000000..715831f --- /dev/null +++ b/api/models/TBAModels.ts @@ -0,0 +1,40 @@ +export interface TBATeam { + key: string; + nickname: string; + team_number: number; +} +export interface TBAMatch { + alliances: { + blue: { + score: number; + team_keys: string[]; + }, + red: { + score: number; + team_keys: string[]; + } + }; + comp_level: string; + event_key: string; + key: string; + match_number: number; + videos: any[]; + winning_alliance: string; +} +export interface TBAEvent { + key: string; + name: string; +} +export interface TBAMedia { + details: { + base64Image?: string; + model_image?: string; + }, + direct_url?: string; +} + +export interface TBAStatus { + is_datafeed_down: boolean; + max_season: number; + current_season: number; +} \ No newline at end of file diff --git a/api/models/Team.ts b/api/models/Team.ts new file mode 100644 index 0000000..6db8adc --- /dev/null +++ b/api/models/Team.ts @@ -0,0 +1,10 @@ +/** + * Represents an FRC Team + */ +export default interface Team { + id: string; + name: string; + number: number; + media: string[]; + comments: string[]; +} \ No newline at end of file diff --git a/api/models/TemplateModels.ts b/api/models/TemplateModels.ts new file mode 100644 index 0000000..ae88026 --- /dev/null +++ b/api/models/TemplateModels.ts @@ -0,0 +1,19 @@ +export enum TemplateType { + Pit, + Match +} + +export enum ElementType { + title, + subtitle, + text, + hr +} + +export interface ElementData { + type: ElementType; + label: string; + options: any; +} + +export type ScoutingTemplate = ElementData[]; \ No newline at end of file diff --git a/components/common/Button.tsx b/components/common/Button.tsx index 678caba..2a83869 100644 --- a/components/common/Button.tsx +++ b/components/common/Button.tsx @@ -5,10 +5,10 @@ export type ButtonProps = TouchableOpacity['props']; export default function Button(props: ButtonProps) { const { style, ...otherProps } = props; - + return ; + alignItems: "center", + padding: 10, + alignSelf: 'stretch' + }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/components/common/DarkBackground.tsx b/components/common/DarkBackground.tsx index 42e1805..2711256 100644 --- a/components/common/DarkBackground.tsx +++ b/components/common/DarkBackground.tsx @@ -1,15 +1,13 @@ import * as React from 'react'; import { StyleSheet, View } from "react-native"; -interface DarkBackgroundProps -{ +interface DarkBackgroundProps { isTransparent: boolean; } -export default function DarkBackground(props: DarkBackgroundProps) -{ +export default function DarkBackground(props: DarkBackgroundProps) { return ( - + ); } diff --git a/components/common/FadeIn.tsx b/components/common/FadeIn.tsx index 59980a6..a6d9a68 100644 --- a/components/common/FadeIn.tsx +++ b/components/common/FadeIn.tsx @@ -1,11 +1,10 @@ -import * as React from 'react'; import { useFocusEffect } from "@react-navigation/core"; +import * as React from 'react'; import { Animated, View } from "react-native"; export type ViewProps = View['props']; -export default function FadeIn(props: ViewProps) -{ +export default function FadeIn(props: ViewProps) { const { style, ...otherProps } = props; const fadeAnim = React.useRef(new Animated.Value(0)).current; @@ -26,10 +25,10 @@ export default function FadeIn(props: ViewProps) }); return ( + style={[{ + flex: 1, + opacity: fadeAnim, + }, style]} + {...otherProps} /> ); } \ No newline at end of file diff --git a/components/common/HorizontalBar.tsx b/components/common/HorizontalBar.tsx index 37774d2..f9e615a 100644 --- a/components/common/HorizontalBar.tsx +++ b/components/common/HorizontalBar.tsx @@ -3,8 +3,7 @@ import { View } from "react-native"; export type ViewProps = View['props']; -export default function HorizontalBar(props: ViewProps) -{ +export default function HorizontalBar(props: ViewProps) { const { style, ...otherProps } = props; return (); } \ No newline at end of file diff --git a/components/common/Modal.tsx b/components/common/Modal.tsx index 076740e..6b5ad0f 100644 --- a/components/common/Modal.tsx +++ b/components/common/Modal.tsx @@ -1,18 +1,16 @@ import * as React from 'react'; import { ReactNode } from "react"; -import { StyleSheet, Modal as DefaultModal, View, ScrollView } from "react-native"; +import { Modal as DefaultModal, ScrollView, StyleSheet, View } from "react-native"; import Text from '../text/Text'; import Button from './Button'; import DarkBackground from "./DarkBackground"; -export interface ModalProps -{ +export interface ModalProps { setVisible: (isVisible: boolean) => void; children: ReactNode; } -export default function Modal(props: ModalProps) -{ +export default function Modal(props: ModalProps) { return ( {props.setVisible(false);}}> + onPress={() => { props.setVisible(false); }}> Return - + ); diff --git a/components/common/StandardButton.tsx b/components/common/StandardButton.tsx index a195051..39c06af 100644 --- a/components/common/StandardButton.tsx +++ b/components/common/StandardButton.tsx @@ -1,12 +1,9 @@ import { FontAwesome } from '@expo/vector-icons'; import * as React from 'react'; -import { ReactNode } from "react"; -import { StyleSheet, Modal as DefaultModal, View, ScrollView, TouchableOpacity, GestureResponderEvent, Image } from "react-native"; +import { GestureResponderEvent, Image, StyleSheet, TouchableOpacity, View } from "react-native"; import Text from '../text/Text'; -import DarkBackground from "./DarkBackground"; -export interface ButtonProps -{ +export interface ButtonProps { iconType?: React.ComponentProps['name']; iconText?: string; iconData?: string; @@ -16,31 +13,30 @@ export interface ButtonProps onPress: (event: GestureResponderEvent) => void; } -export default function StandardButton(props: ButtonProps) -{ +export default function StandardButton(props: ButtonProps) { return ( - + {/* Text Icon */} - {props.iconText ? + {props.iconText ? {props.iconText} - : null} + : null} {/* FA Icon */} {props.iconType ? - + - : null} + : null} {/* SVG Icon */} {props.iconData ? - - : null} - + + : null} + {/* Titles */} diff --git a/components/containers/PhotoModal.tsx b/components/containers/PhotoModal.tsx index 1d4ff49..1e3da88 100644 --- a/components/containers/PhotoModal.tsx +++ b/components/containers/PhotoModal.tsx @@ -1,29 +1,25 @@ import { FontAwesome } from "@expo/vector-icons"; import React from "react"; import { Alert, Dimensions, Image, Modal, StyleSheet, ToastAndroid, View } from "react-native"; -import { BlitzDB } from "../../api/BlitzDB"; +import BlitzDB from "../../api/BlitzDB"; import Button from "../common/Button"; import DarkBackground from "../common/DarkBackground"; import Text from "../text/Text"; -import * as Sharing from 'expo-sharing'; -interface PhotoProps -{ +interface PhotoProps { teamID: string; imageIndex: number; setImageIndex: (imageIndex: number) => void; } // TODO Photo Zoom, Delete, and Re-Arrange -export default function PhotoModal(props: PhotoProps) -{ +export default function PhotoModal(props: PhotoProps) { if (props.imageIndex < 0) return null; // Team - let team = BlitzDB.getTeam(props.teamID); - if (!(team)) - { + let team = BlitzDB.teams.get(props.teamID); + if (!(team)) { Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); props.setImageIndex(-1); return null; @@ -41,8 +37,8 @@ export default function PhotoModal(props: PhotoProps) - - + + - + ); } -function trashImage(teamID: string, imageIndex: number) -{ - Alert.alert( "Are you sure?", "Are you sure you want to delete this image?", +function trashImage(teamID: string, imageIndex: number) { + Alert.alert("Are you sure?", "Are you sure you want to delete this image?", [ { text: "Confirm", - onPress: () => { BlitzDB.removeTeamMedia(teamID, imageIndex); } + onPress: () => { /*BlitzDB.removeTeamMedia(teamID, imageIndex);*/ } }, { text: "Cancel", @@ -106,13 +101,12 @@ function trashImage(teamID: string, imageIndex: number) ); } -function setThumbnail(teamID: string, imageIndex: number) -{ - Alert.alert( "Are you sure?", "Are you sure you want to set this as Team Thumbnail?", +function setThumbnail(teamID: string, imageIndex: number) { + Alert.alert("Are you sure?", "Are you sure you want to set this as Team Thumbnail?", [ { text: "Confirm", - onPress: () => { BlitzDB.swapTeamMedia(teamID, imageIndex, 0); } + onPress: () => { /*BlitzDB.swapTeamMedia(teamID, imageIndex, 0);*/ } }, { text: "Cancel", @@ -122,13 +116,12 @@ function setThumbnail(teamID: string, imageIndex: number) ); } -function setRobot(teamID: string, imageIndex: number) -{ - Alert.alert( "Are you sure?", "Are you sure you want to set this as the Robot Image?", +function setRobot(teamID: string, imageIndex: number) { + Alert.alert("Are you sure?", "Are you sure you want to set this as the Robot Image?", [ { text: "Confirm", - onPress: () => { BlitzDB.swapTeamMedia(teamID, imageIndex); } + onPress: () => { /*BlitzDB.swapTeamMedia(teamID, imageIndex);*/ } }, { text: "Cancel", @@ -138,11 +131,10 @@ function setRobot(teamID: string, imageIndex: number) ); } -function shareImage(mediaData: string) -{ +function shareImage(mediaData: string) { // TODO Convert the storage medium for image data from sqlite to local device storage //Sharing.shareAsync(mediaData); // <-- Using the Expo Sharing Library - + ToastAndroid.show("To be implemented!", ToastAndroid.SHORT); } @@ -154,7 +146,7 @@ const styles = StyleSheet.create({ left: 0, bottom: 0, right: 0 - + }, image: { width: Dimensions.get('window').width, diff --git a/components/containers/ScrollContainer.tsx b/components/containers/ScrollContainer.tsx index 341ca89..c700779 100644 --- a/components/containers/ScrollContainer.tsx +++ b/components/containers/ScrollContainer.tsx @@ -1,16 +1,14 @@ import * as React from 'react'; -import { RefreshControl, ScrollView, StyleProp, ToastAndroid, View, ViewStyle } from "react-native"; +import { RefreshControl, ScrollView, View } from "react-native"; import FadeIn from '../common/FadeIn'; export type ViewProps = View['props']; -interface ScrollContainerProps -{ +interface ScrollContainerProps { children: React.ReactNode onRefresh?: () => Promise; } -export default function ScrollContainer(props: ScrollContainerProps) -{ +export default function ScrollContainer(props: ScrollContainerProps) { const [isRefreshing, setRefreshing] = React.useState(false); const onRefresh = React.useCallback(() => { diff --git a/components/elements/HRElement.tsx b/components/elements/HRElement.tsx index 35e822a..5412800 100644 --- a/components/elements/HRElement.tsx +++ b/components/elements/HRElement.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { ElementData } from "../../api/DBModels"; +import { ElementData } from "../../api/models/TemplateModels"; import HorizontalBar from "../common/HorizontalBar"; -export default function HRElement(props: {data: ElementData}) -{ +export default function HRElement(props: { data: ElementData }) { return ( ); diff --git a/components/elements/ScoutingElement.tsx b/components/elements/ScoutingElement.tsx index 54bc838..6450a1f 100644 --- a/components/elements/ScoutingElement.tsx +++ b/components/elements/ScoutingElement.tsx @@ -1,14 +1,12 @@ import React from "react"; -import { ElementData, ElementType } from "../../api/DBModels"; +import { ElementData, ElementType } from "../../api/models/TemplateModels"; import HRElement from "./HRElement"; import SubtitleElement from "./SubtitleElement"; import TextElement from "./TextElement"; import TitleElement from "./TitleElement"; -export default function ScoutingElement(props: {data: ElementData}): JSX.Element -{ - switch (props.data.type) - { +export default function ScoutingElement(props: { data: ElementData }) { + switch (props.data.type) { case ElementType.title: return (); case ElementType.subtitle: diff --git a/components/elements/SubtitleElement.tsx b/components/elements/SubtitleElement.tsx index f1c8576..d8fb24b 100644 --- a/components/elements/SubtitleElement.tsx +++ b/components/elements/SubtitleElement.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { ElementData } from "../../api/DBModels"; +import { ElementData } from "../../api/models/TemplateModels"; import Subtitle from "../text/Subtitle"; -export default function SubtitleElement(props: {data: ElementData}) -{ +export default function SubtitleElement(props: { data: ElementData }) { return ( {props.data.label} ); diff --git a/components/elements/TextElement.tsx b/components/elements/TextElement.tsx index 8d7e24d..3b6378c 100644 --- a/components/elements/TextElement.tsx +++ b/components/elements/TextElement.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { ElementData } from "../../api/DBModels"; +import { ElementData } from "../../api/models/TemplateModels"; import Text from "../text/Text"; -export default function TextElement(props: {data: ElementData}) -{ +export default function TextElement(props: { data: ElementData }) { return ( {props.data.label} ); diff --git a/components/elements/TitleElement.tsx b/components/elements/TitleElement.tsx index 933dd98..047cf95 100644 --- a/components/elements/TitleElement.tsx +++ b/components/elements/TitleElement.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { ElementData } from "../../api/DBModels"; +import { ElementData } from "../../api/models/TemplateModels"; import Title from "../text/Title"; -export default function TitleElement(props: {data: ElementData}) -{ +export default function TitleElement(props: { data: ElementData }) { return ( {props.data.label} ); diff --git a/components/text/Header.tsx b/components/text/Header.tsx index d9bf3fd..bfd1203 100644 --- a/components/text/Header.tsx +++ b/components/text/Header.tsx @@ -5,10 +5,10 @@ export type TextProps = Text['props']; export default function Header(props: TextProps) { const { style, ...otherProps } = props; - + return ; + color: "#fff", + fontSize: 24, + fontWeight: 'bold' + }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/components/text/Subtitle.tsx b/components/text/Subtitle.tsx index d552515..579093b 100644 --- a/components/text/Subtitle.tsx +++ b/components/text/Subtitle.tsx @@ -5,10 +5,10 @@ export type TextProps = Text['props']; export default function Subtitle(props: TextProps) { const { style, ...otherProps } = props; - + return ; + color: "#bbb", + fontSize: 15, + fontWeight: 'bold' + }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/components/text/Title.tsx b/components/text/Title.tsx index bf04200..23da4fb 100644 --- a/components/text/Title.tsx +++ b/components/text/Title.tsx @@ -5,10 +5,10 @@ export type TextProps = Text['props']; export default function Title(props: TextProps) { const { style, ...otherProps } = props; - + return ; + color: "#fff", + fontSize: 30, + fontWeight: 'bold' + }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/hooks/useCachedResources.ts b/hooks/useCachedResources.ts index c18be49..cb34421 100644 --- a/hooks/useCachedResources.ts +++ b/hooks/useCachedResources.ts @@ -2,7 +2,7 @@ import { FontAwesome } from '@expo/vector-icons'; import * as Font from 'expo-font'; import * as SplashScreen from 'expo-splash-screen'; import * as React from 'react'; -import { BlitzDB } from '../api/BlitzDB'; +import BlitzDB from '../api/BlitzDB'; export default function useCachedResources() { const [isLoadingComplete, setLoadingComplete] = React.useState(false); @@ -20,10 +20,9 @@ export default function useCachedResources() { }); // Load Save File - await BlitzDB.loadSave(); + await BlitzDB.loadAll(); } catch (e) { - // We might want to provide this error information to an error reporting service console.warn(e); } finally { setLoadingComplete(true); diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index 8fa715f..95d5230 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Alert, ScrollView, StyleSheet, View } from "react-native"; -import { BlitzDB } from "../../api/BlitzDB"; -import { TBA } from "../../api/TBA"; +import BlitzDB from "../../api/BlitzDB"; +import TBA from "../../api/TBA"; import HorizontalBar from "../../components/common/HorizontalBar"; import StandardButton from "../../components/common/StandardButton"; import Subtitle from "../../components/text/Subtitle"; @@ -16,7 +16,7 @@ export default function MatchScreen({ route }: any) { // Grab Match Data const matchID = route.params.matchID; - const match = BlitzDB.getMatch(matchID); + const match = BlitzDB.matches.get(matchID); if (!(match)) { Alert.alert("Error", "There was an error grabbing the data from that match. Try re-downloading TBA data then try again."); return null; diff --git a/screens/Matches/MatchBanner.tsx b/screens/Matches/MatchBanner.tsx index 800d2f4..4d2ba40 100644 --- a/screens/Matches/MatchBanner.tsx +++ b/screens/Matches/MatchBanner.tsx @@ -1,7 +1,7 @@ import { useNavigation } from "@react-navigation/core"; import React from "react"; import { StyleSheet, View } from "react-native"; -import { BlitzDB } from "../../api/BlitzDB"; +import BlitzDB from "../../api/BlitzDB"; import StandardButton from "../../components/common/StandardButton"; interface MatchBannerProps { @@ -11,7 +11,7 @@ interface MatchBannerProps { export default function MatchBanner(props: MatchBannerProps) { const navigator = useNavigation(); - const match = BlitzDB.getMatch(props.matchID); + const match = BlitzDB.matches.get(props.matchID); if (!match) return null; diff --git a/screens/Matches/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx index 53965ef..6e04821 100644 --- a/screens/Matches/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -1,16 +1,16 @@ import * as React from 'react'; import { ToastAndroid } from 'react-native'; -import { BlitzDB } from '../../api/BlitzDB'; +import BlitzDB from '../../api/BlitzDB'; import ScrollContainer from '../../components/containers/ScrollContainer'; import Text from '../../components/text/Text'; import MatchBanner from './MatchBanner'; export default function MatchesScreen() { const [version, setVersion] = React.useState(0); - let matchDisplay: JSX.Element[] = []; + // Updates matches on refresh const onRefresh = async () => { - let success = await BlitzDB.downloadMatches(); + let success = await BlitzDB.matches.downloadEvent(BlitzDB.event.id, () => { }); if (!success) ToastAndroid.show("Failed to connect to TBA", 1000); else @@ -18,15 +18,18 @@ export default function MatchesScreen() { setVersion(version + 1); }; - if (BlitzDB.event) - if (BlitzDB.event.matches.length > 0) - for (let match of BlitzDB.event.matches) - matchDisplay.push(); + // Adds MatchBanner to display if the event is downloaded + let matchDisplay: JSX.Element[] = []; + if (BlitzDB.event.isLoaded) + if (BlitzDB.event.matchIDs.length > 0) + for (let matchID of BlitzDB.event.matchIDs) + matchDisplay.push(); else matchDisplay.push(This event has no match data posted yet! You can try refreshing by pulling down.); else matchDisplay.push(Match data has not been downloaded from TBA yet. Download is available under settings); + // Return return ( {matchDisplay} diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index de4dce8..278b930 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -2,7 +2,7 @@ import { FontAwesome } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/core'; import * as React from 'react'; import { Alert, Image, StyleSheet, View } from "react-native"; -import { BlitzDB } from '../../api/BlitzDB'; +import BlitzDB from '../../api/BlitzDB'; import Button from '../../components/common/Button'; import Text from '../../components/text/Text'; @@ -14,7 +14,7 @@ export default function TeamPreview(props: TeamThumbnailProps) { const navigator = useNavigation(); // Grab Team Data - let team = BlitzDB.getTeam(props.teamID); + let team = BlitzDB.teams.get(props.teamID); if (!(team)) { Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); return null; diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalModal.tsx index 9ead72f..d7d4a14 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalModal.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Alert, StyleSheet } from "react-native"; import { TextInput } from "react-native-gesture-handler"; -import { BlitzDB } from "../../api/BlitzDB"; -import { TBAEvent } from "../../api/DBModels"; -import { TBA } from "../../api/TBA"; +import BlitzDB from "../../api/BlitzDB"; +import { TBAEvent } from "../../api/models/TBAModels"; +import TBA from "../../api/TBA"; import Button from "../../components/common/Button"; import Modal from "../../components/common/Modal"; import Subtitle from "../../components/text/Subtitle"; @@ -48,7 +48,7 @@ export default function RegionalModal(props: ModalProps) { regionalsDisplay.push( + + + + + + ); +} + +function trashImage(teamID: string, imageIndex: number) { + Alert.alert("Are you sure?", "Are you sure you want to delete this image?", + [ + { + text: "Confirm", + onPress: () => { /*BlitzDB.removeTeamMedia(teamID, imageIndex);*/ } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true } + ); +} + +function shareImage(mediaData: string) { + // TODO Convert the storage medium for image data from sqlite to local device storage + //Sharing.shareAsync(mediaData); // <-- Using the Expo Sharing Library + + ToastAndroid.show("To be implemented!", ToastAndroid.SHORT); +} + +const styles = StyleSheet.create({ + container: { + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + + }, + image: { + width: Dimensions.get('window').width, + height: Dimensions.get('window').width, + borderRadius: 5 + }, + button: { + minWidth: 80 + }, + buttonText: { + fontSize: 12, + marginTop: 1 + }, + buttonBar: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, + flexDirection: "row", + justifyContent: "space-evenly" + } +}); diff --git a/components/containers/PanContainer.tsx b/components/containers/PanContainer.tsx new file mode 100644 index 0000000..a4a2fd0 --- /dev/null +++ b/components/containers/PanContainer.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { View } from "react-native"; +import { PanGestureHandler } from "react-native-gesture-handler"; +import Animated, { useAnimatedGestureHandler, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; + +export type ViewProps = View['props']; + +export default function PanContainer(props: ViewProps) { + // Hooks + const start = { x: useSharedValue(0), y: useSharedValue(0) }; + const end = { x: useSharedValue(0), y: useSharedValue(0) }; + + // Handler + const gestureHandler = useAnimatedGestureHandler({ + onStart: (event, ctx) => { + + }, + onActive: (event, ctx) => { + end.x.value = start.x.value + event.translationX; + end.y.value = start.y.value + event.translationY; + }, + onEnd: (event, ctx) => { + start.x.value = end.x.value; + start.y.value = end.y.value; + } + }); + + // Style + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ translateX: end.x.value }, { translateY: end.y.value }], + }; + }); + + return ( + + + + + + ); +} \ No newline at end of file diff --git a/components/containers/PanZoomContainer.tsx b/components/containers/PanZoomContainer.tsx new file mode 100644 index 0000000..e5a464b --- /dev/null +++ b/components/containers/PanZoomContainer.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { View } from "react-native"; +import { PanGestureHandler, PanGestureHandlerGestureEvent, PinchGestureHandler, PinchGestureHandlerGestureEvent } from "react-native-gesture-handler"; +import Animated, { useAnimatedGestureHandler, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; + +export type ViewProps = View['props']; + +export default function PanZoomContainer(props: ViewProps) { + // Hooks + const pinchRef = React.createRef(); + const startPan = { x: useSharedValue(0), y: useSharedValue(0) }; + const endPan = { x: useSharedValue(0), y: useSharedValue(0) }; + const startZoom = useSharedValue(1); + const endZoom = useSharedValue(1); + + // Handler + const panGestureHandler = useAnimatedGestureHandler({ + onStart: (event, ctx) => { + + }, + onActive: (event, ctx: any) => { + endPan.x.value = startPan.x.value + event.translationX / endZoom.value; + endPan.y.value = startPan.y.value + event.translationY / endZoom.value; + }, + onEnd: (event, ctx) => { + startPan.x.value = endPan.x.value; + startPan.y.value = endPan.y.value; + } + }); + const zoomGestureHandler = useAnimatedGestureHandler({ + onStart: (event, ctx) => { + + }, + onActive: (event, ctx) => { + endZoom.value = startZoom.value * event.scale; + }, + onEnd: (event, ctx) => { + startZoom.value = endZoom.value; + } + }); + + // Style + const panStyle = useAnimatedStyle(() => { + return { + transform: [{ translateX: endPan.x.value }, { translateY: endPan.y.value }], + }; + }); + const zoomStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: endZoom.value }], + }; + }); + + return ( + + + + + + + + ); +} \ No newline at end of file diff --git a/components/containers/PhotoModal.tsx b/components/containers/PhotoModal.tsx deleted file mode 100644 index 1e3da88..0000000 --- a/components/containers/PhotoModal.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { FontAwesome } from "@expo/vector-icons"; -import React from "react"; -import { Alert, Dimensions, Image, Modal, StyleSheet, ToastAndroid, View } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; -import Button from "../common/Button"; -import DarkBackground from "../common/DarkBackground"; -import Text from "../text/Text"; - -interface PhotoProps { - teamID: string; - imageIndex: number; - setImageIndex: (imageIndex: number) => void; -} - -// TODO Photo Zoom, Delete, and Re-Arrange -export default function PhotoModal(props: PhotoProps) { - if (props.imageIndex < 0) - return null; - - // Team - let team = BlitzDB.teams.get(props.teamID); - if (!(team)) { - Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); - props.setImageIndex(-1); - return null; - } - - // Media - if (props.imageIndex >= team.media.length) - return null; - let mediaData = team.media[props.imageIndex] - - return ( - props.setImageIndex(-1)} > - - - - - - - - - - - - - - - - - - ); -} - -function trashImage(teamID: string, imageIndex: number) { - Alert.alert("Are you sure?", "Are you sure you want to delete this image?", - [ - { - text: "Confirm", - onPress: () => { /*BlitzDB.removeTeamMedia(teamID, imageIndex);*/ } - }, - { - text: "Cancel", - style: "cancel" - } - ], { cancelable: true } - ); -} - -function setThumbnail(teamID: string, imageIndex: number) { - Alert.alert("Are you sure?", "Are you sure you want to set this as Team Thumbnail?", - [ - { - text: "Confirm", - onPress: () => { /*BlitzDB.swapTeamMedia(teamID, imageIndex, 0);*/ } - }, - { - text: "Cancel", - style: "cancel" - } - ], { cancelable: true } - ); -} - -function setRobot(teamID: string, imageIndex: number) { - Alert.alert("Are you sure?", "Are you sure you want to set this as the Robot Image?", - [ - { - text: "Confirm", - onPress: () => { /*BlitzDB.swapTeamMedia(teamID, imageIndex);*/ } - }, - { - text: "Cancel", - style: "cancel" - } - ], { cancelable: true } - ); -} - -function shareImage(mediaData: string) { - // TODO Convert the storage medium for image data from sqlite to local device storage - //Sharing.shareAsync(mediaData); // <-- Using the Expo Sharing Library - - ToastAndroid.show("To be implemented!", ToastAndroid.SHORT); -} - -const styles = StyleSheet.create({ - container: { - position: "absolute", - justifyContent: "center", - top: 0, - left: 0, - bottom: 0, - right: 0 - - }, - image: { - width: Dimensions.get('window').width, - height: Dimensions.get('window').width - }, - button: { - minWidth: 80 - }, - buttonText: { - fontSize: 12, - marginTop: 1 - }, - buttonBar: { - position: "absolute", - bottom: 0, - left: 0, - right: 0, - flexDirection: "row", - justifyContent: "space-evenly" - } -}); diff --git a/components/containers/ScrollContainer.tsx b/components/containers/ScrollContainer.tsx index c700779..70358bf 100644 --- a/components/containers/ScrollContainer.tsx +++ b/components/containers/ScrollContainer.tsx @@ -23,19 +23,20 @@ export default function ScrollContainer(props: ScrollContainerProps) { return ( : undefined - } - > + }> + {props.children} + ); diff --git a/components/containers/ZoomContainer.tsx b/components/containers/ZoomContainer.tsx new file mode 100644 index 0000000..0678c5d --- /dev/null +++ b/components/containers/ZoomContainer.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { View } from "react-native"; +import { PinchGestureHandler, PinchGestureHandlerGestureEvent } from "react-native-gesture-handler"; +import Animated, { useAnimatedGestureHandler, useAnimatedStyle, useSharedValue } from "react-native-reanimated"; + +export type ViewProps = View['props']; + +export default function ZoomContainer(props: ViewProps) { + // Hooks + const start = useSharedValue(1); + const end = useSharedValue(1); + + // Handler + const gestureHandler = useAnimatedGestureHandler({ + onStart: (event, ctx) => { + + }, + onActive: (event, ctx) => { + end.value = start.value * event.scale; + }, + onEnd: (event, ctx) => { + start.value = end.value; + } + }); + + // Style + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [{ scale: end.value }], + }; + }); + + return ( + + + + + + ); +} \ No newline at end of file diff --git a/components/text/NavTitle.tsx b/components/text/NavTitle.tsx new file mode 100644 index 0000000..fec0ae8 --- /dev/null +++ b/components/text/NavTitle.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { Text } from "react-native"; + +export type TextProps = Text['props']; + +export default function NavTitle(props: TextProps) { + const { style, ...otherProps } = props; + + return ; +} \ No newline at end of file diff --git a/components/text/Title.tsx b/components/text/Title.tsx index 23da4fb..765e1ad 100644 --- a/components/text/Title.tsx +++ b/components/text/Title.tsx @@ -9,6 +9,7 @@ export default function Title(props: TextProps) { return ; } \ No newline at end of file diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx new file mode 100644 index 0000000..b03244e --- /dev/null +++ b/navigation/RootNavigator.tsx @@ -0,0 +1,38 @@ +import { DarkTheme, NavigationContainer } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import * as React from 'react'; +import MediaScreen from "../components/containers/MediaScreen"; +import MatchScreen from "../screens/Match/MatchScreen"; +import RegionalScreen from "../screens/Settings/RegionalScreen"; +import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; +import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; +import YearScreen from "../screens/Settings/YearScreen"; +import TeamScreen from "../screens/Team/TeamScreen"; +import TabNavigator from "./TabNavigator"; + +const Stack = createNativeStackNavigator(); + +export default function RootNavigator() { + return ( + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/navigation/TabNavigator.tsx b/navigation/TabNavigator.tsx new file mode 100644 index 0000000..7ec5530 --- /dev/null +++ b/navigation/TabNavigator.tsx @@ -0,0 +1,67 @@ +import { MaterialIcons } from '@expo/vector-icons'; +import { BottomTabBarButtonProps, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import * as React from 'react'; +import { TouchableNativeFeedback, View } from 'react-native'; +import MatchesScreen from '../screens/Matches/MatchesScreen'; +import SettingsScreen from '../screens/Settings/SettingsScreen'; +import SharingScreen from '../screens/Sharing/SharingScreen'; +import TeamsScreen from '../screens/Teams/TeamsScreen'; + +const Tab = createBottomTabNavigator(); +export default function TabNavigator() { + const buttonNativeFeedback = ({ children, style, ...props }: BottomTabBarButtonProps) => ( + + {children} + + ); + + return ( + + + + { return () } }} + /> + { return () } }} + /> + { return () } }} + /> + { return () } }} + /> + + ); +} \ No newline at end of file diff --git a/navigation/index.tsx b/navigation/index.tsx deleted file mode 100644 index 7e9d27b..0000000 --- a/navigation/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { FontAwesome } from '@expo/vector-icons'; -import { createDrawerNavigator } from '@react-navigation/drawer'; -import { DarkTheme, NavigationContainer } from '@react-navigation/native'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import * as React from 'react'; -import MatchScreen from '../screens/Match/MatchScreen'; -import MatchesScreen from '../screens/Matches/MatchesScreen'; -import SettingsScreen from '../screens/Settings/SettingsScreen'; -import SharingScreen from '../screens/Sharing/SharingScreen'; -import TeamScreen from '../screens/Team/TeamScreen'; -import TeamMatchesScreen from '../screens/TeamMatches/TeamMatchesScreen'; -import TeamsScreen from '../screens/Teams/TeamsScreen'; -import { RootStackParamList, RootTabParamList } from '../types'; - -export default function Navigation() { - return ( - - - - ); -} - -const Stack = createNativeStackNavigator(); - -function RootNavigator() { - return ( - - - - - - - - - - ); -} - -const Drawer = createDrawerNavigator(); - -function BottomTabNavigator() { - return ( - - { return () } }} - /> - { return () } }} - /> - { return () } }} - /> - { return () } }} - /> - - ); -} \ No newline at end of file diff --git a/package.json b/package.json index 57acfee..0443ae8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-dom": "16.13.1", "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", "react-native-gesture-handler": "~1.10.2", + "react-native-pager-view": "5.0.12", "react-native-reanimated": "~2.2.0", "react-native-safe-area-context": "3.2.0", "react-native-screens": "~3.4.0", diff --git a/screens/DefaultTeam/DefaultTeamScreen.tsx b/screens/DefaultTeam/DefaultTeamScreen.tsx new file mode 100644 index 0000000..a7c7e6c --- /dev/null +++ b/screens/DefaultTeam/DefaultTeamScreen.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { ScrollView, StyleSheet, View } from "react-native"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import StandardButton from "../../components/common/StandardButton"; +import Subtitle from "../../components/text/Subtitle"; +import Title from "../../components/text/Title"; + +export default function DefaultTeamScreen({ route }: any) { + return ( + + + Default Team + Select the default team to scout + + + + { }} /> + { }} /> + { }} /> + + + + { }} /> + { }} /> + { }} /> + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20, + paddingTop: 20 + } +}); diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index 95d5230..b112b1c 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -40,13 +40,13 @@ export default function MatchScreen({ route }: any) { { }} /> { match ? TBA.openMatch(match.id) : null }} /> diff --git a/screens/Matches/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx index 6e04821..1141e91 100644 --- a/screens/Matches/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { ToastAndroid } from 'react-native'; import BlitzDB from '../../api/BlitzDB'; import ScrollContainer from '../../components/containers/ScrollContainer'; +import NavTitle from '../../components/text/NavTitle'; import Text from '../../components/text/Text'; import MatchBanner from './MatchBanner'; @@ -25,13 +26,14 @@ export default function MatchesScreen() { for (let matchID of BlitzDB.event.matchIDs) matchDisplay.push(); else - matchDisplay.push(This event has no match data posted yet! You can try refreshing by pulling down.); + matchDisplay.push(This event has no matches posted yet!{"\n"}Pull down to refresh data.); else - matchDisplay.push(Match data has not been downloaded from TBA yet. Download is available under settings); + matchDisplay.push(Download match data from the Blue Alliance under the settings tab.); // Return return ( + Matches {matchDisplay} ); diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index 278b930..ddb5163 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -1,4 +1,4 @@ -import { FontAwesome } from '@expo/vector-icons'; +import { MaterialIcons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/core'; import * as React from 'react'; import { Alert, Image, StyleSheet, View } from "react-native"; @@ -34,8 +34,8 @@ export default function TeamPreview(props: TeamThumbnailProps) { else { mediaIcon = ( - @@ -83,6 +83,7 @@ const styles = StyleSheet.create({ alignItems: "center", backgroundColor: "#444", marginRight: 15, + borderRadius: 5 }, teamName: { fontSize: 18, diff --git a/screens/Settings/RegionalModal.tsx b/screens/Settings/RegionalScreen.tsx similarity index 75% rename from screens/Settings/RegionalModal.tsx rename to screens/Settings/RegionalScreen.tsx index d7d4a14..a4b4478 100644 --- a/screens/Settings/RegionalModal.tsx +++ b/screens/Settings/RegionalScreen.tsx @@ -1,3 +1,4 @@ +import { useNavigation } from "@react-navigation/core"; import React from "react"; import { Alert, StyleSheet } from "react-native"; import { TextInput } from "react-native-gesture-handler"; @@ -5,30 +6,21 @@ import BlitzDB from "../../api/BlitzDB"; import { TBAEvent } from "../../api/models/TBAModels"; import TBA from "../../api/TBA"; import Button from "../../components/common/Button"; -import Modal from "../../components/common/Modal"; -import Subtitle from "../../components/text/Subtitle"; +import ScrollContainer from "../../components/containers/ScrollContainer"; import Text from "../../components/text/Text"; -import Title from "../../components/text/Title"; import DownloadingModal from "./DownloadingModal"; -interface ModalProps { - isVisible: boolean; - setVisible: (isVisible: boolean) => void; -} - -export default function RegionalModal(props: ModalProps) { +export default function RegionalScreen({ route }: any) { const [searchTerm, updateSearch] = React.useState(""); const [regionalList, updateRegionals] = React.useState([] as TBAEvent[]); const [downloadStatus, setDownloadStatus] = React.useState(""); - - // Default Behaviour - if (!props.isVisible) - return null; + const navigator = useNavigation(); + const year = route.params.year; // Generate List let regionalsDisplay: JSX.Element[] = []; - if (regionalList.length <= 0 && props.isVisible) { - TBA.getEvents().then((events) => { + if (regionalList.length <= 0) { + TBA.getEvents(year).then((events) => { if (events) updateRegionals(events); }).catch(() => { @@ -48,7 +40,7 @@ export default function RegionalModal(props: ModalProps) { regionalsDisplay.push( - ); - } - } - - // Display Data - - return ( - - Set Year - Select the current season / year: - - {yearsDisplay} - - - ); -} - -const styles = StyleSheet.create({ - regionalButton: { - padding: 8, - marginLeft: 4, - textAlign: "center" - }, - regionalText: { - fontSize: 16 - }, - loadingText: { - textAlign: "center", - fontStyle: "italic" - } -}); diff --git a/screens/Settings/YearScreen.tsx b/screens/Settings/YearScreen.tsx new file mode 100644 index 0000000..b6b6e52 --- /dev/null +++ b/screens/Settings/YearScreen.tsx @@ -0,0 +1,105 @@ +import { useNavigation } from "@react-navigation/core"; +import React from "react"; +import { StyleSheet, ToastAndroid } from "react-native"; +import TBA from "../../api/TBA"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import StandardButton from "../../components/common/StandardButton"; +import ScrollContainer from "../../components/containers/ScrollContainer"; +import Subtitle from "../../components/text/Subtitle"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; + +/* + While hard-coding season names isn't best practice, + The Blue Alliance doesn't provide season names. + While we could use First's API, that would require + managing two seperate API connections for a single name. +*/ +const INIT_YEAR = 1992; +const SEASON_NAMES = [ + "Maize Craze", + "Rug Rage", + "Tower Power", + "Ramp N' Roll", // 1995 + "Hexagon Havoc", + "Toroid Terror", + "Ladder Logic", + "Double Trouble", + "Co-operation FIRST", // 2000 + "Diabolical Dynamics", + "Zone Zeal", + "Shark Attack", + "Frenzy", + "Triple Play", // 2005 + "Aim High", + "Rack N' Roll", + "Overdrive", + "Lunacy", + "Breakaway", // 2010 + "Logo Motion", + "Rebound Rumble", + "Ultimate Ascent", + "Aerial Assist", + "Recycle Rush", // 2015 + "Stronghold", + "Steamworks", + "Power Up", + "Destination: Deep Space", + "Infinite Recharge", // 2020 + "Infinite Recharge II", + "Rapid React" +]; + +export default function YearScreen() { + const [maxYear, setMaxYear] = React.useState(0); + const navigator = useNavigation(); + + // Generate List + let yearsDisplay: JSX.Element[] = []; + if (maxYear <= INIT_YEAR) { + TBA.getServerStatus().then((status) => { + if (status) + setMaxYear(status.max_season); + else + ToastAndroid.show("Failed to connect to TBA", 1000); + }) + + yearsDisplay.push( + + Loading All Seasons... + + ); + } + else { + for (let y = maxYear; y >= INIT_YEAR; y--) { + let year = y; + let index = y - INIT_YEAR; + yearsDisplay.push( + { navigator.navigate("Regional", { year: year }) }} + key={year} /> + ); + } + } + + // Display Data + + return ( + + Years + Select the target year/season + + {yearsDisplay} + + ); +} + +const styles = StyleSheet.create({ + loadingText: { + textAlign: "center", + fontStyle: "italic" + } +}); diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index dee7056..7368901 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; +import NavTitle from '../../components/text/NavTitle'; import ExportQRModal from './ExportQRModal'; import ImportQRModal from './ImportQRModal'; @@ -12,17 +13,19 @@ export default function SharingScreen() { return ( + Sharing + {/* QR Codes */} { setExportQRVisible(true); }} /> { setImportQRVisible(true); }} /> @@ -30,13 +33,13 @@ export default function SharingScreen() { {/* File Formats */} { }} /> { }} /> @@ -61,18 +64,37 @@ export default function SharingScreen() { {/* Hardware Sync */} { }} /> + { }} /> + + + { }} /> { }} /> + + + { }} /> { }} /> diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index b82019e..86b4dc2 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -8,7 +8,6 @@ import TBA from "../../api/TBA"; import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import StandardButton from "../../components/common/StandardButton"; -import PhotoModal from "../../components/containers/PhotoModal"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; @@ -17,7 +16,6 @@ export interface TeamProps { } export default function TeamScreen({ route }: any) { - const [previewIndex, setPreviewIndex] = React.useState(-1); const [version, setVersion] = React.useState(0); const navigator = useNavigation(); const teamID = route.params.teamID; @@ -43,7 +41,7 @@ export default function TeamScreen({ route }: any) { mediaList.push( @@ -92,29 +90,25 @@ export default function TeamScreen({ route }: any) { { }} /> - { navigator.navigate("TeamMatches", { teamID }) }} /> + onPress={() => { navigator.navigate("TeamMatches", { teamID }) }} />*/} { team ? TBA.openTeam(team.number) : null }} /> - - ); @@ -169,7 +163,8 @@ const styles = StyleSheet.create({ }, thumbnail: { height: 200, - width: 200 + width: 200, + borderRadius: 1 }, imageButton: { height: 200, diff --git a/screens/TeamMatches/TeamMatchesScreen.tsx b/screens/TeamMatches/TeamMatchesScreen.tsx index f2ee86e..ac5bc67 100644 --- a/screens/TeamMatches/TeamMatchesScreen.tsx +++ b/screens/TeamMatches/TeamMatchesScreen.tsx @@ -1,16 +1,13 @@ -import { useNavigation } from "@react-navigation/core"; import React from "react"; -import { ScrollView, StyleSheet, View } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; +import { StyleSheet } from "react-native"; import Text from "../../components/text/Text"; -import MatchBanner from "../Matches/MatchBanner"; export interface TeamMatchesProps { teamID: string; } export default function TeamMatchesScreen({ route }: any) { - const navigator = useNavigation(); + /* const teamID = route.params.teamID; // Matches @@ -30,6 +27,9 @@ export default function TeamMatchesScreen({ route }: any) { ); + */ + + return (Depricated); } const styles = StyleSheet.create({ diff --git a/screens/Teams/TeamBanner.tsx b/screens/Teams/TeamBanner.tsx index 98b3f42..9f3b182 100644 --- a/screens/Teams/TeamBanner.tsx +++ b/screens/Teams/TeamBanner.tsx @@ -22,7 +22,7 @@ export default function TeamBanner(props: TeamBannerProps) { 0 ? team.media[0] : undefined} - iconType={team.media.length > 0 ? undefined : "ban"} + iconType={team.media.length > 0 ? undefined : "do-not-disturb"} title={team.name} subtitle={team.number.toString()} onPress={() => { navigator.navigate("Team", { teamID: team.id }) }} /> diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index 95317ca..12f0a54 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { ToastAndroid } from 'react-native'; import BlitzDB from '../../api/BlitzDB'; import ScrollContainer from '../../components/containers/ScrollContainer'; +import NavTitle from '../../components/text/NavTitle'; import Text from '../../components/text/Text'; import TeamBanner from './TeamBanner'; @@ -18,8 +19,6 @@ export default function TeamsScreen() { setVersion(version + 1); }; - - if (BlitzDB.event.isLoaded) { if (BlitzDB.event.teamIDs.length > 0) for (let teamID of BlitzDB.event.teamIDs) @@ -33,6 +32,7 @@ export default function TeamsScreen() { return ( + Teams {teamList} ); diff --git a/types.tsx b/types.tsx index f227834..99c39cf 100644 --- a/types.tsx +++ b/types.tsx @@ -1,25 +1,19 @@ -/** - * Learn more about using TypeScript with React Navigation: - * https://reactnavigation.org/docs/typescript/ - */ - -import { NavigatorScreenParams } from '@react-navigation/native'; -import { MatchProps } from './screens/Match/MatchScreen'; -import { TeamProps } from './screens/Team/TeamScreen'; -import { TeamMatchesProps } from './screens/TeamMatches/TeamMatchesScreen'; - +/* declare global { namespace ReactNavigation { interface RootParamList extends RootStackParamList { } } } +*/ +/* export type RootStackParamList = { Root: NavigatorScreenParams | undefined; Team: TeamProps; Match: MatchProps; TeamMatches: TeamMatchesProps; -}; + DefaultTeam: undefined; +};*/ /* export type RootStackScreenProps = NativeStackScreenProps< @@ -28,12 +22,13 @@ export type RootStackScreenProps = Nati >; */ +/* export type RootTabParamList = { Teams: undefined; Matches: undefined; Sharing: undefined; Settings: undefined; -}; +};*/ /* export type RootTabScreenProps = CompositeScreenProps< diff --git a/yarn.lock b/yarn.lock index f8c6d08..1406ef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7341,6 +7341,11 @@ react-native-iphone-x-helper@^1.3.0: resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== +react-native-pager-view@5.0.12: + version "5.0.12" + resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-5.0.12.tgz#5106735d944e7f876b006377ab6a18859bf7730c" + integrity sha512-QmHUnQeP2qcxDofEOnKRmoUue0RaT55lhNJDfcQ1/SNuxif4Q2UyvDfqeItm1+toaE5tVnXqoreZh82FqUqnvw== + react-native-reanimated@~2.2.0: version "2.2.2" resolved "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.2.2.tgz" From 23bc2d20aca688f4e6bd78a02b77b1353ca7baf8 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Tue, 4 Jan 2022 01:44:32 -0600 Subject: [PATCH 18/38] Scouting Template Editing --- api/BlitzDB.ts | 20 ++++- api/ScoutDB.ts | 3 + api/TBA.ts | 4 +- api/TemplateDB.ts | 84 +++++++++-------- api/models/TemplateModels.ts | 12 ++- app.json | 3 +- components/elements/CheckboxElement.tsx | 85 ++++++++++++++++++ components/elements/CounterElement.tsx | 90 +++++++++++++++++++ components/elements/HRElement.tsx | 20 +++-- components/elements/ScoutingElement.tsx | 38 ++++++-- components/elements/SubtitleElement.tsx | 44 +++++++-- components/elements/TextBoxElement.tsx | 41 +++++++++ components/elements/TextElement.tsx | 44 +++++++-- components/elements/TitleElement.tsx | 44 +++++++-- components/text/NavTitle.tsx | 2 +- navigation/RootNavigator.tsx | 4 +- navigation/RootParamList.tsx | 21 +++++ navigation/TabNavigator.tsx | 2 +- package.json | 2 + screens/Match/MatchScreen.tsx | 5 +- screens/Scout/ScoutingScreen.tsx | 56 ++++++++++++ screens/Settings/RegionalScreen.tsx | 24 +++-- screens/Settings/SettingsScreen.tsx | 6 +- .../Settings/Template/EditTemplateScreen.tsx | 80 +++++++++-------- .../Template/ElementChooserScreen.tsx | 23 ++--- screens/Team/TeamScreen.tsx | 26 ++---- yarn.lock | 10 +++ 27 files changed, 635 insertions(+), 158 deletions(-) create mode 100644 api/ScoutDB.ts create mode 100644 components/elements/CheckboxElement.tsx create mode 100644 components/elements/CounterElement.tsx create mode 100644 components/elements/TextBoxElement.tsx create mode 100644 navigation/RootParamList.tsx create mode 100644 screens/Scout/ScoutingScreen.tsx diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts index 40beda6..a1bf6a0 100644 --- a/api/BlitzDB.ts +++ b/api/BlitzDB.ts @@ -1,14 +1,17 @@ +import EventEmitter from "eventemitter3"; import { Alert } from "react-native"; import EventDB from "./EventDB"; import MatchDB from "./MatchDB"; import Match from "./models/Match"; import TeamDB from "./TeamDB"; import TemplateDB from "./TemplateDB"; + export default class BlitzDB { static teams: TeamDB = new TeamDB(); static matches: MatchDB = new MatchDB(); static event: EventDB = new EventDB(); - static templates: TemplateDB = new TemplateDB(); + static matchTemplate: TemplateDB = new TemplateDB("matchtemplate-data"); + static eventEmitter = new EventEmitter(); /** * Downloads all event data from the Blue Alliance @@ -51,7 +54,7 @@ export default class BlitzDB { await this.teams.save(); await this.matches.save(); await this.event.save(); - await this.templates.save(); + await this.matchTemplate.save(); } /** @@ -91,7 +94,7 @@ export default class BlitzDB { await this.teams.load(); await this.matches.load(); await this.event.load(); - await this.templates.load(); + await this.matchTemplate.load(); } /** @@ -117,4 +120,15 @@ export default class BlitzDB { this.event.deleteAll(); } } + + /** + * Generates a random identifier for objects + * @returns identifying string of alphanumeric characters + */ + static generateID(size = 2) { + let id = ""; + for (let i = 0; i < size; i++) + id += Math.random().toString(36).slice(2); + return id; + } } \ No newline at end of file diff --git a/api/ScoutDB.ts b/api/ScoutDB.ts new file mode 100644 index 0000000..6f333ea --- /dev/null +++ b/api/ScoutDB.ts @@ -0,0 +1,3 @@ +export class ScoutDB { + +} \ No newline at end of file diff --git a/api/TBA.ts b/api/TBA.ts index 6baaa5e..fb4dea6 100644 --- a/api/TBA.ts +++ b/api/TBA.ts @@ -54,8 +54,8 @@ export default class TBA { * Opens a team in a new browser window * @param teamNumber - Number of the team */ - static openTeam(teamNumber: number) { - Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber); + static openTeam(teamNumber: number, year: number) { + Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + year); } /** diff --git a/api/TemplateDB.ts b/api/TemplateDB.ts index f32153e..b742c54 100644 --- a/api/TemplateDB.ts +++ b/api/TemplateDB.ts @@ -1,73 +1,83 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; -import { ElementData, ScoutingTemplate, TemplateType } from "./models/TemplateModels"; - -const SAVE_KEY = "template-data"; -const StringTypes = [ - "Pit", - "Match" -]; +import React, { useEffect } from "react"; +import BlitzDB from "./BlitzDB"; +import { ElementData, ScoutingTemplate } from "./models/TemplateModels"; /** * Handles scouting / pit scouting templates */ export default class TemplateDB { - pitTemplate: ScoutingTemplate = []; - matchTemplate: ScoutingTemplate = []; + template: ScoutingTemplate = []; + saveKey: string; + + constructor(saveKey: string) { + this.saveKey = saveKey; + } /** * Adds an element to a scouting template - * @param templateType - Template to append to * @param element - Element to append */ - addElement(templateType: TemplateType, element: ElementData) { - if (templateType === TemplateType.Match) - this.matchTemplate.push(element); - else - this.pitTemplate.push(element); + addElement(element: ElementData) { + this.template.push(element); + this.save(); } /** - * Gets the cooresponding template to the type - * @param templateType - Type of template - * @returns Template of that type + * Sets all data of an element + * @param element - Element to set */ - getTemplate(templateType: TemplateType) { - if (templateType === TemplateType.Match) - return this.matchTemplate; + setElement(element: ElementData) { + let index = this.template.findIndex(e => e.id === element.id); + if (index >= 0) + this.template[index] = element; else - return this.pitTemplate; + console.error("Could not find element of id:" + element.id) } /** - * Gets a string representation of the template type - * @param templateType - Type of template - * @returns String representation of the type (Ex: "Pit" or "Match") + * Replaces the current template + * @param template - Replacement template */ - getTemplateString(templateType: TemplateType) { - return StringTypes[templateType]; + setTemplate(template: ScoutingTemplate) { + this.template = template; + } + + /** + * Subscribes to a template as a react hook + * @returns React Hook of the template + */ + useTemplate() { + const [version, setVersion] = React.useState(0); + + useEffect(() => { + function onTemplateChange() { + setVersion(v => v + 1); + } + + BlitzDB.eventEmitter.addListener("template", onTemplateChange); + return () => { BlitzDB.eventEmitter.removeListener("template", onTemplateChange); } + }, []); + + return this.template; } /** * Saves all template data */ async save() { - let templateData = { - match: this.matchTemplate, - pit: this.pitTemplate - }; - let jsonTemplates = JSON.stringify(templateData); - await AsyncStorage.setItem(SAVE_KEY, jsonTemplates); + let jsonTemplates = JSON.stringify(this.template); + await AsyncStorage.setItem(this.saveKey, jsonTemplates); + BlitzDB.eventEmitter.emit("template"); } /** * Loads all template data */ async load() { - let jsonTemplates = await AsyncStorage.getItem(SAVE_KEY); + let jsonTemplates = await AsyncStorage.getItem(this.saveKey); if (!jsonTemplates) return; - let templates = JSON.parse(jsonTemplates); - this.pitTemplate = templates.pit; - this.matchTemplate = templates.match; + this.template = JSON.parse(jsonTemplates); } } \ No newline at end of file diff --git a/api/models/TemplateModels.ts b/api/models/TemplateModels.ts index ae88026..3f45d11 100644 --- a/api/models/TemplateModels.ts +++ b/api/models/TemplateModels.ts @@ -7,13 +7,23 @@ export enum ElementType { title, subtitle, text, - hr + hr, + counter, + checkbox, + textbox } export interface ElementData { + id: string; type: ElementType; label: string; options: any; } +export interface ElementProps { + data: ElementData; + isEditable: boolean; + onChange?: (value: any) => void +} + export type ScoutingTemplate = ElementData[]; \ No newline at end of file diff --git a/app.json b/app.json index 3c8a2f8..25da716 100644 --- a/app.json +++ b/app.json @@ -7,6 +7,7 @@ "icon": "./assets/images/icon.png", "scheme": "blitz", "userInterfaceStyle": "light", + "primaryColor": "#856a00", "splash": { "image": "./assets/images/splash.png", "resizeMode": "contain", @@ -28,8 +29,6 @@ "foregroundImage": "./assets/images/icon.png", "backgroundColor": "#1e1e1e" }, - "primaryColor": "#856a00", - "color": "#856a00", "package": "com.nbblitz.blitzscouter", "versionCode": 1 }, diff --git a/components/elements/CheckboxElement.tsx b/components/elements/CheckboxElement.tsx new file mode 100644 index 0000000..462253f --- /dev/null +++ b/components/elements/CheckboxElement.tsx @@ -0,0 +1,85 @@ +import Checkbox from 'expo-checkbox'; +import React from "react"; +import { StyleSheet, TextInput, Vibration, View } from "react-native"; +import BlitzDB from '../../api/BlitzDB'; +import { ElementProps } from "../../api/models/TemplateModels"; +import Text from '../text/Text'; + +export default function CheckboxElement(props: ElementProps) { + let elementData = props.data; + const defaultValue = elementData.options.defaultValue; + const [isChecked, setChecked] = React.useState(defaultValue ? defaultValue as boolean : false); + + const changeChecked = (isChecked: boolean) => { + setChecked(isChecked); + + // Edit + if (props.isEditable) { + elementData.options.defaultValue = isChecked; + BlitzDB.matchTemplate.setElement(elementData); + } + + // Vibrate + if (isChecked) + Vibration.vibrate(10); + else + Vibration.vibrate(100); + + // Callback + if (props.onChange) + props.onChange(isChecked); + } + + const changeText = (text: string) => { + elementData.label = text; + BlitzDB.matchTemplate.setElement(elementData); + }; + + return ( + + + {props.isEditable ? + + : + {elementData.label} + } + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + marginTop: 15, + marginLeft: 15 + }, + textInput: { + color: "#fff", + backgroundColor: "#222222", + borderRadius: 10, + padding: 5, + marginLeft: 10, + fontSize: 20 + }, + title: { + textAlign: "left", + fontWeight: "bold", + marginTop: 2, + marginLeft: 20, + fontSize: 20 + }, + checkbox: { + marginLeft: 15, + marginTop: 2, + transform: [{ scaleX: 2 }, { scaleY: 2 }] + } +}); diff --git a/components/elements/CounterElement.tsx b/components/elements/CounterElement.tsx new file mode 100644 index 0000000..cddeefc --- /dev/null +++ b/components/elements/CounterElement.tsx @@ -0,0 +1,90 @@ +import { MaterialIcons } from "@expo/vector-icons"; +import React from "react"; +import { StyleSheet, Vibration, View } from "react-native"; +import BlitzDB from "../../api/BlitzDB"; +import { ElementProps } from "../../api/models/TemplateModels"; +import Button from "../common/Button"; +import Title from "../text/Title"; + +export default function CounterElement(props: ElementProps) { + let elementData = props.data; + const defaultValue = elementData.options.defaultValue; + const [value, setValue] = React.useState(defaultValue ? defaultValue as number : 0); + + // Value State Change + const changeValue = (delta: number) => { + // Update + let newValue = value; + if (value + delta >= 0) { + newValue += delta; + setValue(newValue); + } + + // Edit + if (props.isEditable) { + elementData.options.defaultValue = newValue; + BlitzDB.matchTemplate.setElement(elementData); + } + + // Vibrate + if (delta > 0) + Vibration.vibrate(10); + else + Vibration.vibrate(100); + + // Callback + if (props.onChange) + props.onChange(newValue); + } + + return ( + + {value} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + justifyContent: "space-evenly" + }, + textInput: { + color: "#fff", + backgroundColor: "#222222", + borderRadius: 10, + padding: 5, + margin: 5, + fontSize: 15 + }, + title: { + width: 50, + textAlign: "center" + }, + button: { + height: 50, + width: 100, + borderRadius: 5, + paddingLeft: 35, + margin: 5, + + flexDirection: "row", + justifyContent: "flex-start", + alignItems: "center", + alignSelf: 'stretch', + } +}); diff --git a/components/elements/HRElement.tsx b/components/elements/HRElement.tsx index 5412800..df2b28f 100644 --- a/components/elements/HRElement.tsx +++ b/components/elements/HRElement.tsx @@ -1,9 +1,19 @@ import React from "react"; -import { ElementData } from "../../api/models/TemplateModels"; +import { ElementProps } from "../../api/models/TemplateModels"; import HorizontalBar from "../common/HorizontalBar"; -export default function HRElement(props: { data: ElementData }) { - return ( - - ); +export default function HRElement(props: ElementProps) { + if (props.isEditable) { + return ( + + ); + } + else { + return ( + + ); + } } \ No newline at end of file diff --git a/components/elements/ScoutingElement.tsx b/components/elements/ScoutingElement.tsx index 6450a1f..25a25b0 100644 --- a/components/elements/ScoutingElement.tsx +++ b/components/elements/ScoutingElement.tsx @@ -1,19 +1,43 @@ import React from "react"; -import { ElementData, ElementType } from "../../api/models/TemplateModels"; +import { StyleSheet } from "react-native"; +import { ElementProps, ElementType } from "../../api/models/TemplateModels"; +import CheckboxElement from "./CheckboxElement"; +import CounterElement from "./CounterElement"; import HRElement from "./HRElement"; import SubtitleElement from "./SubtitleElement"; +import TextBoxElement from "./TextBoxElement"; import TextElement from "./TextElement"; import TitleElement from "./TitleElement"; -export default function ScoutingElement(props: { data: ElementData }) { +export default function ScoutingElement(props: ElementProps) { + let element: JSX.Element | undefined; switch (props.data.type) { case ElementType.title: - return (); + element = (); + break; case ElementType.subtitle: - return (); + element = (); + break; case ElementType.text: - return (); + element = (); + break; case ElementType.hr: - return (); + element = (); + break; + case ElementType.counter: + element = (); + break; + case ElementType.checkbox: + element = (); + break; + case ElementType.textbox: + element = (); + break; } -} \ No newline at end of file + return element; +} + + +const styles = StyleSheet.create({ + +}); diff --git a/components/elements/SubtitleElement.tsx b/components/elements/SubtitleElement.tsx index d8fb24b..e9b9225 100644 --- a/components/elements/SubtitleElement.tsx +++ b/components/elements/SubtitleElement.tsx @@ -1,9 +1,41 @@ import React from "react"; -import { ElementData } from "../../api/models/TemplateModels"; +import { StyleSheet, TextInput } from "react-native"; +import BlitzDB from "../../api/BlitzDB"; +import { ElementProps } from "../../api/models/TemplateModels"; import Subtitle from "../text/Subtitle"; -export default function SubtitleElement(props: { data: ElementData }) { - return ( - {props.data.label} - ); -} \ No newline at end of file +export default function SubtitleElement(props: ElementProps) { + let elementData = props.data; + if (props.isEditable) { + const onEdit = (text: string) => { + elementData.label = text; + BlitzDB.matchTemplate.setElement(elementData); + } + + return ( + + ); + } + else { + return ( + {elementData.label} + ); + } +} + +const styles = StyleSheet.create({ + textInput: { + color: "#bbb", + backgroundColor: "#222222", + borderRadius: 10, + padding: 5, + margin: 5, + fontSize: 15, + fontWeight: "bold" + } +}); diff --git a/components/elements/TextBoxElement.tsx b/components/elements/TextBoxElement.tsx new file mode 100644 index 0000000..ec0d803 --- /dev/null +++ b/components/elements/TextBoxElement.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { StyleSheet, TextInput } from "react-native"; +import BlitzDB from "../../api/BlitzDB"; +import { ElementProps } from "../../api/models/TemplateModels"; + +export default function TextBoxElement(props: ElementProps) { + let elementData = props.data; + + const onEdit = (text: string) => { + elementData.label = text; + BlitzDB.matchTemplate.setElement(elementData); + }; + + const onScoutingEdit = (text: string) => { + if (props.onChange) + props.onChange(text); + } + + return ( + + ); +} + +const styles = StyleSheet.create({ + textInput: { + color: "#fff", + backgroundColor: "#222222", + borderRadius: 10, + padding: 10, + margin: 5, + fontSize: 15, + height: 100, + textAlignVertical: "top" + } +}); diff --git a/components/elements/TextElement.tsx b/components/elements/TextElement.tsx index 3b6378c..2912e30 100644 --- a/components/elements/TextElement.tsx +++ b/components/elements/TextElement.tsx @@ -1,9 +1,41 @@ import React from "react"; -import { ElementData } from "../../api/models/TemplateModels"; +import { StyleSheet, TextInput } from "react-native"; +import BlitzDB from "../../api/BlitzDB"; +import { ElementProps } from "../../api/models/TemplateModels"; import Text from "../text/Text"; -export default function TextElement(props: { data: ElementData }) { - return ( - {props.data.label} - ); -} \ No newline at end of file +export default function TextElement(props: ElementProps) { + let elementData = props.data; + if (props.isEditable) { + const onEdit = (text: string) => { + elementData.label = text; + BlitzDB.matchTemplate.setElement(elementData); + } + + return ( + + ); + } + else { + return ( + {elementData.label} + ); + } +} + +const styles = StyleSheet.create({ + textInput: { + color: "#fff", + backgroundColor: "#222222", + borderRadius: 10, + padding: 5, + margin: 5, + fontSize: 15 + } +}); diff --git a/components/elements/TitleElement.tsx b/components/elements/TitleElement.tsx index 047cf95..bfc2c62 100644 --- a/components/elements/TitleElement.tsx +++ b/components/elements/TitleElement.tsx @@ -1,9 +1,41 @@ import React from "react"; -import { ElementData } from "../../api/models/TemplateModels"; +import { StyleSheet, TextInput } from "react-native"; +import BlitzDB from "../../api/BlitzDB"; +import { ElementProps } from "../../api/models/TemplateModels"; import Title from "../text/Title"; -export default function TitleElement(props: { data: ElementData }) { - return ( - {props.data.label} - ); -} \ No newline at end of file +export default function TitleElement(props: ElementProps) { + let elementData = props.data; + if (props.isEditable) { + const onEdit = (text: string) => { + elementData.label = text; + BlitzDB.matchTemplate.setElement(elementData); + } + + return ( + + ); + } + else { + return ( + {elementData.label} + ); + } +} + +const styles = StyleSheet.create({ + textInput: { + color: "#fff", + backgroundColor: "#222222", + borderRadius: 10, + padding: 5, + margin: 5, + fontSize: 30, + fontWeight: "bold" + } +}); diff --git a/components/text/NavTitle.tsx b/components/text/NavTitle.tsx index fec0ae8..cb534b0 100644 --- a/components/text/NavTitle.tsx +++ b/components/text/NavTitle.tsx @@ -11,6 +11,6 @@ export default function NavTitle(props: TextProps) { fontSize: 30, fontWeight: 'bold', marginTop: 60, - marginBottom: 10 + marginBottom: 15 }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx index b03244e..a917ceb 100644 --- a/navigation/RootNavigator.tsx +++ b/navigation/RootNavigator.tsx @@ -3,6 +3,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; import * as React from 'react'; import MediaScreen from "../components/containers/MediaScreen"; import MatchScreen from "../screens/Match/MatchScreen"; +import ScoutingScreen from "../screens/Scout/ScoutingScreen"; import RegionalScreen from "../screens/Settings/RegionalScreen"; import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; @@ -27,9 +28,10 @@ export default function RootNavigator() { + - + diff --git a/navigation/RootParamList.tsx b/navigation/RootParamList.tsx new file mode 100644 index 0000000..b7ed3ca --- /dev/null +++ b/navigation/RootParamList.tsx @@ -0,0 +1,21 @@ +import { TemplateType } from "../api/models/TemplateModels"; + +// Params +export type RootNavParamList = { + Drawer: undefined, + Match: { matchID: string }, + Team: { teamID: string }, + Media: { teamID: string, imageIndex: number }, + Scout: { templateType: TemplateType } + Year: undefined, + Regional: { year: number }, + EditTemplate: { templateType: TemplateType }, + ElementChooser: { templateType: TemplateType } +}; + +// Default +declare global { + namespace ReactNavigation { + interface RootParamList extends RootNavParamList { } + } +} \ No newline at end of file diff --git a/navigation/TabNavigator.tsx b/navigation/TabNavigator.tsx index 7ec5530..699d874 100644 --- a/navigation/TabNavigator.tsx +++ b/navigation/TabNavigator.tsx @@ -50,7 +50,7 @@ export default function TabNavigator() { { return () } }} + options={{ tabBarIcon: (props) => { return () } }} /> { }} /> + onPress={() => { navigator.navigate("Scout", { templateType: TemplateType.Match }); }} /> ) + + return ( + + + {elementList} + + + + + + + ); +} + +const styles = StyleSheet.create({ + parentView: { + height: "100%", + width: "100%" + }, + submitButton: { + height: 40, + borderRadius: 5, + padding: 10, + margin: 5, + marginTop: 0, + + backgroundColor: "#c89f00" + }, + buttonText: { + color: "#000000", + fontWeight: "bold" + } +}); \ No newline at end of file diff --git a/screens/Settings/RegionalScreen.tsx b/screens/Settings/RegionalScreen.tsx index a4b4478..725e74e 100644 --- a/screens/Settings/RegionalScreen.tsx +++ b/screens/Settings/RegionalScreen.tsx @@ -1,6 +1,6 @@ import { useNavigation } from "@react-navigation/core"; import React from "react"; -import { Alert, StyleSheet } from "react-native"; +import { Alert, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; import BlitzDB from "../../api/BlitzDB"; import { TBAEvent } from "../../api/models/TBAModels"; @@ -43,7 +43,7 @@ export default function RegionalScreen({ route }: any) { onPress={() => { BlitzDB.event.setYear(year); BlitzDB.downloadEvent(key, setDownloadStatus).then(() => { navigator.goBack(); navigator.goBack(); }); }} style={styles.regionalButton}> - + {regional.name} @@ -55,21 +55,29 @@ export default function RegionalScreen({ route }: any) { // Display Data return ( - + + { updateSearch(text.toLowerCase()) }} /> - {regionalsDisplay} - - + + {regionalsDisplay} + + + + ); } const styles = StyleSheet.create({ + container: { + width: "100%", + height: "100%" + }, regionalButton: { padding: 8, marginLeft: 4, @@ -84,7 +92,9 @@ const styles = StyleSheet.create({ borderRadius: 10, padding: 10, marginBottom: 10, - marginTop: 10 + marginTop: 10, + marginLeft: 20, + marginRight: 20 }, loadingText: { textAlign: "center", diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index ac9f009..0ec6621 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -44,16 +44,18 @@ export default function SettingsScreen() { {/* Scouting Buttons */} + {/* { navigator.navigate("EditTemplate", { type: TemplateType.Pit }); }} /> + onPress={() => { navigator.navigate("EditTemplate", { templateType: TemplateType.Pit }); }} /> + */} { navigator.navigate("EditTemplate", { type: TemplateType.Match }); }} /> + onPress={() => { navigator.navigate("EditTemplate", { templateType: TemplateType.Match }); }} /> { + Alert.alert("Are you sure?", "This will wipe this entire template from your device. Are you sure you want to continue?", [{ + text: "Confirm", + onPress: () => { + BlitzDB.matchTemplate.setTemplate([]); + } + }, + { text: "Cancel", style: "cancel" }], { cancelable: true }); + }; + + // Delete Button + React.useLayoutEffect(() => { + navigator.setOptions({ + headerRight: () => ( + + ) + }); + }); + + // Save on Exit + React.useEffect(() => { + navigator.addListener("beforeRemove", (e) => { + BlitzDB.matchTemplate.save(); + }); + }); + // Element Preview - const stringType = BlitzDB.templates.getTemplateString(templateType); - const template: ElementData[] = BlitzDB.templates.getTemplate(templateType); let elementList: JSX.Element[] = []; if (template.length > 0) { - for (let elementData of template) { - elementList.push( - - ); - } + elementList = template.map(element => ) } else { - elementList.push(There are no elements yet. Add an element to scout below.); - } - - // Clear Behaviour - const clearTemplate = () => { - Alert.alert("Are you sure?", "This will delete all elements in this template. Are you sure you want to continue?", - [ - { - text: "Confirm", - onPress: () => { - //BlitzDB.templates[props.type] = []; - setVersion(version + 1); - } - }, - { text: "Cancel", style: "cancel" } - ], { cancelable: true } - ); + elementList = [There are no elements yet. Add an element to scout below.]; } return ( - Edit Template - {stringType} Scouting - + Match Scouting {elementList} + - + ); } diff --git a/screens/Settings/Template/ElementChooserScreen.tsx b/screens/Settings/Template/ElementChooserScreen.tsx index 81d7182..50e9966 100644 --- a/screens/Settings/Template/ElementChooserScreen.tsx +++ b/screens/Settings/Template/ElementChooserScreen.tsx @@ -10,12 +10,13 @@ import Title from '../../../components/text/Title'; export default function ElementChooserScreen({ route }: any) { const navigator = useNavigation(); - const templateType = route.params.type; + const templateType = route.params.templateType; const chooseElement = (elementType: ElementType) => { - BlitzDB.templates.addElement(templateType, { + BlitzDB.matchTemplate.addElement({ + id: BlitzDB.generateID(), type: elementType, - label: "Default", + label: "", options: {} }); navigator.goBack(); @@ -31,25 +32,19 @@ export default function ElementChooserScreen({ route }: any) { iconType={"import-export"} title={"Counter"} subtitle={"Increment and decrement a number"} - onPress={() => { }} /> + onPress={() => { chooseElement(ElementType.counter); }} /> { }} /> + onPress={() => { chooseElement(ElementType.checkbox); }} /> { }} /> - - { }} /> + onPress={() => { chooseElement(ElementType.textbox); }} /> @@ -62,7 +57,7 @@ export default function ElementChooserScreen({ route }: any) { { chooseElement(ElementType.subtitle); }} /> { chooseElement(ElementType.text); }} /> { chooseElement(ElementType.hr); }} /> diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 86b4dc2..74cd851 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -1,4 +1,4 @@ -import { FontAwesome } from "@expo/vector-icons"; +import { MaterialIcons } from "@expo/vector-icons"; import { useNavigation } from "@react-navigation/core"; import * as ImagePicker from 'expo-image-picker'; import React from "react"; @@ -58,28 +58,18 @@ export default function TeamScreen({ route }: any) { @@ -106,7 +96,7 @@ export default function TeamScreen({ route }: any) { iconType={"open-in-browser"} title={"View on TBA"} subtitle={"View Team " + team.number + " on The Blue Alliance"} - onPress={() => { team ? TBA.openTeam(team.number) : null }} /> + onPress={() => { team ? TBA.openTeam(team.number, BlitzDB.event.year) : null }} /> diff --git a/yarn.lock b/yarn.lock index 1406ef0..c802a56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3926,6 +3926,11 @@ eventemitter3@^3.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + exec-sh@^0.3.2: version "0.3.6" resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz" @@ -4011,6 +4016,11 @@ expo-asset@~8.3.2, expo-asset@~8.3.3: path-browserify "^1.0.0" url-parse "^1.4.4" +expo-checkbox@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/expo-checkbox/-/expo-checkbox-1.0.3.tgz#1d7510947814b19fc710fb7fc80a2433d98e3cc3" + integrity sha512-7w8H1yt7V1pdnbIAIoV3RvI+Rjzc8nlxy4eKdEtZhYupenLmBY173glAx+/eJcW/vlCqKYHxedQmRTqEUpAQ7A== + expo-constants@~11.0.1, expo-constants@~11.0.2: version "11.0.2" resolved "https://registry.npmjs.org/expo-constants/-/expo-constants-11.0.2.tgz" From 862d5013d8b52e0b9472489d5923842179cf2900 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Fri, 7 Jan 2022 05:26:05 -0600 Subject: [PATCH 19/38] AsyncStorage DB Rewrite & Offline Media --- api/BlitzDB.ts | 134 ------------------ api/EventDB.ts | 90 ------------- api/MatchDB.ts | 135 ------------------- api/ScoutDB.ts | 3 - api/TBA.ts | 8 +- api/TBAAdapter.ts | 157 ++++++++++++++++++++++ api/TeamDB.ts | 140 ------------------- api/TemplateDB.ts | 83 ------------ api/models/Match.ts | 13 -- api/models/Team.ts | 10 -- api/models/TemplateModels.ts | 29 ---- babel.config.js | 12 +- components/containers/MediaScreen.tsx | 60 +++------ hooks/useCachedResources.ts | 4 - hooks/useEvent.ts | 27 ++++ hooks/useMatch.ts | 23 ++++ hooks/useStorage.ts | 74 ++++++++++ hooks/useTeam.ts | 19 +++ navigation/RootNavigator.tsx | 11 +- navigation/RootParamList.tsx | 9 +- package.json | 1 + screens/Match/MatchScreen.tsx | 33 +---- screens/Matches/MatchBanner.tsx | 33 +---- screens/Matches/MatchesScreen.tsx | 41 +++--- screens/Matches/TeamPreview.tsx | 26 +--- screens/Scout/ScoutingScreen.tsx | 1 - screens/Settings/RegionalScreen.tsx | 33 +++-- screens/Settings/SettingsScreen.tsx | 18 +-- screens/Settings/YearScreen.tsx | 17 +-- screens/Sharing/ExportQRModal.tsx | 3 +- screens/Sharing/ImportQRModal.tsx | 10 -- screens/Sharing/SharingScreen.tsx | 5 +- screens/Team/TeamScreen.tsx | 153 ++++++++++----------- screens/TeamMatches/TeamMatchesScreen.tsx | 40 ------ screens/Teams/TeamBanner.tsx | 21 +-- screens/Teams/TeamsScreen.tsx | 40 +++--- types/DBTypes.ts | 29 ++++ {api/models => types}/TBAModels.ts | 0 38 files changed, 541 insertions(+), 1004 deletions(-) delete mode 100644 api/BlitzDB.ts delete mode 100644 api/EventDB.ts delete mode 100644 api/MatchDB.ts delete mode 100644 api/ScoutDB.ts create mode 100644 api/TBAAdapter.ts delete mode 100644 api/TeamDB.ts delete mode 100644 api/TemplateDB.ts delete mode 100644 api/models/Match.ts delete mode 100644 api/models/Team.ts delete mode 100644 api/models/TemplateModels.ts create mode 100644 hooks/useEvent.ts create mode 100644 hooks/useMatch.ts create mode 100644 hooks/useStorage.ts create mode 100644 hooks/useTeam.ts delete mode 100644 screens/Sharing/ImportQRModal.tsx delete mode 100644 screens/TeamMatches/TeamMatchesScreen.tsx create mode 100644 types/DBTypes.ts rename {api/models => types}/TBAModels.ts (100%) diff --git a/api/BlitzDB.ts b/api/BlitzDB.ts deleted file mode 100644 index a1bf6a0..0000000 --- a/api/BlitzDB.ts +++ /dev/null @@ -1,134 +0,0 @@ -import EventEmitter from "eventemitter3"; -import { Alert } from "react-native"; -import EventDB from "./EventDB"; -import MatchDB from "./MatchDB"; -import Match from "./models/Match"; -import TeamDB from "./TeamDB"; -import TemplateDB from "./TemplateDB"; - -export default class BlitzDB { - static teams: TeamDB = new TeamDB(); - static matches: MatchDB = new MatchDB(); - static event: EventDB = new EventDB(); - static matchTemplate: TemplateDB = new TemplateDB("matchtemplate-data"); - static eventEmitter = new EventEmitter(); - - /** - * Downloads all event data from the Blue Alliance - * @param eventID - ID of event to download - * @param setDownloadStatus - Sets the download status of the event - * @returns - */ - static async downloadEvent(eventID: string, setDownloadStatus: (status: string) => void) { - // Teams - setDownloadStatus("Downloading Teams..."); - let teamSuccess = await this.teams.downloadEvent(eventID, (teamNumber) => { - setDownloadStatus("Downloading Team " + teamNumber + "..."); - }); - if (!teamSuccess) - return setDownloadStatus(""); - - // Matches - let matchSuccess = await this.matches.downloadEvent(eventID, (matchNumber) => { - setDownloadStatus("Downloading Match " + matchNumber + "..."); - }); - if (!matchSuccess) - return setDownloadStatus(""); - - // Event - this.event.setLoaded(true, eventID); - - // Save - setDownloadStatus("Saving..."); - await this.saveAll(); - - // Exit - setDownloadStatus(""); - Alert.alert("Success", "Successfully downloaded data from The Blue Alliance."); - } - - /** - * Saves all DB data to storage - */ - static async saveAll() { - await this.teams.save(); - await this.matches.save(); - await this.event.save(); - await this.matchTemplate.save(); - } - - /** - * Gets all of the matches from a team - * @param teamID - ID of team to get matches from - * @returns an array of matches - */ - static getTeamMatches(teamID: string): Match[] { - let matchList: Match[] = []; - for (let match of this.matches.matchCache) - if (match.blueTeamIDs.includes(teamID) || match.redTeamIDs.includes(teamID)) - matchList.push(match); - return matchList; - } - - /** - * Exports all comments in a compressed format - * @returns compressed string of comments - */ - static exportComments(): string { - let data = ""; - for (let team of this.teams.teamCache) { - if (team.comments.length > 0) - data += ";;" + team.id; - for (let comment of team.comments) { - data += "::" + comment; - } - } - - return data; - } - - /** - * Loads all DB data from storage - */ - static async loadAll() { - await this.teams.load(); - await this.matches.load(); - await this.event.load(); - await this.matchTemplate.load(); - } - - /** - * Deletes all data from the database - * @param alert - Whether or not to alert the user - */ - static async deleteAll(alert: boolean) { - - if (alert) { - Alert.alert("Are you sure?", "This will delete all scouting data from your device. Are you sure you want to continue?", [{ - text: "Confirm", - onPress: () => { - BlitzDB.deleteAll(false).then(() => { - Alert.alert("Done", "All data has been cleared"); - }); - } - }, - { text: "Cancel", style: "cancel" }], { cancelable: true }); - } - else { - this.teams.deleteAll(); - this.matches.deleteAll(); - this.event.deleteAll(); - } - } - - /** - * Generates a random identifier for objects - * @returns identifying string of alphanumeric characters - */ - static generateID(size = 2) { - let id = ""; - for (let i = 0; i < size; i++) - id += Math.random().toString(36).slice(2); - return id; - } -} \ No newline at end of file diff --git a/api/EventDB.ts b/api/EventDB.ts deleted file mode 100644 index ffbfb9f..0000000 --- a/api/EventDB.ts +++ /dev/null @@ -1,90 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; - -const SAVE_KEY = "event-data"; - -/** - * Handles the currently loaded event - */ -export default class EventDB { - isLoaded: boolean = false; - id: string = ""; - year: number = 0; - matchIDs: string[] = []; - teamIDs: string[] = []; - - /** - * Updates the list of matches at an event - * @param matchIDs - ID of each match in an event - */ - setMatches(matchIDs: string[]) { - this.matchIDs = matchIDs; - } - - /** - * Updates the list of teams at an event - * @param teamIDs - ID of each team at an event - */ - setTeams(teamIDs: string[]) { - this.teamIDs = teamIDs; - } - - /** - * Sets the current event year - * @param year - Year to set to - */ - setYear(year: number) { - this.year = year; - } - - /** - * Sets whether or not the event is loaded - * @param isLoaded - True if the event is loaded - * @param eventID - ID of the event - */ - setLoaded(isLoaded: boolean, eventID?: string) { - this.isLoaded = isLoaded; - if (eventID) - this.id = eventID; - } - - /** - * Deletes all event data - */ - async deleteAll() { - this.isLoaded = false; - this.id = ""; - this.year = 0; - this.matchIDs = []; - this.teamIDs = []; - AsyncStorage.removeItem(SAVE_KEY); - } - - /** - * Saves all event data - */ - async save() { - let eventData = { - matchIDs: this.matchIDs, - teamIDs: this.teamIDs, - year: this.year, - id: this.id - }; - let jsonEvent = JSON.stringify(eventData); - await AsyncStorage.setItem(SAVE_KEY, jsonEvent); - } - - /** - * Loads all event data - */ - async load() { - let jsonEvent = await AsyncStorage.getItem(SAVE_KEY); - if (!jsonEvent) - return; - let event = JSON.parse(jsonEvent); - this.matchIDs = event.matchIDs as string[]; - this.teamIDs = event.teamIDs as string[]; - this.year = event.year as number; - this.id = event.id as string; - this.isLoaded = true; - } -} \ No newline at end of file diff --git a/api/MatchDB.ts b/api/MatchDB.ts deleted file mode 100644 index 96eb908..0000000 --- a/api/MatchDB.ts +++ /dev/null @@ -1,135 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import BlitzDB from "./BlitzDB"; -import Match from "./models/Match"; -import { TBAMatch } from "./models/TBAModels"; -import TBA from "./TBA"; - -const SAVE_KEY = "match-data"; -const MATCH_TYPES = ["qm", "qf", "sf", "f"]; - -/** - * Represents a database of event matches - */ -export default class MatchDB { - matchCache: Match[] = []; - - /** - * Downloads all matches at an event - * @param eventID - ID of the event - */ - async downloadEvent(eventID: string, matchCallback: (matchNumber: number) => void): Promise { - - // Download - let tbaMatches = await TBA.getMatches(eventID); - if (!tbaMatches) - return false; - - // Sort - tbaMatches.sort((a, b) => - (a.match_number + MATCH_TYPES.indexOf(a.comp_level) * 1000) - - (b.match_number + MATCH_TYPES.indexOf(b.comp_level) * 1000) - ); - - // Add - let matchIDs: string[] = []; - for (let match of tbaMatches) { - matchCallback(match.match_number); - matchIDs.push(match.key); - let existingMatch = this.get(match.key); - if (!existingMatch) { - this.matchCache.push({ - id: match.key, - name: this._generateName(match), - description: this._generateDesc(match), - number: match.match_number, - compLevel: match.comp_level, - blueTeamIDs: match.alliances.blue.team_keys, - redTeamIDs: match.alliances.red.team_keys, - comment: "" - }); - } - } - BlitzDB.event.setMatches(matchIDs); - - return true; - } - - /** - * Gets the a match by its id - * @param matchID - The ID of the match - */ - get(matchID: string): Match | undefined { - let match = this.matchCache.find(match => match.id === matchID); - return match; - } - - /** - * Generates a name for a match - * (Ex: "Qualifications 12") - * @param match - Match to generate name - */ - _generateName(match: TBAMatch): string { - let matchName = match.comp_level + "-" + match.match_number; - switch (match.comp_level) { - case "qm": - matchName = "Qualification " + match.match_number; - break; - case "qf": - matchName = "Quarter-Finals " + match.match_number; - break; - case "sf": - matchName = "Semi-Finals " + match.match_number; - break; - case "f": - matchName = "Finals " + match.match_number; - break; - } - return matchName; - } - - /** - * Generates a description for a match - * (Ex: "5141 5142 5143 - 1231 1232 1233") - * @param match - Match to generate name - */ - _generateDesc(match: TBAMatch): string { - let matchDesc = ""; - match.alliances.blue.team_keys.forEach(teamKey => { - let teamNumber = parseInt(teamKey.substring(3)); - matchDesc += teamNumber + " "; - }); - matchDesc += " - " - match.alliances.red.team_keys.forEach(teamKey => { - let teamNumber = parseInt(teamKey.substring(3)); - matchDesc += teamNumber + " "; - }); - return matchDesc; - } - - /** - * Deletes all matches from the database - */ - async deleteAll() { - this.matchCache = []; - await AsyncStorage.removeItem(SAVE_KEY); - } - - /** - * Saves data from the cache to the database - */ - async save() { - let jsonMatches = JSON.stringify(this.matchCache); - await AsyncStorage.setItem(SAVE_KEY, jsonMatches); - } - - /** - * Loads the match cache - */ - async load() { - let jsonMatches = await AsyncStorage.getItem(SAVE_KEY); - if (!jsonMatches) - return; - let matches = JSON.parse(jsonMatches); - this.matchCache = matches; - } -} \ No newline at end of file diff --git a/api/ScoutDB.ts b/api/ScoutDB.ts deleted file mode 100644 index 6f333ea..0000000 --- a/api/ScoutDB.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class ScoutDB { - -} \ No newline at end of file diff --git a/api/TBA.ts b/api/TBA.ts index fb4dea6..b9aadc0 100644 --- a/api/TBA.ts +++ b/api/TBA.ts @@ -1,5 +1,5 @@ import { Linking } from 'react-native'; -import { TBAEvent, TBAMatch, TBAMedia, TBAStatus, TBATeam } from './models/TBAModels'; +import { TBAEvent, TBAMatch, TBAMedia, TBAStatus, TBATeam } from '../types/TBAModels'; const API_KEY = "i90dAcKHXvQ9havypHJKeGY8O1tfymFpaW1Po3RGYpvoMTRVwtiUsUFaLmstCDp3"; const URL_PREFIX = "https://www.thebluealliance.com/api/v3/"; const URL_SUFFIX = "?X-TBA-Auth-Key=" + API_KEY; @@ -54,8 +54,8 @@ export default class TBA { * Opens a team in a new browser window * @param teamNumber - Number of the team */ - static openTeam(teamNumber: number, year: number) { - Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + year); + static openTeam(teamNumber: number, year?: number) { + Linking.openURL("https://www.thebluealliance.com/team/" + teamNumber + "/" + (year ? year : "")); } /** @@ -110,6 +110,6 @@ export default class TBA { }, 5000); }) - return Promise.race>([fetchPromise, timeoutPromise]); + return Promise.race([fetchPromise, timeoutPromise]); } } \ No newline at end of file diff --git a/api/TBAAdapter.ts b/api/TBAAdapter.ts new file mode 100644 index 0000000..25b8cec --- /dev/null +++ b/api/TBAAdapter.ts @@ -0,0 +1,157 @@ +import * as FileSystem from 'expo-file-system'; +import { Alert } from "react-native"; +import { putStorage } from "../hooks/useStorage"; +import { Event, Match, Team } from "../types/DBTypes"; +import { TBAMatch } from "../types/TBAModels"; +import TBA from "./TBA"; + +const MATCH_TYPES = ["qm", "qf", "sf", "f"]; +const BASE64_PREFIX = "data:image/png;base64, "; + +export async function DownloadEvent(eventID: string, callback: (status: string) => void) { + + // Matches + const matchIDs = await DownloadMatches(eventID, matchNumber => { + callback("Downloading Match " + matchNumber + "..."); + }); + if (matchIDs === undefined) + return callback(""); + + // Teams + const teamIDs = await DownloadTeams(eventID, teamNumber => { + callback("Downloading Team " + teamNumber + "..."); + }); + if (teamIDs === undefined) + return callback(""); + + // Event + putStorage("current-event", { + id: eventID, + year: parseInt(eventID.substring(0, 4)), + matchIDs, + teamIDs, + }) + Alert.alert("Success", "Successfully downloaded data from The Blue Alliance."); +} + +export async function DownloadMatches(eventID: string, callback: (matchNumber: number) => void) { + const tbaMatches = await TBA.getMatches(eventID); + if (!tbaMatches) + return undefined; + + // Match Name + const generateMatchName = (match: TBAMatch) => { + let matchName = match.comp_level + "-" + match.match_number; + switch (match.comp_level) { + case "qm": + matchName = "Qualification " + match.match_number; + break; + case "qf": + matchName = "Quarter-Finals " + match.match_number; + break; + case "sf": + matchName = "Semi-Finals " + match.match_number; + break; + case "f": + matchName = "Finals " + match.match_number; + break; + } + return matchName; + } + + // Match Description + const generateMatchDescription = (match: TBAMatch) => { + let matchDesc = ""; + match.alliances.blue.team_keys.forEach(teamKey => { + let teamNumber = parseInt(teamKey.substring(3)); + matchDesc += teamNumber + " "; + }); + matchDesc += " - " + match.alliances.red.team_keys.forEach(teamKey => { + let teamNumber = parseInt(teamKey.substring(3)); + matchDesc += teamNumber + " "; + }); + return matchDesc; + } + + // Sort + tbaMatches.sort((a, b) => + (a.match_number + MATCH_TYPES.indexOf(a.comp_level) * 1000) - + (b.match_number + MATCH_TYPES.indexOf(b.comp_level) * 1000) + ); + + // Iterate + let matchIDs: string[] = []; + tbaMatches.forEach(match => { + callback(match.match_number); + matchIDs.push(match.key); + putStorage(match.key, { + id: match.key, + name: generateMatchName(match), + description: generateMatchDescription(match), + number: match.match_number, + compLevel: match.comp_level, + blueTeamIDs: match.alliances.blue.team_keys, + redTeamIDs: match.alliances.red.team_keys, + comment: "" + }) + }); + + return matchIDs; +} + +export async function DownloadTeams(eventID: string, callback: (teamNumber: number) => void) { + const tbaTeams = await TBA.getTeams(eventID); + if (!tbaTeams) + return undefined; + const year = parseInt(eventID.substring(0, 4)); + + // Sort + tbaTeams.sort((a, b) => a.team_number - b.team_number); + + // Iterate + let teamIDs: string[] = []; + for (let team of tbaTeams) { + callback(team.team_number); + teamIDs.push(team.key); + + const mediaPaths = await DownloadMedia(team.key, year); + await putStorage(team.key, { + id: team.key, + name: team.nickname, + number: team.team_number, + mediaPaths + }); + } + + return teamIDs; +} + +export async function DownloadMedia(teamID: string, year: number) { + const tbaMedia = await TBA.getTeamMedia(teamID, year); + if (!tbaMedia) + return []; + + let mediaPaths = [] as string[]; + for (let i = 0; i < tbaMedia.length; i++) { + const media = tbaMedia[i]; + const mediaID = teamID + "_" + Math.random().toString(36).slice(2); + + if (media.details.base64Image) { + const path = FileSystem.documentDirectory + mediaID + ".png"; + await FileSystem.writeAsStringAsync(path, media.details.base64Image, { + encoding: FileSystem.EncodingType.Base64, + }); + + mediaPaths.push(path); + } + else if (media.direct_url) { + const path = FileSystem.documentDirectory + mediaID + ".jpg"; + console.log(media.direct_url + " --> " + path); + await FileSystem.downloadAsync(media.direct_url, path); + mediaPaths.push(path); + } + } + + return mediaPaths; +} \ No newline at end of file diff --git a/api/TeamDB.ts b/api/TeamDB.ts deleted file mode 100644 index 62d7901..0000000 --- a/api/TeamDB.ts +++ /dev/null @@ -1,140 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import BlitzDB from "./BlitzDB"; -import Team from "./models/Team"; -import TBA from "./TBA"; - -const SAVE_KEY = "teams-data"; -const BASE64_PREFIX = "data:image/png;base64, "; - -/** - * Represents a database of FRC teams - */ -export default class TeamDB { - teamCache: Team[] = []; - - /** - * Downloads all the teams attending an event - * @param event - Event to reference teams - * @returns true if successful - */ - async downloadEvent(eventID: string, teamCallback: (teamNumber: number) => void): Promise { - - // Download - let tbaTeams = await TBA.getTeams(eventID); - if (!tbaTeams) - return false; - tbaTeams.sort((a, b) => a.team_number - b.team_number); - - // Parse - let teamIDs: string[] = []; - for (let team of tbaTeams) { - teamCallback(team.team_number); - teamIDs.push(team.key); - let existingTeam = this.get(team.key); - if (!existingTeam) { - this.teamCache.push({ - id: team.key, - name: team.nickname, - number: team.team_number, - media: [], - comments: [] - }); - } - await this.downloadMedia(team.key); - } - BlitzDB.event.setTeams(teamIDs); - - return true; - } - - /** - * Downloads all media from an FRC team - * @param teamID - ID of the team - * @returns true if successful - */ - async downloadMedia(teamID: string) { - let mediaList = await TBA.getTeamMedia(teamID, BlitzDB.event.year); - if (mediaList == undefined) - return false; - mediaList.forEach(media => { - let imageData: string | undefined; - if (media.details.base64Image) - imageData = BASE64_PREFIX + media.details.base64Image; - else if (media.details.model_image) - imageData = media.details.model_image; - else if (media.direct_url) - imageData = media.direct_url; - if (imageData) - this.addMedia(teamID, imageData, true); - }); - } - - /** - * Gets a team by it's id - * @param teamID - TBA ID of the team - */ - get(teamID: string): Team | undefined { - let team = this.teamCache.find(team => team.id === teamID); - return team; - } - - /** - * Adds a team to the database - * @param team - Team to add - */ - add(team: Team) { - this.teamCache.push(team); - } - - /** - * Adds media onto the team - * @param teamID - ID of the team - * @param media - Media in Base64 format - */ - addMedia(teamID: string, media: string, removePrefix?: boolean) { - - // TODO: Store media outside of database - let team = this.get(teamID); - let b64 = removePrefix ? media : BASE64_PREFIX + media; - if (team) - if (team.media.indexOf(b64) === -1) - team.media.push(b64); - } - - /** - * Adds comment onto the team - * @param teamID - ID of the team - * @param comment - Text of the comment - */ - addComment(teamID: string, comment: string) { - let team = this.get(teamID); - if (team) - team.comments.push(comment); - } - - /** - * Deletes all teams from the database - */ - async deleteAll() { - this.teamCache = []; - await AsyncStorage.removeItem(SAVE_KEY); - } - - /** - * Saves data from the cache to the database - */ - async save() { - let jsonTeams = JSON.stringify(this.teamCache); - await AsyncStorage.setItem(SAVE_KEY, jsonTeams); - } - - /** - * Loads the team cache - */ - async load() { - let jsonTeams = await AsyncStorage.getItem(SAVE_KEY); - if (!jsonTeams) - return; - this.teamCache = JSON.parse(jsonTeams) as Team[]; - } -} \ No newline at end of file diff --git a/api/TemplateDB.ts b/api/TemplateDB.ts deleted file mode 100644 index b742c54..0000000 --- a/api/TemplateDB.ts +++ /dev/null @@ -1,83 +0,0 @@ -import AsyncStorage from "@react-native-async-storage/async-storage"; -import React, { useEffect } from "react"; -import BlitzDB from "./BlitzDB"; -import { ElementData, ScoutingTemplate } from "./models/TemplateModels"; - -/** - * Handles scouting / pit scouting templates - */ -export default class TemplateDB { - template: ScoutingTemplate = []; - saveKey: string; - - constructor(saveKey: string) { - this.saveKey = saveKey; - } - - /** - * Adds an element to a scouting template - * @param element - Element to append - */ - addElement(element: ElementData) { - this.template.push(element); - this.save(); - } - - /** - * Sets all data of an element - * @param element - Element to set - */ - setElement(element: ElementData) { - let index = this.template.findIndex(e => e.id === element.id); - if (index >= 0) - this.template[index] = element; - else - console.error("Could not find element of id:" + element.id) - } - - /** - * Replaces the current template - * @param template - Replacement template - */ - setTemplate(template: ScoutingTemplate) { - this.template = template; - } - - /** - * Subscribes to a template as a react hook - * @returns React Hook of the template - */ - useTemplate() { - const [version, setVersion] = React.useState(0); - - useEffect(() => { - function onTemplateChange() { - setVersion(v => v + 1); - } - - BlitzDB.eventEmitter.addListener("template", onTemplateChange); - return () => { BlitzDB.eventEmitter.removeListener("template", onTemplateChange); } - }, []); - - return this.template; - } - - /** - * Saves all template data - */ - async save() { - let jsonTemplates = JSON.stringify(this.template); - await AsyncStorage.setItem(this.saveKey, jsonTemplates); - BlitzDB.eventEmitter.emit("template"); - } - - /** - * Loads all template data - */ - async load() { - let jsonTemplates = await AsyncStorage.getItem(this.saveKey); - if (!jsonTemplates) - return; - this.template = JSON.parse(jsonTemplates); - } -} \ No newline at end of file diff --git a/api/models/Match.ts b/api/models/Match.ts deleted file mode 100644 index 1a9c889..0000000 --- a/api/models/Match.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Represents a game match - */ -export default interface Match { - id: string; - name: string; - description: string; - number: number; - compLevel: string; - blueTeamIDs: string[]; - redTeamIDs: string[]; - comment: string; -} \ No newline at end of file diff --git a/api/models/Team.ts b/api/models/Team.ts deleted file mode 100644 index 6db8adc..0000000 --- a/api/models/Team.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Represents an FRC Team - */ -export default interface Team { - id: string; - name: string; - number: number; - media: string[]; - comments: string[]; -} \ No newline at end of file diff --git a/api/models/TemplateModels.ts b/api/models/TemplateModels.ts deleted file mode 100644 index 3f45d11..0000000 --- a/api/models/TemplateModels.ts +++ /dev/null @@ -1,29 +0,0 @@ -export enum TemplateType { - Pit, - Match -} - -export enum ElementType { - title, - subtitle, - text, - hr, - counter, - checkbox, - textbox -} - -export interface ElementData { - id: string; - type: ElementType; - label: string; - options: any; -} - -export interface ElementProps { - data: ElementData; - isEditable: boolean; - onChange?: (value: any) => void -} - -export type ScoutingTemplate = ElementData[]; \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index db538eb..3443250 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,7 +1,7 @@ -module.exports = function(api) { - api.cache(true); - return { - presets: ['babel-preset-expo'], - plugins: ['react-native-reanimated/plugin'], - }; +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: ['react-native-reanimated/plugin'], + }; }; diff --git a/components/containers/MediaScreen.tsx b/components/containers/MediaScreen.tsx index db2f000..0c2e08d 100644 --- a/components/containers/MediaScreen.tsx +++ b/components/containers/MediaScreen.tsx @@ -1,39 +1,45 @@ import { MaterialIcons } from "@expo/vector-icons"; +import * as Sharing from 'expo-sharing'; import React from "react"; -import { Alert, Dimensions, Image, StyleSheet, ToastAndroid, View } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; +import { Alert, Dimensions, Image, StyleSheet, View } from "react-native"; import Button from "../common/Button"; import DarkBackground from "../common/DarkBackground"; import Text from "../text/Text"; import PanZoomContainer from "./PanZoomContainer"; export default function MediaScreen({ route }: any) { - const teamID = route.params.teamID as string; - const imageIndex = route.params.imageIndex as number; + const mediaPath = route.params.mediaPath; - // Team - let team = BlitzDB.teams.get(teamID); - if (!(team)) { - Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); - return null; + const shareImage = () => { + Sharing.shareAsync(mediaPath); } - // Media - if (imageIndex >= team.media.length) - return null; - let mediaData = team.media[imageIndex]; + const deleteImage = () => { + Alert.alert("Are you sure?", "Are you sure you want to delete this image?", + [ + { + text: "Confirm", + onPress: () => { } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true } + ); + } return ( - + + ); } - } + }); } // Display Data diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 0ec6621..ed1fcd4 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -1,13 +1,12 @@ import { useNavigation } from '@react-navigation/core'; import * as Application from 'expo-application'; import * as React from 'react'; -import BlitzDB from '../../api/BlitzDB'; -import { TemplateType } from '../../api/models/TemplateModels'; import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; import Subtitle from '../../components/text/Subtitle'; +import { clearStorage } from '../../hooks/useStorage'; import DownloadingModal from './DownloadingModal'; export default function SettingsScreen() { @@ -20,18 +19,9 @@ export default function SettingsScreen() { Settings {/* Data Buttons */} - {BlitzDB.event.isLoaded ? - { BlitzDB.downloadEvent(BlitzDB.event.id, setDownloadStatus); }} - /> - : null} - { navigator.navigate("Year"); }} /> @@ -39,7 +29,7 @@ export default function SettingsScreen() { iconType={"delete-outline"} title={"Clear All Data"} subtitle={"Wipes all data on your device"} - onPress={() => { BlitzDB.deleteAll(true); }} /> + onPress={() => { clearStorage(); }} /> @@ -55,7 +45,7 @@ export default function SettingsScreen() { iconType={"edit"} title={"Edit Match Scouting"} subtitle={"Adjust the match scouting template"} - onPress={() => { navigator.navigate("EditTemplate", { templateType: TemplateType.Match }); }} /> + onPress={() => { /*navigator.navigate("EditTemplate", { templateType: TemplateType.Match });*/ }} /> { + if (status) + setMaxYear(status.max_season); + else + ToastAndroid.show("Failed to connect to TBA", 1000); + }); + // Generate List let yearsDisplay: JSX.Element[] = []; if (maxYear <= INIT_YEAR) { - TBA.getServerStatus().then((status) => { - if (status) - setMaxYear(status.max_season); - else - ToastAndroid.show("Failed to connect to TBA", 1000); - }) - yearsDisplay.push( Loading All Seasons... diff --git a/screens/Sharing/ExportQRModal.tsx b/screens/Sharing/ExportQRModal.tsx index 2961a19..79d7587 100644 --- a/screens/Sharing/ExportQRModal.tsx +++ b/screens/Sharing/ExportQRModal.tsx @@ -2,7 +2,6 @@ import LZString from 'lz-string'; import * as React from 'react'; import { Dimensions, Modal, StyleSheet, View } from 'react-native'; import QRCode from 'react-qr-code'; -import BlitzDB from '../../api/BlitzDB'; import DarkBackground from '../../components/common/DarkBackground'; export interface ModalProps { @@ -17,7 +16,7 @@ export default function ExportQRModal(props: ModalProps) { const windowSize = Dimensions.get("window"); const qrSize = Math.min(windowSize.width, windowSize.height); - const commentData = BlitzDB.exportComments(); + const commentData = ""; // BlitzDB.exportComments(); const compressedData = LZString.compress(commentData); return ( diff --git a/screens/Sharing/ImportQRModal.tsx b/screens/Sharing/ImportQRModal.tsx deleted file mode 100644 index 8bab05f..0000000 --- a/screens/Sharing/ImportQRModal.tsx +++ /dev/null @@ -1,10 +0,0 @@ - -export interface ModalProps { - isVisible: boolean; - setVisible: (isVisible: boolean) => void; -} - -export default function ImportQRModal(props: ModalProps) { - // TODO: View and import QRCode - return null; -} \ No newline at end of file diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index 7368901..0aa1186 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -4,11 +4,9 @@ import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; import ExportQRModal from './ExportQRModal'; -import ImportQRModal from './ImportQRModal'; export default function SharingScreen() { const [isExportQRVisible, setExportQRVisible] = React.useState(false); - const [isImportQRVisible, setImportQRVisible] = React.useState(false); return ( @@ -23,12 +21,11 @@ export default function SharingScreen() { subtitle={"Export Scouting Data"} onPress={() => { setExportQRVisible(true); }} /> - { setImportQRVisible(true); }} /> + onPress={() => { }} /> {/* File Formats */} diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 74cd851..1ae3ffd 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -1,63 +1,97 @@ import { MaterialIcons } from "@expo/vector-icons"; import { useNavigation } from "@react-navigation/core"; +import * as FileSystem from 'expo-file-system'; import * as ImagePicker from 'expo-image-picker'; import React from "react"; -import { Alert, Dimensions, Image, ScrollView, StyleSheet, View } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; +import { Dimensions, Image, ScrollView, StyleSheet, View } from "react-native"; import TBA from "../../api/TBA"; import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import StandardButton from "../../components/common/StandardButton"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; - -export interface TeamProps { - teamID: string; -} +import useTeam from "../../hooks/useTeam"; export default function TeamScreen({ route }: any) { - const [version, setVersion] = React.useState(0); const navigator = useNavigation(); - const teamID = route.params.teamID; - - // Re-render on New Media - /*BlitzDB.eventEmitter.addListener("mediaUpdate", () => { - BlitzDB.eventEmitter.removeCurrentListener(); - setVersion(version + 1); - setPreviewIndex(-1); - });*/ - - // Grab Team Data - let team = BlitzDB.teams.get(teamID); - if (!team) { - Alert.alert("Error", "There was an error grabbing the data from that team. Try re-downloading TBA data then try again."); - return null; + const [team, setTeam] = useTeam(route.params.teamID); + + const generateID = () => { + return team.id + "_" + Math.random().toString(36).slice(2); } - // Grab Team Media - let mediaList: JSX.Element[] = []; - for (let i = 0; i < team.media.length; i++) { - let imageData = team.media[i]; - mediaList.push( - - ); + const takePhoto = async () => { + const path = FileSystem.documentDirectory + generateID() + ".png"; + const cameraResult = await ImagePicker.launchCameraAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + base64: true + }); + + if (cameraResult.cancelled || !cameraResult.base64) + return; + + await FileSystem.writeAsStringAsync(path, cameraResult.base64, { + encoding: FileSystem.EncodingType.Base64, + }); + + let mediaPaths = team.mediaPaths; + mediaPaths.push(path); + setTeam({ + id: team.id, + name: team.name, + number: team.number, + mediaPaths + }); + } + + const uploadPhoto = async () => { + const path = FileSystem.documentDirectory + generateID() + ".png"; + const cameraResult = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [1, 1], + quality: .5, + base64: true + }); + + if (cameraResult.cancelled || !cameraResult.base64) + return; + + await FileSystem.writeAsStringAsync(path, cameraResult.base64, { + encoding: FileSystem.EncodingType.Base64, + }); + + let mediaPaths = team.mediaPaths; + mediaPaths.push(path); + setTeam({ + id: team.id, + name: team.name, + number: team.number, + mediaPaths + }); } - // Return Modal return ( - {mediaList} + {team.mediaPaths.map((mediaPath, index) => + + )} + + {props.isEditable ? + + : + {elementData.label} + } ); } const styles = StyleSheet.create({ container: { - flexDirection: "row", - justifyContent: "space-evenly" + flexDirection: "row" }, textInput: { color: "#fff", @@ -69,18 +90,26 @@ const styles = StyleSheet.create({ borderRadius: 10, padding: 5, margin: 5, - fontSize: 15 + fontSize: 15, + flex: 1 }, title: { + fontWeight: "bold", + marginTop: 15, + marginLeft: 20, + fontSize: 20 + }, + counter: { width: 50, textAlign: "center" }, button: { height: 50, - width: 100, + width: 70, borderRadius: 5, - paddingLeft: 35, - margin: 5, + paddingLeft: 20, + marginLeft: 5, + marginTop: 5, flexDirection: "row", justifyContent: "flex-start", diff --git a/components/elements/HRElement.tsx b/components/elements/HRElement.tsx index df2b28f..ce6c130 100644 --- a/components/elements/HRElement.tsx +++ b/components/elements/HRElement.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ElementProps } from "../../api/models/TemplateModels"; +import { ElementProps } from "../../types/TemplateTypes"; import HorizontalBar from "../common/HorizontalBar"; export default function HRElement(props: ElementProps) { diff --git a/components/elements/ScoutingElement.tsx b/components/elements/ScoutingElement.tsx index 25a25b0..062f733 100644 --- a/components/elements/ScoutingElement.tsx +++ b/components/elements/ScoutingElement.tsx @@ -1,6 +1,8 @@ +import { MaterialIcons } from "@expo/vector-icons"; import React from "react"; -import { StyleSheet } from "react-native"; -import { ElementProps, ElementType } from "../../api/models/TemplateModels"; +import { StyleSheet, View } from "react-native"; +import { ElementProps, ElementType } from "../../types/TemplateTypes"; +import Button from "../common/Button"; import CheckboxElement from "./CheckboxElement"; import CounterElement from "./CounterElement"; import HRElement from "./HRElement"; @@ -34,10 +36,41 @@ export default function ScoutingElement(props: ElementProps) { element = (); break; } - return element; -} -const styles = StyleSheet.create({ + const onRemove = () => { + if (props.onRemove) + props.onRemove(props.data); + } + + return ( + + + + {element} -}); + + + {props.isEditable && props.onRemove ? + + + + : null} + + ); +} + +const styles = StyleSheet.create({ + deleteButton: { + position: "absolute", + right: 0, + top: 0, + padding: 10 + } +}); \ No newline at end of file diff --git a/components/elements/SubtitleElement.tsx b/components/elements/SubtitleElement.tsx index e9b9225..a61e84b 100644 --- a/components/elements/SubtitleElement.tsx +++ b/components/elements/SubtitleElement.tsx @@ -1,7 +1,6 @@ import React from "react"; import { StyleSheet, TextInput } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; -import { ElementProps } from "../../api/models/TemplateModels"; +import { ElementProps } from "../../types/TemplateTypes"; import Subtitle from "../text/Subtitle"; export default function SubtitleElement(props: ElementProps) { @@ -9,7 +8,8 @@ export default function SubtitleElement(props: ElementProps) { if (props.isEditable) { const onEdit = (text: string) => { elementData.label = text; - BlitzDB.matchTemplate.setElement(elementData); + if (props.onChange) + props.onChange(elementData); } return ( diff --git a/components/elements/TextBoxElement.tsx b/components/elements/TextBoxElement.tsx index ec0d803..de9c4bd 100644 --- a/components/elements/TextBoxElement.tsx +++ b/components/elements/TextBoxElement.tsx @@ -1,19 +1,28 @@ import React from "react"; import { StyleSheet, TextInput } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; -import { ElementProps } from "../../api/models/TemplateModels"; +import { ElementProps } from "../../types/TemplateTypes"; export default function TextBoxElement(props: ElementProps) { let elementData = props.data; + // Default Value + if (elementData.value === undefined && props.onChange) { + elementData.value = ""; + props.onChange(elementData); + } + + // On Edit const onEdit = (text: string) => { elementData.label = text; - BlitzDB.matchTemplate.setElement(elementData); + if (props.onChange) + props.onChange(elementData); }; + // On Scout const onScoutingEdit = (text: string) => { + elementData.value = text; if (props.onChange) - props.onChange(text); + props.onChange(elementData); } return ( diff --git a/components/elements/TextElement.tsx b/components/elements/TextElement.tsx index 2912e30..39eb54e 100644 --- a/components/elements/TextElement.tsx +++ b/components/elements/TextElement.tsx @@ -1,7 +1,6 @@ import React from "react"; import { StyleSheet, TextInput } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; -import { ElementProps } from "../../api/models/TemplateModels"; +import { ElementProps } from "../../types/TemplateTypes"; import Text from "../text/Text"; export default function TextElement(props: ElementProps) { @@ -9,7 +8,8 @@ export default function TextElement(props: ElementProps) { if (props.isEditable) { const onEdit = (text: string) => { elementData.label = text; - BlitzDB.matchTemplate.setElement(elementData); + if (props.onChange) + props.onChange(elementData); } return ( diff --git a/components/elements/TitleElement.tsx b/components/elements/TitleElement.tsx index bfc2c62..5d0003b 100644 --- a/components/elements/TitleElement.tsx +++ b/components/elements/TitleElement.tsx @@ -1,7 +1,6 @@ import React from "react"; import { StyleSheet, TextInput } from "react-native"; -import BlitzDB from "../../api/BlitzDB"; -import { ElementProps } from "../../api/models/TemplateModels"; +import { ElementProps } from "../../types/TemplateTypes"; import Title from "../text/Title"; export default function TitleElement(props: ElementProps) { @@ -9,7 +8,8 @@ export default function TitleElement(props: ElementProps) { if (props.isEditable) { const onEdit = (text: string) => { elementData.label = text; - BlitzDB.matchTemplate.setElement(elementData); + if (props.onChange) + props.onChange(elementData); } return ( diff --git a/hooks/useEvent.ts b/hooks/useEvent.ts index 4070462..65392f8 100644 --- a/hooks/useEvent.ts +++ b/hooks/useEvent.ts @@ -2,7 +2,7 @@ import { Event } from "../types/DBTypes"; import useStorage, { getStorage } from "./useStorage"; const DEFAULT_EVENT = { - id: "", + id: "bogus", matchIDs: [], teamIDs: [], year: 0 @@ -14,7 +14,6 @@ const DEFAULT_EVENT = { */ export default function useEvent(): [Event, (event: Event) => Promise] { const [eventData, setEventData] = useStorage("current-event", DEFAULT_EVENT); - return [eventData, setEventData]; } diff --git a/hooks/useMatch.ts b/hooks/useMatch.ts index 83a7d32..d189f6f 100644 --- a/hooks/useMatch.ts +++ b/hooks/useMatch.ts @@ -9,7 +9,7 @@ const DEFAULT_MATCH = { compLevel: "", blueTeamIDs: [], redTeamIDs: [], - comment: "" + scoutingData: [] } as Match; /** diff --git a/hooks/useStats.ts b/hooks/useStats.ts new file mode 100644 index 0000000..432bcb9 --- /dev/null +++ b/hooks/useStats.ts @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; +import { TemplateType } from "../types/TemplateTypes"; +import useTeam from "./useTeam"; +import useTemplate from "./useTemplate"; + +export type TeamStats = TeamMetric[]; +export interface TeamMetric { + label: string, + average: number, + max: number, + min: number +} + +export default function useStats(teamID: string) { + const [team, setTeam] = useTeam(teamID); + const [template, setTemplate] = useTemplate(TemplateType.Match); + const [teamStats, setTeamStats] = useState([] as TeamStats); + + useEffect(() => { + if (team.scoutingData.length === 0 || template.length === 0) { + return; + } + + let newStats: TeamStats = []; + + template.forEach(element => { + if (typeof element.value === "number") { + const metric: TeamMetric = { + label: element.label, + average: 0, + max: Number.MIN_SAFE_INTEGER, + min: Number.MAX_SAFE_INTEGER + }; + + newStats.push(metric); + } + }); + + team.scoutingData.forEach(scout => { + scout.values.forEach((value, index) => { + if (typeof value === "number") { + newStats[index].average += value; + newStats[index].max = Math.max(newStats[index].max, value); + newStats[index].min = Math.min(newStats[index].min, value); + } + }); + }); + + newStats.forEach((stat, index) => { + newStats[index].average /= team.scoutingData.length; + }); + + setTeamStats(newStats); + }, [team, template, setTeamStats]); + + return teamStats; +} \ No newline at end of file diff --git a/hooks/useStorage.ts b/hooks/useStorage.ts index aedcef5..c2cf98f 100644 --- a/hooks/useStorage.ts +++ b/hooks/useStorage.ts @@ -1,5 +1,6 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import EventEmitter from "eventemitter3"; +import * as FileSystem from 'expo-file-system'; import { useEffect, useState } from "react"; const eventEmitter = new EventEmitter(); @@ -20,7 +21,9 @@ export default function useStorage(id: string, defaultValue: Type): [Type, if (jsonData) setData(JSON.parse(jsonData) as Type); }; - useEffect(() => { getData(); }, [version]); + useEffect(() => { + getData(); + }, [id, version, setData]); // Save Data const saveData = async (value: Type) => { @@ -30,13 +33,15 @@ export default function useStorage(id: string, defaultValue: Type): [Type, eventEmitter.emit(id); } useEffect(() => { - function handleDataChange() { + const handleDataChange = () => { setVersion(v => v + 1); } eventEmitter.addListener(id, handleDataChange); - return () => { eventEmitter.removeListener(id, handleDataChange); } - }, []); + return () => { + eventEmitter.removeListener(id, handleDataChange); + }; + }, [id, setVersion]); return [data, saveData]; } @@ -67,8 +72,18 @@ export async function getStorage(id: string) { * Removes all keys from AsyncStorage */ export async function clearStorage() { + // AsyncStorage const keys = await AsyncStorage.getAllKeys(); await AsyncStorage.clear(); - for (let key of keys) + for (let key of keys) { + console.log(key); eventEmitter.emit(key); + } + + // FileSystem + const files = await FileSystem.readDirectoryAsync(FileSystem.documentDirectory ? FileSystem.documentDirectory : ""); + for (let file of files) { + console.log(file); + await FileSystem.deleteAsync(FileSystem.documentDirectory + file); + } } \ No newline at end of file diff --git a/hooks/useTeam.ts b/hooks/useTeam.ts index 1143c96..0734ef5 100644 --- a/hooks/useTeam.ts +++ b/hooks/useTeam.ts @@ -1,11 +1,12 @@ import { Team } from "../types/DBTypes"; -import useStorage from "./useStorage"; +import useStorage, { getStorage } from "./useStorage"; const DEFAULT_TEAM = { id: "", name: "", number: 0, - mediaPaths: [] + mediaPaths: [], + scoutingData: [] } as Team; /** @@ -16,4 +17,8 @@ const DEFAULT_TEAM = { export default function useTeam(teamID: string): [Team, (team: Team) => Promise] { const [teamData, setTeamData] = useStorage(teamID, DEFAULT_TEAM); return [teamData, setTeamData]; +} + +export async function getTeam(teamID: string) { + return await getStorage(teamID); } \ No newline at end of file diff --git a/hooks/useTemplate.ts b/hooks/useTemplate.ts new file mode 100644 index 0000000..3f86445 --- /dev/null +++ b/hooks/useTemplate.ts @@ -0,0 +1,12 @@ +import { ScoutingTemplate, TemplateType } from "../types/TemplateTypes"; +import useStorage from "./useStorage"; + +/** + * Grabs template data as a react hook + * @param templateType - Type of the template + * @returns current template data and a setting function + */ +export default function useTemplate(templateType: TemplateType): [ScoutingTemplate, (template: ScoutingTemplate) => Promise] { + const [templateData, setTemplateData] = useStorage("template-" + templateType, []); + return [templateData, setTemplateData]; +} \ No newline at end of file diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx index 204faf0..9d94d26 100644 --- a/navigation/RootNavigator.tsx +++ b/navigation/RootNavigator.tsx @@ -2,11 +2,16 @@ import { DarkTheme, NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack"; import * as React from 'react'; import MediaScreen from "../components/containers/MediaScreen"; +import DefaultTeamScreen from "../screens/DefaultTeam/DefaultTeamScreen"; +import TeamSelectScreen from "../screens/DefaultTeam/TeamSelectScreen"; import MatchScreen from "../screens/Match/MatchScreen"; +import ScoutingScreen from "../screens/Scout/ScoutingScreen"; import RegionalScreen from "../screens/Settings/RegionalScreen"; -//import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; -//import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; +import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; +import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; import YearScreen from "../screens/Settings/YearScreen"; +import ExportQRScreen from "../screens/Sharing/ExportQRScreen"; +import ImportQRScreen from "../screens/Sharing/ImportQRScreen"; import TeamScreen from "../screens/Team/TeamScreen"; import TabNavigator from "./TabNavigator"; @@ -29,11 +34,13 @@ export default function RootNavigator() { - {/* + - */} + + + diff --git a/navigation/RootParamList.tsx b/navigation/RootParamList.tsx index bd51d59..45b14a8 100644 --- a/navigation/RootParamList.tsx +++ b/navigation/RootParamList.tsx @@ -1,17 +1,20 @@ +import { TemplateType } from "../types/TemplateTypes"; // Params export type RootNavParamList = { Drawer: undefined, Match: { matchID: string }, Team: { teamID: string }, - Media: { mediaPath: string }, + Media: { mediaPath: string, onDelete: (mediaPath: string) => void }, Year: undefined, Regional: { year: number }, - /* EditTemplate: { templateType: TemplateType }, ElementChooser: { templateType: TemplateType }, - Scout: { templateType: TemplateType } - */ + TeamSelect: { matchID: string }, + Scout: { targetID: string, templateType: TemplateType }, + DefaultTeam: undefined, + ExportQR: undefined, + ImportQR: undefined }; // Default diff --git a/package.json b/package.json index 7ca123c..8af69ad 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "expo": "~42.0.1", "expo-application": "~3.2.0", "expo-asset": "~8.3.2", + "expo-barcode-scanner": "~10.2.2", "expo-checkbox": "~1.0.3", "expo-constants": "~11.0.1", "expo-file-system": "~11.1.3", diff --git a/screens/DefaultTeam/DefaultTeamScreen.tsx b/screens/DefaultTeam/DefaultTeamScreen.tsx index a7c7e6c..c9f9e2f 100644 --- a/screens/DefaultTeam/DefaultTeamScreen.tsx +++ b/screens/DefaultTeam/DefaultTeamScreen.tsx @@ -6,6 +6,8 @@ import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; export default function DefaultTeamScreen({ route }: any) { + + return ( diff --git a/screens/DefaultTeam/TeamSelectScreen.tsx b/screens/DefaultTeam/TeamSelectScreen.tsx new file mode 100644 index 0000000..6e2eb82 --- /dev/null +++ b/screens/DefaultTeam/TeamSelectScreen.tsx @@ -0,0 +1,45 @@ +import { useNavigation } from '@react-navigation/native'; +import * as React from 'react'; +import { ScrollView, StyleSheet, View } from "react-native"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import Subtitle from "../../components/text/Subtitle"; +import Title from "../../components/text/Title"; +import useMatch from '../../hooks/useMatch'; +import { TemplateType } from '../../types/TemplateTypes'; +import TeamBanner from '../Teams/TeamBanner'; + +export default function TeamSelectScreen({ route }: any) { + const navigator = useNavigation(); + const [match, setMatch] = useMatch(route.params.matchID); + + const onClick = (teamID: string) => { + navigator.navigate("Scout", { targetID: teamID, templateType: TemplateType.Match }); + } + + return ( + + + {match.name} + Select the team to scout + + + + {match.redTeamIDs.map((teamID) => )} + + + + {match.blueTeamIDs.map((teamID) => )} + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20, + paddingTop: 20 + } +}); diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index 23b0b78..7f18ff2 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -7,11 +7,14 @@ import StandardButton from "../../components/common/StandardButton"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; import useMatch from "../../hooks/useMatch"; +import useTemplate from "../../hooks/useTemplate"; +import { TemplateType } from "../../types/TemplateTypes"; import TeamPreview from "../Matches/TeamPreview"; export default function MatchScreen({ route }: any) { const navigator = useNavigation(); const [match, setMatch] = useMatch(route.params.matchID); + const [template, setTemplate] = useTemplate(TemplateType.Match); return ( @@ -21,11 +24,13 @@ export default function MatchScreen({ route }: any) { - { /*navigator.navigate("Scout", { templateType: TemplateType.Match });*/ }} /> + {template.length > 0 ? + { navigator.navigate("TeamSelect", { matchID: match.id }); }} /> + : undefined} { + if (Platform.OS !== "android") + return; const matchIDs = await DownloadMatches(event.id, () => { }); if (matchIDs) { - setEvent({ + await setEvent({ id: event.id, matchIDs, teamIDs: event.teamIDs, year: event.year }); + ToastAndroid.show("Successfully updated from TBA", 1000); } - else if (Platform.OS === "android") { + else { ToastAndroid.show("Failed to connect to TBA", 1000); } }; return ( - + Matches {event.matchIDs.length > 0 ? event.matchIDs.map((teamID) => ) : - Match data has not been downloaded from TBA yet. Download is available under the settings tab. + isLoaded ? + There is no match data yet. Download it under the settings tab. : + } ); diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index 81514d9..759c9eb 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -4,12 +4,17 @@ import * as React from 'react'; import { Image, StyleSheet, View } from "react-native"; import Button from '../../components/common/Button'; import Text from '../../components/text/Text'; +import useStats from '../../hooks/useStats'; import useTeam from '../../hooks/useTeam'; +import useTemplate from '../../hooks/useTemplate'; +import { TemplateType } from '../../types/TemplateTypes'; export default function TeamPreview(props: { teamID: string }) { const navigator = useNavigation(); const [team, setTeam] = useTeam(props.teamID); + const stats = useStats(props.teamID); + const [matchTemplate] = useTemplate(TemplateType.Match); let mediaIcon: JSX.Element; if (team.mediaPaths.length > 0) { @@ -32,18 +37,26 @@ export default function TeamPreview(props: { teamID: string }) { } return ( - + + + + {stats.map((element, index) => + + {element.label} + {element.average} + + )} + ); } @@ -53,8 +66,14 @@ const styles = StyleSheet.create({ flexDirection: "row", height: 200 }, + button: { + flexDirection: "row" + }, subContainer: { - width: "100%" + marginRight: 20, + width: 120, + justifyContent: "center", + alignItems: "center" }, background: { position: "absolute", @@ -73,14 +92,14 @@ const styles = StyleSheet.create({ marginRight: 15, borderRadius: 5 }, - teamName: { + title: { fontSize: 18, fontWeight: "bold", - textAlign: "left", - width: 100 + textAlign: "center" }, - teamDesc: { + subtitle: { color: "#bbb", - fontWeight: "bold" + fontWeight: "bold", + textAlign: "center" }, }); diff --git a/screens/Scout/ScoutingScreen.tsx b/screens/Scout/ScoutingScreen.tsx index 2b78fe3..759537e 100644 --- a/screens/Scout/ScoutingScreen.tsx +++ b/screens/Scout/ScoutingScreen.tsx @@ -1,30 +1,59 @@ import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { Alert, StyleSheet, View } from 'react-native'; import Button from '../../components/common/Button'; import HorizontalBar from '../../components/common/HorizontalBar'; import ScrollContainer from '../../components/containers/ScrollContainer'; import ScoutingElement from '../../components/elements/ScoutingElement'; import Text from '../../components/text/Text'; +import useTeam from '../../hooks/useTeam'; +import useTemplate from '../../hooks/useTemplate'; +import { ElementData, ScoutingData } from '../../types/TemplateTypes'; export default function ScoutingScreen({ route }: any) { - const template = BlitzDB.matchTemplate.useTemplate(); const navigator = useNavigation(); + const [template, setTemplate] = useTemplate(route.params.templateType); + const [team, setTeam] = useTeam(route.params.targetID); - // Element Preview - let elementList: JSX.Element[] = []; - elementList = template.map(element => ) + console.log(team); + + const onChange = (element: ElementData) => { + const index = template.findIndex(e => e.id === element.id); + if (index >= 0) + template[index] = element; + } + + const onSubmit = () => { + let data: ScoutingData = { + values: [] + }; + for (let element of template) { + if (element.value !== undefined) + data.values.push(element.value); + } + team.scoutingData.push(data); + setTeam(team); + navigator.goBack(); + navigator.goBack(); + Alert.alert("Success", "Data has been saved to storage"); + } return ( - {elementList} + {template.map((element, index) => + + )} {stats.map((element, index) => - {element.label} - {element.average} + {element.label} + {element.average} )} @@ -98,7 +97,6 @@ const styles = StyleSheet.create({ textAlign: "center" }, subtitle: { - color: "#bbb", fontWeight: "bold", textAlign: "center" }, diff --git a/screens/Settings/RegionalScreen.tsx b/screens/Settings/RegionalScreen.tsx index 6687b38..fa42d44 100644 --- a/screens/Settings/RegionalScreen.tsx +++ b/screens/Settings/RegionalScreen.tsx @@ -7,10 +7,12 @@ import { DownloadEvent } from "../../api/TBAAdapter"; import Button from "../../components/common/Button"; import ScrollContainer from "../../components/containers/ScrollContainer"; import Text from "../../components/text/Text"; +import { PaletteContext } from "../../context/PaletteContext"; import { TBAEvent } from "../../types/TBAModels"; import DownloadingModal from "./DownloadingModal"; export default function RegionalScreen({ route }: any) { + const paletteContext = React.useContext(PaletteContext); const navigator = useNavigation(); const [searchTerm, updateSearch] = React.useState(""); const [regionalList, updateRegionals] = React.useState([] as TBAEvent[]); @@ -50,9 +52,9 @@ export default function RegionalScreen({ route }: any) { ) }); @@ -78,10 +80,10 @@ export default function EditTemplateScreen({ route }: any) { @@ -107,8 +109,5 @@ const styles = StyleSheet.create({ bottom: 40, right: 25, padding: 10, - - backgroundColor: "#c89f00", - color: "#000000" } }); \ No newline at end of file diff --git a/screens/Sharing/ExportNFCScreen.tsx b/screens/Sharing/ExportNFCScreen.tsx new file mode 100644 index 0000000..1b4ab65 --- /dev/null +++ b/screens/Sharing/ExportNFCScreen.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { StyleSheet, View } from 'react-native'; +import Text from '../../components/text/Text'; + +export default function ExportNFCScreen() { + + return ( + + + Waiting for another device... + + ); +} + +const styles = StyleSheet.create({ + container: { + } +}); diff --git a/types.tsx b/types.tsx deleted file mode 100644 index 99c39cf..0000000 --- a/types.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* -declare global { - namespace ReactNavigation { - interface RootParamList extends RootStackParamList { } - } -} -*/ - -/* -export type RootStackParamList = { - Root: NavigatorScreenParams | undefined; - Team: TeamProps; - Match: MatchProps; - TeamMatches: TeamMatchesProps; - DefaultTeam: undefined; -};*/ - -/* -export type RootStackScreenProps = NativeStackScreenProps< - RootStackParamList, - Screen ->; -*/ - -/* -export type RootTabParamList = { - Teams: undefined; - Matches: undefined; - Sharing: undefined; - Settings: undefined; -};*/ - -/* -export type RootTabScreenProps = CompositeScreenProps< - BottomTabScreenProps, - NativeStackScreenProps ->; -*/ diff --git a/types/OtherTypes.ts b/types/OtherTypes.ts new file mode 100644 index 0000000..8071f2b --- /dev/null +++ b/types/OtherTypes.ts @@ -0,0 +1,10 @@ +export interface Palette { + background: string; + button: string; + navigation: string; + navigationText: string; + navigationSelected: string; + navigationTextSelected: string; + textPrimary: string; + textSecondary: string; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9f49fe0..a2a66c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1158,6 +1158,11 @@ dependencies: "@types/hammerjs" "^2.0.36" +"@expo-google-fonts/righteous@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@expo-google-fonts/righteous/-/righteous-0.2.0.tgz#c81bd43dfaf05c03c9ebeab7ee7126a8babda027" + integrity sha512-fZXVTfVutI0LGYWh/4s0Nu0HfrQnuOJYs11I38UU8Vn9yCFLzpREhJE/m3rF7CPC5iFg2eSRGJ9aj5X9cPjY5g== + "@expo/config-plugins@1.0.33": version "1.0.33" resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-1.0.33.tgz" @@ -1195,7 +1200,7 @@ xcode "^3.0.1" xml2js "^0.4.23" -"@expo/config-plugins@3.1.0", "@expo/config-plugins@^3.0.0": +"@expo/config-plugins@3.1.0", "@expo/config-plugins@^3.0.0", "@expo/config-plugins@^3.0.6": version "3.1.0" resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-3.1.0.tgz" integrity sha512-V5qxaxCAExBM0TXmbU1QKiZcAGP3ecu7KXede8vByT15cro5PkcWu2sSdJCYbHQ/gw6Vf/i8sr8gKlN8V8TSLg== @@ -7354,11 +7359,23 @@ react-native-gesture-handler@~1.10.2: invariant "^2.2.4" prop-types "^15.7.2" +react-native-global-props@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/react-native-global-props/-/react-native-global-props-1.1.5.tgz#443e0ffc89d5402fa20ebedf37bcbca6d861c34d" + integrity sha512-QDeAdRel6zyJfbgyFxZi9QXZe78OdlANxJae0rJn76uTqdt/A+iWBVjJy3NmaN/fpKy0uV0HhW6Hu4xM0QCisQ== + react-native-iphone-x-helper@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== +react-native-nfc-manager@^3.11.4: + version "3.11.4" + resolved "https://registry.yarnpkg.com/react-native-nfc-manager/-/react-native-nfc-manager-3.11.4.tgz#28206e6fa6f733281ce495a6a6c55e060eaf7292" + integrity sha512-aHuoOWLet9vmEIUeD18T8V2rug6t70UzkrWbNQMU86y/lROyI88W0thjctqrFPwlDP+MvYBUOnN/hXO33Mv7xg== + dependencies: + "@expo/config-plugins" "^3.0.6" + react-native-pager-view@5.0.12: version "5.0.12" resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-5.0.12.tgz#5106735d944e7f876b006377ab6a18859bf7730c" From 1e38687fa963bc632c699b44a29482704ff34a7f Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 12 Jan 2022 23:39:01 -0600 Subject: [PATCH 22/38] Import/Export Framework & QR Codes --- components/common/StandardButton.tsx | 13 ++-- hooks/useCompressedData.ts | 89 ++++++++++++++++++++++++ hooks/useStats.ts | 4 +- hooks/useTeam.ts | 6 +- navigation/RootNavigator.tsx | 2 + navigation/RootParamList.tsx | 5 +- package.json | 1 + screens/DefaultTeam/TeamSelectScreen.tsx | 2 +- screens/Scout/ScoutingScreen.tsx | 5 +- screens/Settings/AboutScreen.tsx | 62 +++++++++++++++++ screens/Settings/SettingsScreen.tsx | 27 +++---- screens/Sharing/ExportQRScreen.tsx | 30 +------- screens/Sharing/ImportQRScreen.tsx | 7 +- screens/Team/TeamScreen.tsx | 2 + types/TemplateTypes.ts | 1 + yarn.lock | 9 +++ 16 files changed, 209 insertions(+), 56 deletions(-) create mode 100644 hooks/useCompressedData.ts create mode 100644 screens/Settings/AboutScreen.tsx diff --git a/components/common/StandardButton.tsx b/components/common/StandardButton.tsx index c47a54b..dd7e47d 100644 --- a/components/common/StandardButton.tsx +++ b/components/common/StandardButton.tsx @@ -70,16 +70,17 @@ const styles = StyleSheet.create({ }, buttonIconFA: { marginRight: 12, - width: 45, + width: 50, alignItems: 'center', justifyContent: 'center' }, buttonIconSVG: { - width: 45, - height: 45, + width: 60, + height: 60, + margin: -10, marginRight: 12, - resizeMode: 'stretch', - borderRadius: 1 + borderTopLeftRadius: 1, + borderBottomLeftRadius: 1, }, buttonIconTXT: { fontSize: 20, @@ -87,6 +88,6 @@ const styles = StyleSheet.create({ textAlign: "center", paddingTop: 5, marginRight: 12, - width: 45, + width: 50, } }); \ No newline at end of file diff --git a/hooks/useCompressedData.ts b/hooks/useCompressedData.ts new file mode 100644 index 0000000..dbc1684 --- /dev/null +++ b/hooks/useCompressedData.ts @@ -0,0 +1,89 @@ +import LZString from "lz-string"; +import React, { useEffect, useState } from "react"; +import { ToastAndroid } from "react-native"; +import { ScoutingData } from "../types/TemplateTypes"; +import useEvent from "./useEvent"; +import { getTeam, setTeam } from "./useTeam"; + +export interface ExportData { + eventID: string, + exportID: string, + scoutingData: Record +} + +export function useCompressedData() { + const [event] = useEvent(); + const [data, setData] = useState(""); + + const compressData = async () => { + const teams = await Promise.all(event.teamIDs.map(getTeam)); + let scoutingData: Record = {}; + for (const team of teams) { + if (team) + if (team.scoutingData.length > 0) + scoutingData[team.id] = team.scoutingData; + } + const outputData: ExportData = { + exportID: Math.random().toString(36).slice(2), + eventID: event.id, + scoutingData + }; + const jsonData = JSON.stringify(outputData); + const compressedData = LZString.compressToEncodedURIComponent(jsonData); + setData(compressedData); + } + + useEffect(() => { + compressData(); + }, [event]); + + return data; +} + +export function decompressData(data: string): ExportData | undefined { + const decompressedData = LZString.decompressFromEncodedURIComponent(data); + if (decompressedData) { + const data = JSON.parse(decompressedData) as ExportData; + return data; + } + return undefined; +} + +export function useDecompressedData() { + const [event] = useEvent(); + const [importedIDs, setImportedIDs] = React.useState([] as string[]); + + const importCompressedData = async (data: string) => { + const decompressedData = decompressData(data); + if (!decompressedData) { + ToastAndroid.show("Invalid QR code", ToastAndroid.SHORT); + return; + } + if (importedIDs.includes(decompressedData.exportID)) { + return; + } + importedIDs.push(decompressedData.exportID); + setImportedIDs(importedIDs); + if (decompressedData.eventID !== event.id) { + ToastAndroid.show("Invalid Event", ToastAndroid.SHORT); + return; + } + + const teamIDs = Object.keys(decompressedData.scoutingData); + for (const teamID of teamIDs) { + const team = await getTeam(teamID); + if (team) { + const scoutingData = decompressedData.scoutingData[teamID]; + for (let scout of scoutingData) { + if (team.scoutingData.findIndex(s => s.matchID === scout.matchID) === -1) { + team.scoutingData.push(scout); + } + } + await setTeam(team); + } + } + ToastAndroid.show("Imported " + teamIDs.length + " team(s).", ToastAndroid.SHORT); + }; + + return [importCompressedData]; +} \ No newline at end of file diff --git a/hooks/useStats.ts b/hooks/useStats.ts index 432bcb9..7e2324e 100644 --- a/hooks/useStats.ts +++ b/hooks/useStats.ts @@ -12,8 +12,8 @@ export interface TeamMetric { } export default function useStats(teamID: string) { - const [team, setTeam] = useTeam(teamID); - const [template, setTemplate] = useTemplate(TemplateType.Match); + const [team] = useTeam(teamID); + const [template] = useTemplate(TemplateType.Match); const [teamStats, setTeamStats] = useState([] as TeamStats); useEffect(() => { diff --git a/hooks/useTeam.ts b/hooks/useTeam.ts index 0734ef5..aef683b 100644 --- a/hooks/useTeam.ts +++ b/hooks/useTeam.ts @@ -1,5 +1,5 @@ import { Team } from "../types/DBTypes"; -import useStorage, { getStorage } from "./useStorage"; +import useStorage, { getStorage, putStorage } from "./useStorage"; const DEFAULT_TEAM = { id: "", @@ -21,4 +21,8 @@ export default function useTeam(teamID: string): [Team, (team: Team) => Promise< export async function getTeam(teamID: string) { return await getStorage(teamID); +} + +export async function setTeam(team: Team) { + return await putStorage(team.id, team); } \ No newline at end of file diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx index ac0e21c..89915bd 100644 --- a/navigation/RootNavigator.tsx +++ b/navigation/RootNavigator.tsx @@ -7,6 +7,7 @@ import DefaultTeamScreen from "../screens/DefaultTeam/DefaultTeamScreen"; import TeamSelectScreen from "../screens/DefaultTeam/TeamSelectScreen"; import MatchScreen from "../screens/Match/MatchScreen"; import ScoutingScreen from "../screens/Scout/ScoutingScreen"; +import AboutScreen from "../screens/Settings/AboutScreen"; import RegionalScreen from "../screens/Settings/RegionalScreen"; import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; @@ -43,6 +44,7 @@ export default function RootNavigator() { + diff --git a/navigation/RootParamList.tsx b/navigation/RootParamList.tsx index 45b14a8..cfcf434 100644 --- a/navigation/RootParamList.tsx +++ b/navigation/RootParamList.tsx @@ -11,10 +11,11 @@ export type RootNavParamList = { EditTemplate: { templateType: TemplateType }, ElementChooser: { templateType: TemplateType }, TeamSelect: { matchID: string }, - Scout: { targetID: string, templateType: TemplateType }, + Scout: { teamID: string, matchID: string, templateType: TemplateType }, DefaultTeam: undefined, ExportQR: undefined, - ImportQR: undefined + ImportQR: undefined, + About: undefined }; // Default diff --git a/package.json b/package.json index 7457465..397a8af 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "expo-font": "~9.2.1", "expo-image-picker": "~10.2.2", "expo-linking": "~2.3.1", + "expo-sensors": "~10.2.2", "expo-sharing": "~9.2.1", "expo-splash-screen": "~0.11.2", "expo-status-bar": "~1.0.4", diff --git a/screens/DefaultTeam/TeamSelectScreen.tsx b/screens/DefaultTeam/TeamSelectScreen.tsx index 6e2eb82..bab0c98 100644 --- a/screens/DefaultTeam/TeamSelectScreen.tsx +++ b/screens/DefaultTeam/TeamSelectScreen.tsx @@ -13,7 +13,7 @@ export default function TeamSelectScreen({ route }: any) { const [match, setMatch] = useMatch(route.params.matchID); const onClick = (teamID: string) => { - navigator.navigate("Scout", { targetID: teamID, templateType: TemplateType.Match }); + navigator.navigate("Scout", { teamID: teamID, matchID: route.params.matchID, templateType: TemplateType.Match }); } return ( diff --git a/screens/Scout/ScoutingScreen.tsx b/screens/Scout/ScoutingScreen.tsx index 759537e..3c48810 100644 --- a/screens/Scout/ScoutingScreen.tsx +++ b/screens/Scout/ScoutingScreen.tsx @@ -13,9 +13,7 @@ import { ElementData, ScoutingData } from '../../types/TemplateTypes'; export default function ScoutingScreen({ route }: any) { const navigator = useNavigation(); const [template, setTemplate] = useTemplate(route.params.templateType); - const [team, setTeam] = useTeam(route.params.targetID); - - console.log(team); + const [team, setTeam] = useTeam(route.params.teamID); const onChange = (element: ElementData) => { const index = template.findIndex(e => e.id === element.id); @@ -25,6 +23,7 @@ export default function ScoutingScreen({ route }: any) { const onSubmit = () => { let data: ScoutingData = { + matchID: route.params.matchID, values: [] }; for (let element of template) { diff --git a/screens/Settings/AboutScreen.tsx b/screens/Settings/AboutScreen.tsx new file mode 100644 index 0000000..e0570a7 --- /dev/null +++ b/screens/Settings/AboutScreen.tsx @@ -0,0 +1,62 @@ +import { MaterialIcons } from "@expo/vector-icons"; +import * as Application from 'expo-application'; +import { Accelerometer } from 'expo-sensors'; +import * as React from "react"; +import { Image } from "react-native"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import ScrollContainer from "../../components/containers/ScrollContainer"; +import Subtitle from "../../components/text/Subtitle"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; +import { PaletteContext } from "../../context/PaletteContext"; + +export default function AboutScreen() { + const paletteContext = React.useContext(PaletteContext); + const [accelerometer, setAccelerometer] = React.useState({ x: 0, y: 0, z: 0 }); + const [isEasterEgg, setEasterEgg] = React.useState(false); + + Accelerometer.setUpdateInterval(1000); + React.useEffect(() => { + const subscription = Accelerometer.addListener((data) => { + setAccelerometer(data); + }); + + return () => { + subscription.remove(); + }; + }, []); + + React.useEffect(() => { + const acceleration = Math.max(accelerometer.x, accelerometer.y, accelerometer.z); + if (acceleration > 1 && !isEasterEgg) { + setEasterEgg(true); + console.log(acceleration); + } + }, [accelerometer, setEasterEgg, isEasterEgg]); + + return ( + + Blitz Scouter + Version {Application.nativeApplicationVersion} + + + + + Blitz Scouter is a scouting app for the FIRST Robotics Competition. It is designed to be as simple as possible, and is designed to be used by teams of all sizes. + + + + + + Made with by Team 5148, New Berlin Blitz + + + https://team5148.org/ + {"\n"} + 5148nbblitz@gmail.com + + {isEasterEgg ? + + : null} + ) +} \ No newline at end of file diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 97327c3..233faf5 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -1,12 +1,10 @@ import { useNavigation } from '@react-navigation/core'; -import * as Application from 'expo-application'; import * as React from 'react'; import { Alert } from 'react-native'; import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; -import Subtitle from '../../components/text/Subtitle'; import { DARK_PALETTE, PaletteContext } from '../../context/PaletteContext'; import { clearStorage } from '../../hooks/useStorage'; import { TemplateType } from '../../types/TemplateTypes'; @@ -42,8 +40,8 @@ export default function SettingsScreen() { {/* Data Buttons */} { navigator.navigate("Year"); }} /> { paletteContext.setPalette(DARK_PALETTE); }} /> - { clearData(); }} /> - {/* Scouting Buttons */} @@ -74,11 +66,22 @@ export default function SettingsScreen() { { navigator.navigate("DefaultTeam"); }} /> - Blitz Scouter v{Application.nativeApplicationVersion} + + { clearData(); }} /> + + { navigator.navigate("About"); }} /> ); diff --git a/screens/Sharing/ExportQRScreen.tsx b/screens/Sharing/ExportQRScreen.tsx index 7ed5986..548d296 100644 --- a/screens/Sharing/ExportQRScreen.tsx +++ b/screens/Sharing/ExportQRScreen.tsx @@ -1,44 +1,20 @@ -import LZString from 'lz-string'; import * as React from 'react'; import { Dimensions, StyleSheet, View } from 'react-native'; import QRCode from 'react-qr-code'; -import useEvent from '../../hooks/useEvent'; -import { getTeam } from '../../hooks/useTeam'; +import { useCompressedData } from '../../hooks/useCompressedData'; export default function ExportQRScreen() { - const [event, setEvent] = useEvent(); - const [data, setData] = React.useState(""); - - console.log(data); - - const getData = async () => { - let newData = ""; - for (const teamID of event.teamIDs) { - const team = await getTeam(teamID); - if (team) { - const scoutingData = team.scoutingData; - for (const scout of scoutingData) { - newData += scout.values.join(" ") + " "; - } - newData += "+"; - } - } - setData(newData); - }; - React.useEffect(() => { - getData(); - }, [event]); + const data = useCompressedData(); const windowSize = Dimensions.get("window"); const qrSize = Math.min(windowSize.width, windowSize.height); - const compressedData = LZString.compress(data); return ( { - console.log(e); + const onScan = async (e: BarCodeEvent) => { + const compressedData = e.data; + importCompressedData(compressedData); }; return ( diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 8783c49..4278cd2 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -19,6 +19,8 @@ export default function TeamScreen({ route }: any) { const [team, setTeam] = useTeam(route.params.teamID); const stats = useStats(team.id); + console.log(stats); + const generateID = () => { return team.id + "_" + Math.random().toString(36).slice(2); } diff --git a/types/TemplateTypes.ts b/types/TemplateTypes.ts index ef60cab..331b998 100644 --- a/types/TemplateTypes.ts +++ b/types/TemplateTypes.ts @@ -31,5 +31,6 @@ export interface ElementProps { export type ScoutingTemplate = ElementData[]; export interface ScoutingData { + matchID: string; values: (number | boolean | string)[] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a2a66c9..59f7efe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4105,6 +4105,15 @@ expo-modules-core@~0.2.0: resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-0.2.0.tgz" integrity sha512-inpfZ5X/BaTtbj2wG9PA9AC0MN8VyId6KSRlVuEg7+ziurHBy/kKDFxpOddUokhwiln2uhoYPSStJjR/tKypdw== +expo-sensors@~10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/expo-sensors/-/expo-sensors-10.2.2.tgz#882e25b3135995e383d88b21b17e5f1b1155d4e0" + integrity sha512-kZZobyfQQPA7PwuTQ2+HwunZ1o2emWLKqWP9jYOBPI4toQAproSLzhlhKmpm2vRDOL1ctC12Fb0g9elv7r9Omg== + dependencies: + "@expo/config-plugins" "^3.0.0" + expo-modules-core "~0.2.0" + invariant "^2.2.4" + expo-sharing@~9.2.1: version "9.2.1" resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-9.2.1.tgz#50267cd66f54d29f0ab3f185a05d92b197e5b60c" From 5bd4098648a6af1e9048762a6e8efcdf78089bb1 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 19 Jan 2022 05:32:05 -0600 Subject: [PATCH 23/38] Onboarding & Storage Bug Fixes --- App.tsx | 26 +- api/TBA.ts | 2 +- api/TBAAdapter.ts | 7 +- assets/images/icon_512.png | Bin 0 -> 15690 bytes assets/images/logo.png | Bin 0 -> 18201 bytes assets/images/tba_lamp.png | Bin 0 -> 3914 bytes components/common/HorizontalBar.tsx | 4 +- components/common/StandardButton.tsx | 42 +- context/PaletteContext.tsx | 10 +- css.d.ts | 1 + hooks/useCompressedData.ts | 89 +- hooks/useStorage.ts | 5 +- navigation/RootNavigator.tsx | 62 +- navigation/RootParamList.tsx | 8 +- navigation/TabNavigator.tsx | 5 +- package.json | 10 +- screens/Match/MatchScreen.tsx | 2 +- screens/Matches/MatchesScreen.tsx | 33 +- screens/Settings/AboutScreen.tsx | 2 +- screens/Settings/ColorPicker.tsx | 56 + screens/Settings/DownloadScreen.tsx | 61 + screens/Settings/DownloadingModal.tsx | 45 - screens/Settings/OnboardingScreen.tsx | 134 ++ screens/Settings/PaletteScreen.tsx | 80 + screens/Settings/RegionalScreen.tsx | 13 +- screens/Settings/SettingsScreen.tsx | 46 +- .../Settings/Template/EditTemplateScreen.tsx | 6 +- screens/Sharing/ExportQRScreen.tsx | 18 +- screens/Sharing/ImportQRScreen.tsx | 9 +- screens/Sharing/SharingScreen.tsx | 80 +- screens/Team/TeamScreen.tsx | 4 +- screens/Teams/TeamsScreen.tsx | 35 +- yarn-error.log | 1578 ++++++++++++++++- yarn.lock | 1473 ++++++++++++++- 34 files changed, 3562 insertions(+), 384 deletions(-) create mode 100644 assets/images/icon_512.png create mode 100644 assets/images/logo.png create mode 100644 assets/images/tba_lamp.png create mode 100644 css.d.ts create mode 100644 screens/Settings/ColorPicker.tsx create mode 100644 screens/Settings/DownloadScreen.tsx delete mode 100644 screens/Settings/DownloadingModal.tsx create mode 100644 screens/Settings/OnboardingScreen.tsx create mode 100644 screens/Settings/PaletteScreen.tsx diff --git a/App.tsx b/App.tsx index a7a4f58..3c4976c 100644 --- a/App.tsx +++ b/App.tsx @@ -5,28 +5,22 @@ import { PaletteProvider } from './context/PaletteContext'; import useCachedResources from './hooks/useCachedResources'; import RootNavigator from './navigation/RootNavigator'; -function App() { +export default function App() { const isLoadingComplete = useCachedResources(); if (!isLoadingComplete) { return null; } else { return ( - - - - + + + + + + ); } -} - -export default function AppContainer() { - return ( - - - - ); } \ No newline at end of file diff --git a/api/TBA.ts b/api/TBA.ts index 4eb7874..beb1b4f 100644 --- a/api/TBA.ts +++ b/api/TBA.ts @@ -76,7 +76,7 @@ export default class TBA { /* https://github.com/whatwg/fetch/issues/180 - TL;DR; fetch doesn't include a request timeout by default. + TL;DR: fetch doesn't include a request timeout by default. While this solution does introduce memory leaks, there is no other option until a better solution is implemented. */ diff --git a/api/TBAAdapter.ts b/api/TBAAdapter.ts index ca7dc43..db37592 100644 --- a/api/TBAAdapter.ts +++ b/api/TBAAdapter.ts @@ -8,7 +8,7 @@ import TBA from "./TBA"; const MATCH_TYPES = ["qm", "qf", "sf", "f"]; -export async function DownloadEvent(eventID: string, callback: (status: string) => void) { +export async function DownloadEvent(eventID: string, includeMedia: boolean, callback: (status: string) => void) { // Matches const matchIDs = await DownloadMatches(eventID, matchNumber => { @@ -18,19 +18,20 @@ export async function DownloadEvent(eventID: string, callback: (status: string) return callback(""); // Teams - const teamIDs = await DownloadTeams(eventID, true, teamNumber => { + const teamIDs = await DownloadTeams(eventID, includeMedia, teamNumber => { callback("Downloading Team " + teamNumber + "..."); }); if (teamIDs === undefined) return callback(""); // Event - putStorage("current-event", { + await putStorage("current-event", { id: eventID, year: parseInt(eventID.substring(0, 4)), matchIDs, teamIDs, }); + await putStorage("onboard", true); Alert.alert("Success", "Successfully downloaded data from The Blue Alliance."); } diff --git a/assets/images/icon_512.png b/assets/images/icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..58b6a8204bd6941a74c9feb3da8524bd2038f7bd GIT binary patch literal 15690 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 literal 0 HcmV?d00001 diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f0496613331aa5f9d380876e7aa4017a2c852ac1 GIT binary patch literal 18201 zcmeIZWmr{Fw=lW^=?-ZmrE}BWol*h<5_^M4cSv`q(w&NgbSe#-Zjh7?X^^hF@Qw4H z=YD_AbMN=>zz_BwYs}GOtU1S+L};kVW1^9wfj}TkMFkm6;O{921UHU?1bot9*a!!K zu@BQppj0lN4p1tnhm{2gyF7Iuh*%M!3J(&A0+2(|`-M68QFT{8%jg%DP zU8gDg5NX^Ahk-qkuF@h;bAOQ+5%?VW=}JlX5|vy{mUdp2y}R|%^=-XP{y6dLHh}CA z!{X8L{@{Mfm-6m7_^Kpr@P_M5{BV?f^zy*l=T{5w;)ms>n0ZvIN1NZ)ETh6NGJ{)1 zDEC@}k9t3N8uuzA>=+e#_dRE*{KN{KaMDs0j&YgvtYBeF);sI(=&T+E8T{HU zQD53NF|1i;bNXqJ=6guXb)UfmcctiLd7(-xzWP?T$*!v3!)WV+z}R=J>yP6*FJApa>i@&cqRr;87!B9XElZT^UP8+MisoWhuPX1 zR$1}Jl{lo{mti<`+;d|_dA#Yn)+PPTjR&evZBheO$M!p+&dU4 z<@0M&8o?}{Q4*3t-UP=J4A9QLU{w11;SQ~B!Kmafk;(edvkfXVNHoSwl2gmB{Ncl= zCch1~shh`vHxC1b$5ZzVU?LG$KiFcW=dJ2Y{nz2hmm(ym;6J&&QrAnh^12Usihj-6 zyoAgXmOq%SRN&(E=S%5#Yeo4p*lBAT1TTZ9_TM+hP%L^h8&G3SjM!w*%%dADxwA)X zW`dsce@_b7`cXB+wmtxl%9H2ivIl59Jw>%9AQxP`xq}6M*B7=*gqFlzNZLDA(687)?G^$De*$*~G@-f*(F`vq+GC1%jI>|;L|2`fZ?rDo z6Qg}?s-tc(gb@6GHWX%H{+Gw3B-RpE~PV~n@Oh_EVbULDGTa^x{NJLs#hIV=QK`4V*=hWX<2_KJMJL^L1d4IiF zJsioXo>3G)O(4G!QMY}82xV?a4trda{JO;^%l^qFk6oY}3sQSF{W%O- zZ@ioLYYcbII1k;-{4NP9PkKNOmGM-6ui0q0${Xo)Jx>H3xVk=AKvi>ITjX-Snd9~w z>}HHnT6e}TG&$f^RmWqobO!^+H1|F%)Ubs&*+%C}WEJ!gdGVM933`z@_r7!334gZW zDg)m|NtwFmGnFC4=1HU1aGQPfnF|*<{DQZ=^7(7fGF)NPm*HIqTWwBi?M+&WLPF-Y z&lP$y|GBVG3_N5RpQ{>pRS$kHHWdA+Z(Kn$yBP}KZ9G`g-|3&tNLU- zXEOBcI?jbZK+74mWD&l8A9Dh)+s_mr^^|a`a`CVz`}3XC`-`*snO&K|ATer(GMXj5;TDdEc%kl!oe* z8Lym(cVQ5${-S#cQ}|fothtzwk(z`!Uu7mGJ5T)Ppx|w@j4VazafY}Ou@38tZ(?b& zU#KG%fAz%~lu7K)U@@yj^(f7E#xws2+1fKU2=^u>7rkJn?3y^4m(uUjY}(6*gvYK- zGqdyZyB`N0SttZENbNO-!cEgt7Ov&x-UlWq4hQc@{4lopW1n>+;Hg5DQN?XZ#1}Iv zxaF$HZ(=FyR#ca7VFCu#ekJ?e_?lDoggmlprBYAUZuR^7Fk_(v1G zqk4qcXtm46P3MsnHJ-!*wCiX+<77l^#I^C6TXgM+Y>D%#*HKRDH($OWk3bbtd^X?Q zF;vIwWB680u;Q%GMe)E9%2uG>1bs^~S(RNO^1yhX2WbUzTULU6$gLIHx5ivfDf=<< z7^JI`P)=Oym3-_@Yy(2OaGy2Ls%j4m`%{R4>EoJ zTFQ0IrwFN4b|Axy1%~Hj8tQD&8+lxNNMDPu-q^BM%TGve=k=|S$5#e}vJ*aB^W_)p zzN4<1!F$S)M>$U$(l&_-Q>ymE3;|2GU$EibmG@y@tOa*xSrMuCmUv)Masn&S9SHwXj3s z;G&PUyJ4x{pEBaVIQME=Og z`RQzoG?e>tk2hXJs}R8KTYhri9&7t3-7bYKZ9j&OS83#VOt+O>;$z9NXoj3s5_29CRYUD z+=46zc9^i8Lvy@KF`QXcrir}%&Iz|^!_~K{&;vh8|8HOyetoo|m^fZ{WE&zZ+N9V^ zL}iJ7Miy%%?I_HeXY;5*Rk4US)3kl7itY*GT%s)bwvQLb$YCzer9d}KS9#bycHiEf zq}Ry5-X@HuX8ghODo;R`RK(uFWcy(Frl8W&6J5pU(potWbKfr;qbVcx8g!dov zD=g|E(IJ;?QH>$uWKH=(>P{9aT!#s7r|VGnvYROByIEduR@#`pn3(tKU+`T$Mlv|$ z{69~c%LwCu3L3B0%hAe=9qQstvF(yN1{fW$mn6F&ng+Hm-*pbGWO? z6hA}qPr)I$m~)WISq2eSxBdCE+Hz`xM*k_DdnOM{{HJGgqWtu?Tf1$UC`xuZaTL~} zqiF*K+oAxfz<05}l^;y}8>zj}J!V|Dl9 z%c=hO5Knf16U;#%6npx-Rn^Fh?z`j54O2B(j&XPB1<@Mf;Ea%xlkrOO)e-6@WZJ(lKP~&+ny?E0({;(vBKXx?}x86~H(G$;$0Ix3ye;Q1Ai@Lybvl5h6 z!u@$!MC<`W4!Il~c`Tm=1OHvv-tv+smXH1#1MO&k%y`t7h8{I~-}LZf?UceTma-&c z3~@P9LYQWzt?kjo(HPB-q=X3v&iMd6QKVNJ=VVmEO*xk(f2Yx2lJLtNe7%fWPq zQ&-C%Zd#7bIPLs44#Uwr`J^q=XIAga)%K@g&@RV2$C=!WA6)9C34 z{2X4OU4=rZ^d?USIVt5qGAWd#S+`(MuHu*`O?4xVHz3G6-9i5L4r)$=!hP2`cVyx13ww2IU-5XTQw_f5SRBx>xwHZr zGkU5jLgx0i?4}TVGbp=8BjJ5idWmNptHD~Kq~YhG1O zRR?M4J1Ye*C#aT}nzp%@wYeaKMqCU{#6t)mU<-9IrSh<~v2zyk5T*G`R|xq01m>Wj z`itUXElQ)OszD`f?*ygdVdr7zWRvx$&!OhP*9MA6U+eyvjG@v&YpHIrXFl|&a_V?{y{?q>TK?0<=|pvZ%6e+ z)6~q~)kTzs26#^OFZpa8R8{{?-p=`-Rsih5;bH2)!NtzWVQb6rpDUbQWZeKF{}kx| zxWZW*=x=doLY?hhoy?)KZcsZH+W#a0G5_~^2UjPXzv@8DIiNOBTL9G=;L7#iSjsD^ zYW#b}lL;)XY#sit0*w9NFkP%H{>xbZO}3{ef7SU+RFqF;Ilt+M%hu=&9 z0_FM-66#J?KvbIA{HIh;q#yt(UZ?;cn9CH*#s%gFq=B0Au?c{|rfg6MC)CuOk57=B z*ZeOjh`ErQy_2me;7%)BQ%fj^gPrBy2TukUlGIQXr2(^Z{ue~U#?-|ESODaJl^w+1 z-TA*BX${fAG0b^mx|{?63S5(;GaKSSzYaI61=yTDL$ zQvq%s2pcyT%E`vVD+qWB%EiUT%frRX&Ch4S12yCRtH!^nJKI~hxSKjbB`pC>0d0T~ z`l}5U<3Ekb^xyH_-$9=&#R=wO$uKzXH|HuOWBjo>$uK(Z6h4x>kIj9}5!E*=BYM&MyyMc2al9{r+4CwLc zH}`u<5&%JUP|$YR0tiF}Qk0R@_Lx7& za&IMvet#4n9lBVmDY>NELQcf_GGUEE2pu$)ccCTKYQ-f;`}s+*L3c3t9(?5@Dofv8?^5B~-{*Js z&0siMWq%-1Ua4p*LsF>gTOO=C*-)rPGB%AoBpInX|14AkVkU2qNhGZ|3IbvDJR!K< z?SF`Ix+*k&2QLAapPIk~uA&H|n2VBtfiOVu3c;c?0Kdp1zZzpTfl_RRKW%&%xeYOw zAvE}J9Cu%4NqFt;l`+=Va?@%-%*eLx3N?GDi~NW9Xb|4LH~$H+BO4K8fPd|E+eE3) z$kV{}goaKy^fi&$9*>waOc}((O^a{l!`@~RZDX)YsE?hX-tbsySw6CxHU#U&BKNT& zI6vQgKso`*nAfG#;16A?7TT^ zy}5$0_J-bX-ygm(g$7`0WicAtX-nJ^oqUKDsge)@ZMsc1uwsmoI_NZiCGm!cNCgm8 z|A&hqWuWn#=Yogxw3imTY%#1manaW~MrDz^ekt>JmaMX}uyiQE!f8B>Vr;U-FXx?v zGBP(HMe_Q|BTLFa^L}oUIMOhT0)r${g|%FiRGXv%J#0wFwlqwr=sQLh9X?EhAN&26`Un zxSFR`Tc|%xkInQv@fKWPZDEG0K!r+5BnmM@>aikipn5`lmXq%*r#>WZHN-S{PWesj z_Pju*Ct(?a(Gjtxzh#@nZ?9OSh!Af3$`&@c2{oY-i|p} z$AEU!`XE(X4E#tHmCep5S_jMM)_YNe$fp#ezx?%z)?CT_e)_j}M4A5<9~LzLMVnN6 z!J-v=81#$Q(~8Xgx&A94qK4K|&GCO)doQ;7G=_#V=6?BPoluTNS;t3xlRyubV>)tC z-$k`H^J*d$n#oCdTe2FOGCAJV8@Ne+LlO$+^_JgP*YYlnJ$!sN$L4eezn^+1W8A1JcUWaa=IQX=NEm zE3>qnon72pGaH<`+$V-&4_v#g9oh;FJf0 zcufEPBKnz;WvnwiOGYk}fm0I#j8qWre@z>5!s0nx6swAp4)ni)C$wpyCW zcA%CB4QqG>sdirWte5HZnjrsvDil&2Jq&asp$}#~P5si)I)X)m!WD;3=VTXNOqvNe> zoO)c#jl$Tl?8SCDpzbc5CQ(lH7{gxtG;gB~#gxFqz>HJ@f#fNlOM2?hh}o`>;#I>Hnvr|v!=9!1*hXjiI>oV# z5~&qGpBK^(B}iMDh!V;zvc2kh9zVcX>&8i|wL!C8Z*2`}`;H$X9&8)@l+{m_IVzKT zG@>0^vRW)DV`p0eBWMawdmJRaHq<8{_;$n#k?AT>=O2ee8fFfO*p`vI%3zDwRh_#M ziyTJK5OMdZ#hS-eDK6Bi3tgsQk&ixl+xqFRoD4gksBy`Qb(br}y5xF;pNnI{1lhF`7nx;l*!-R%F;`fe+yE1|{mbp`-8 zddndj-k-nP4liTksdIG4i8Q2J^wcVvidJ3taN4H^KBZN!=X|+p*OF(=Q--Z5(aYQK2>oRiR>4aH-;<$ z1YD17iXUVn&7WS5OXObpFtydff|ix(qN|zB_Y(FWfr^dna2vyW+7pBy zJOA8SfJCsD3lh2ij6HQ6_0MyAx^Ar@K!YvTthembt7T_Ia{Dy2mKZ>|+7309@WJ51 z`Kx?PYG$lfy*}KRo3sCwo&QS|oUXAG zqlpq}qxHkZhGRfEX3dt&egt@NhgQ5mlT0|!EBev+>W8;xPR->iX+Er~F!az^x7^k+ zxM%Rz{tEKz1eHo_}{}AkA?MII8V7F+}M-T3PjIeCRYKn#4w=wNm zTi1*^(}SA&SSA{fjFXMZuTXE#)<)q*r0`6j!baNAvI|(3k$X8{yYwEo5kF`46ho=>z~Bhks)I7+PXlf{e?c1p?L4IDtl9*DUhL+1L5%|>w{fsl41-lqwW{vN`ojV zblLjbv!B;@P)`l42Pp!D>UQKP|Kvt8*AFFDVD4(yrpyr&^waP5JOds0x9tZrVH09VLPAf=5Cspr8B3YLhQ$IOFSz_;%AZ_r=6$;ty)cb5(J1&^jdYWJken`Z*EYr&c+5LeS6%E$wNyb=>DDYTjtUXOS z)Jw~Hk9DD^1z~aMjPQ|&MiiWwu(w;6(Ty}TnE{SSIS#Lf#y|R6GXR@&b}I4aV%<`* zb&Y*jiXvEMwqgQ0OuwWT$BmmTuKv%0uzp=J3~_BHV-K>H?X$l^%7(V{+g$Dm7R{7( z%*;uap>gva7ceY(`;z#V#E9EMe;)Kvj5Wo-AjEAKI3HGln$nqF3}ENrxmCw7Sjd_R z{WMP(feT01&y-Fe80T$k@Lgq594+R}LOQ*ZdZkpR77xXlVLhTfD^p4V_HVO=pR9)M zeL2m?*XCdqAhxepTYkO&=tF<*(Ad8pO}<&i9+@exmNgMn9p6z`L!aWU9-Jh}=#+hR zQ3M3w70Tu6fTY}EOjTbx&31&n(F7{nK;S?Ktm@g5NGq$trJK&{nl@zH3L7%%I@Xby z$g%gDl#5naq}!z%lSqJ-M!1w;lqH`$U!3Q&L@|NPS*s>E!w@I?U=PSH(vHF0?sTAn zIQ~?c?RYJ{BQGtp$A?#o$)m_s-|g&M0JalcJMWP^ai*-u z#8-a5^Lx`L7bduJV?W(b9Y!yZBSfjQ_c637&K9BsT#?~ESVU=k5+w){3RG%AWDy7J zES2}sfqnd&47AP|X*AoxLD+i%eJp?i5w*aZ6q0n^6Zd|jUD45hn}3symdpfTN$>p7 zl(oMyzyPn1NRmdA0lO1~YU(7fc7}VKf%Hdy=ffCkC%Akh!^%_y>b zA%m9kxIB6#BW*A=wgVtpGuq=SY~q9H|5ObYNzgR70f!(PczJR%S$(VIL4vawc(BI1 z_eC%Q&yU{rZUvbxdf4stj*_rI9cyHK;U6{ye9aM};RH_(Lgj&iFv;(O=BWBhG|aCgRYcFOc^?zA80#Tq}&lV&XZKmu)& z#j8zSw7G@}70eG?lk`|~owOj`+L5%1xTPD{w0KhbH%=Tk%qpFxByePl$@#kYkoD@) z2}TW~opX*J20;`$gyu(`6NX?p{iNUC8-Z?L;A?pf!nD*P%~fm@-(2a@u{d|PKNNbC z9yT`+NU(}$8PycWeX1#Tk*W%c!f`%^v%jMbNA{g9^cOidctc$$3N?cKi*CwUD#VnlsQ`cIdT!@>T$uZohn?PS?3-TR<2!mOl@81@gM%(HEu$sv?cP_juok&Ai+gaJr$Owpc+hzSknw8u@uGXI*T$ z(Lj`<)UF(X%7uveZg1jbQMqI3`>+Um%QxeTRN|0cEOsdsNrG=~=BRc#lZ9?tw|sH@ zZy8exE{@hL&Vo~?_zwGnZx>o6>h6Z|lSB7VmLVvd^+OBU0 zI$cS=VvnQk#&F5~-WT%Qhg;fd>SOfX(wwUkEa77+&SZ5J_SLY~X4jXHxa^oC&%!*K z9%y86Y5&e$5ts)1WTju77KL16^VHGW`tbtQg0!QN(Y$z3c|W^gy6aG_&= zvrq-OFaF9ur9Vx38*64Gip|IX&~aogcQ~Vf(%xU8d<6}sUnKzrklF(0neXMTQ>LH( zY>iGp4_V%Kz^;UfUOwz)>EZ--EX;U68yYe%T?w%My{uVQ)NioBioWq56$=F z4W(aPAU|H~YS70-I7y324M`yI$$1l>g^W{_@|Q*5!LsVWRa|I2%)Hda@e>4uY*?{3 zA41Ow^sYr!fQ zRD;(oh4UG!tn5-l?bxl?YAAh~gj~?oiFoiS_nA@=Gk_nUIMZ`OU~Fjoe(UvNIy$i1 zUvITatI#B;TB0Sc5m^G-5cF=no!k!(~HpYz<>o#}M+$1D}{ z{&Y%?^hZ=C_-di!KKH=J?L1Xxdm2hF2&Iw~(n4==>c^OZQ1p`QS=*Zq9O38JH!P$Z z-LyB2*hJJ8tH>|B8hKuihId@AuvPAhilKf>ii#g!hnp!oGpVH26cjAB8jJScj?n>| z#u2=v?=Ew(0&!G+hVAD33@GL`{^HCqr==QB(iqTIhP67zl#oeb-YPBU`#VqRi$e(` z7d3s+fSy6u8&m z$wE(&=e09C=xVD>2k7?RS3O36>CG-5$*ETH8@6i;B2okLj>#ooGYeTgS zd@dXdC(yn`&pV;^f0Uwc+1yJ|v4&aP<_IaleW?vCt2)2`5*-MeAxjux4rja)L9)A2 zV=;7e(<20vRnG17;77Gyf3P!j*3W8O`~GfzN1Cr7{wH~~65K8iPK-UJ_lfNVigNK( zvPmc&T_RdPq^%51f+jCs!TuBNi`NR!dLd%vcMA5mLKX2`Q-tGYs3H4&I`_ZWcv`mY z-8<|?-FnYoJHM`zO2iGp)=f59btcpK@ld>G>rPlRQvRvwvbJ+L+K`yX#I^x@{Zjq2 z$fN!m-S5)s#nMK9Vz`iBVx%Ltzlva31@#jvZapEL2zLh@VutQ-zUIRq+EcT{{*p;& zvG92L$19zYfp?yf`vM6y`&Wz_e7T5ME?f$7`zj+XNw(rUZRJ7-BW!k8TPlEyWTAiWc_ z#+A09I?k~1>RwvjMPeH9o%T>Z{@xcK*&F4DV6k6!t4dhx=YArKoVtX(^hK9-!`aT! zXArWN_B{q2-HV3r+1b4!NJ~|+t&=d%65_y|P>?dc&Ul%8tw=B@Ii-VY82NqX`kRH8 zGRo}TH=sq9m=wLgCbO4`9}+8kglX`b{}lxJl@YarGxF5p8lu=k3FYOmy)MwS40ibG zKY}-GyC6{E(C#P0%5O7h=B_!(HiS3KB)M|Qp{-Z5uP=n9L|nmIa~NWM^Jwa$I;7}j|nNt2YhwszB#sXsA)&^60$t3~(_(aq%X2ZP%QOW`*f zI$>zpLm9Q2$vjicnj;r&#@k^yp*Nz9H+{BGOSSZ|o{3~mL-|6{8+R+v1|QWT_i>n& z5aBq4TU1_xS|9p24?i>l#!e&}=q!B;F-s>|$|5!P_|nZTo+1$7@TGkZnY&tF=RKy! zZII@o*O461fq6hdYq;h@M|9~~(!26#=Mx7G+Ru#4>*iU(&7G0_fgUCuPiV%Ddn7M3 z?KSygOI%gjJ}>Ad88KB$A2-$5P-X;BrgAqdx`}7d2p2`{ua{N4T1V8ayjY!oGfyB( zkC;c-9xXD@>`IR)a_Q8><|iiWh62rFU4RvIAhzBrqE<9l0WxcH4k!zJwdR=V@J0_e7$BVqeNSTl~-jx4amu5k4~ms;yFyeyf;_ zBEjnKC77e0(&nVaR)(~7N}mmoyQ&h*!A?UKAuS;+6dsQx z1W|b)%fqVsu=}w*O5IY}lX1>t_bXJuQ?6lsEt_NxRo$F}@dStFd(&VDkuiqA`DysU zxYfxaC+P0B3vZFzK8e;kugpN$U*;?QGBH_yOXTTH8Cl(jLJG2@AsC)cN;cgVH3&etfsg1G$Xj1wm^UC0QoQXHFD3&33( z|GN6Q#!Dx`h_A|3&H};n1AKda(es|GJDPfSiB+moLc{jo(=S**^vfqid>?DV@kD2m zUX5cJJnl}!jHJO|IF3R8En}%Zo2+2Bdu%$xZ=jQ4ELO!*rvp#400a3KDtF!WNy~n? z2U;sMEw0(u&xpqAM}qe3Y_)(fTWmU7K+8|=YIsdjhb5!uvOd6(H-?D?zww-=4b^vL znt$LJww?imZN1E;vSUgvQf>WjkHa2~@jiIjdgpSm#v%G3SKvg0KJ8Gv>%3}al= zB7Q!G>uUZW?TZ9p@w1nEFa$vs(Ky=hI?a{&oKAqw27k8M(nn(iJY0%xqZ!!|(pKrca?=D_F;x^0))pchOs(rG`tkUVO7C(4@+iIoDrL#dN$H zj0BQ(LxP%8ZE%1SjM{Ill^qV)REL0ABnb|L_v2z~(UGv1>RSteBLvb+!wC(3AS}Nh z7K(Lu-$iAYHaTPi^zO7BULY_gd(NQ2m@)gZCzJtcY8;3riK{6+HX<|$g=F1KY(7*o zMjH7VO32rsbFguk=4nB;aZjK3iC81F=Rbe0cf2p8`<1Esj1iSkQqB$T>zljA4K%ff zTo_gn>&6|Q6HUQ!uTuqYF$}9Xe`=0I-^Ii>4Eyb)F&Utf+c%i0r6uHqdFXqb=(Kc* zx^PC~JwZN`fR+X2@q{(C_jM-9dJ@UpDk72F=Wfz?6-HL$fe>7vy_hL%8!h0bn}V)) z;YG4Fv@Qm;kXE00>r?Fwy|X!VGoJ&qN9V$1C9D|NVK4SD{4ZZ8uPnxUnzJ*ClOk&XK{S5&Yle41e=FVV_DI%%_VB8V8i8NLpSz^k%|dX-`^WS zC2DqU;tS=wg5sPSndC4PV%<zELQ;-t{Qx(DPfwTq;dcmTtLDZ!?7v8q9)6 zMeutHo))B-jWe27@{k2-XO6}Ie(V2Yy zmd0P&wK&S2ChJK?uMB8Vu2}`X4kCIl;&tyIIh_KsNaK*8AeN;hn3G^C=U`U*0|PF! zRXj-n#$-Hw!}fVy%8DCY_ntx+M4|D!|7p7!4a?%0vf2{>#@Ghu!TKlYt{r_)y3t;Y z!4H@O{fXYM9$Vcajo;VOyPTJYmHz1uR(2?y9hrUu;kLN_s$VHQv&MA$;keMCz-<* z5sITQ)D2eG&G>dQ+RQTr4X_FD^+)zNlBWFO;T_1;6|EwdnV=lg356g^`1g#S;qU>o zH|A*ia3w-^vdSxGVus8PRfN=p{?7{~Azo9L-7a@0u1^^Z(av<%4I(J*o7ZSZrVMdd zdURr@2WE@;wRBpk7gg{yc0{ovMPYz5zy_rQXgq&55RHvBJK=L--2@4P=ws&P2xJRJ zsa{q@PwHD}yw&lK-bYMsZI}MZ26uEPWI7-Jm)jWe-* zZM%sx5V(w!`=Q!jEE>BpVDl(>t+++tItT!+ToOx*FB-xby&^Jxl#b_*rNL);g$KjX z-zbCEtw3AbdJ6`Mp&3(W$56a*b3TwR;z73#0AE371yc7E5B}1hvYj z3*27DQmQNTZ@TCf7JH-ELMZg^Sb>(1pb%wrboR1N`p00mSKr+bj(iE)JNo+cLW4=> zN}mO_{B%)=i}=|2;pm&KN@@a|YQ0eeA;}_S++Sxe=;!4Q<6s;o?Lr+y(-+g(uPaL& zMYf)2TeTdht6xRi7b+1QE=68+0){R6QVN6DC#kLVnj|e&iP12VN+?%z?NOVd{t!^a z;p7U;`s#Blz(Euv{Idam_QxhCU?6W9t3_ohmL2bYg1&j_5V!e6-a6n$+F&PqMba*R z{;g^->aj6iJ|8dAhU`5{&4Nxjw{OUHio+=~b$$LBLL3bEA8)h-%=ozFKWIm2rgHTp zr5JBYXRIh4W~D#1G6nyPslYsZuvx^=KXFN&O9k`*(?)?v$Ck@iuf>Bsq*1D9MfY3) z0u3q2++_F`5aH9e+q2z{t3Q`25Br!DU2oj`=JhDFz6_LkvAciuAySw`cbp&a%7j+%xCg`_Ihxm+zZ9Gcm@7+U%@+tN;LjUFVjD z2><{TqPNE5$LJOYLInf;Bg$1>-B?FmUFd=jJ3QJPE)na@m%EVQ<6 z^zBaIZE$xTwRhf}Rg5uIWLb599Ub&IC}5!f29-P7!ouWPZ_H&j%f!v31zflHYdwyW zp@`v*w^Ob6?aKz+e9xBS-kk2`(mrFBy9N8~6t&9ZTw&=h4EWp_d+ zPY9;P54v;zwk^mdFa6d)SWgS&`=;rS26@Rl(MF!jlKY6BsI1nwNo}w-TZc&I%#a3NxS|} z>`w(ja=p74)vKTruQIK61peTJrs6E?fgBuAW1X*VEPjTs?Ry?vAk0Ehiq_vhbr*NIYmjn0RIo-N@pvW1z`04Lp5ZO49z6rV+hQ zOGo48ouE<5H&OyW64X0x$bDK~jBhe(7Olz~X{0672cO-9xl@pOy%}4ddS+G~9aAWK(~1+T zJD)P-F30n@2JH{l4w&xjp97Ay$Mbs^hRkGPA}Z4{l-dE#HA)8Qg_DICxY7P2ZM}Ez zrIUriH3eF0R=7g_P{eSuyg7bCAFTMg5kIRI8amXx+;Wx&&%no<84e3Y1*Ag)X3?mZkLfv*SJScfw-_!#tOKd+!%%sjE(9+OjZ-JU zKMSg?MF}$~ySk5B(n#>_wtZ&tZd^|v;Ni0=rTCnZ65ua-Y>u~3ALWy$FPfV};SlJl z0&MXl&2)i{IkD=?7h;`5fgo1g<|%+n2#NwQ9p@4P02!E$)8By%N0|P5&*8!U2j;KA zM>;qR^H&&u!{-s9V}AhW4XnOB1$J0@kIu*EipS!vbhEJ}{-G2~p<%(HU;LHDS*9;lb|DsPPX&4nZix zIXxrQ;OU-@wgSmF=fmyj6s`WT9&VS9GW5iI-38h#FI26EDJ55aJuiW^2_As={#ve? zW1WqioE(re#0b>e&7wkNWPK6j3Q{qt7*(v~hxlQCfl#&pYgCje4Vhl1hiKI{Qz?mE zxSl*cx`kNDxX-*)`fc9U<@bHtp5AuNeFv4W{V$)^vy=qlwRs6G(YJyBi?x3j^@uth zZTcJB{2hu9^mMd?!%90$aejLBYy>pbDWe6z%q$dt(h~*xr(h1y0CaV79jKG(02z4L zbhyV@Sg&A@g@=aMb{d$Yy85X$D+|D(sH?pHZTexv|Ksbg!AC}M80If$hXXqj`9G35 z0_Hz_dVX5 zl`z#BX)9dut6BlZmP-rDzx<8dc`nc_e5pOk92_mV>~n$e){idNvIky+Zp=)>k^iD^ zPzlO7H`dL;_@e}p#a|qT!p#;m1Z)R-OoTgD%xPuNe_x}9`|+Y_xe4xq*xp<+KWE=e z0a4XS*HlcePAaTnpkc~nqpg|oOsm0tFD{uRpK!Cd-wZp(cU-0rxyGn^M??n_JAW_YT60@` zgbP799n`l%PMc7qVOIkdgB-GMi|6mf;pHY|`{B@=OAPCuRjsRRpch7e^pX?S4fXF6 z8izCYZ+bPMwogbv@hS%>Aj|;C21OOi$b(;Y z$K55hBkuWcz8jdotO1mYE>|j1N%ds$XqvB}#n$GCb4UaLy_#0eW7F0RAa>iPc};m+ zcA(xOlLb9QEzveEE)dzi6Z@!aWajz0jc3cs7bSm>-L4lm=DIj;cHuw0P0>arpHW*89wkuM5@; z35p3O?an7TX}w{)9Yh|cS;RlDM)#TVPPwyFF)NE0+=W6<7NbI$afZP!XNU4?zM|iA zHv_q`UA;4VYNim5FxAM0V=p$CKfGBUAvj_TX9Y9I6KAL@5i5<fuEXVQx5``8Zn_Is(~hh*VQ*@XMReUK8cBmk(!$K+|1NT zo=z5-cHCd;)UqI4k=p6Bf9HM@0Zt9n*Q2?OD~9_zWQp;8_rp)({U)xiR=08ND#|5( z#)sBdY@``plEjMKOY8z?|K5;I-(}t=mJB_ty0M3*%M(oum&D|R2fnI=D=`RrwE6Ce zw7kD}_c|P{Sna$LJzgBwe zAZG-+L7d;t=Pkgh6g^3vJNx{81*1G2K|AVmnSX7S?o46eC1sg>(QxPHI=g9jwnU)0 z_i-xQaIU!@(q^q;vW6Hh6?mzuRNqic5dkb^mJ{==Yp-_pN|7Vwz1wvrFwI89ZhNE! z;x+Q|6VEc;}ldqN%8#r|O z5y)6LIOJ?<6kq#+JPY-d3nDy%)$^B6Aieq#I{nt!%!M7E7G}~gZgAT$+pV?tu zXGGi+uyFa6ZiR+m#|*%?%)}+;x|bAELScZ`*S?qN#5eBp(`l@I-!jtEXC+$7*tb&I ze7X9sZUm0KV#nADLj!|up9AA_t`N%erDZN%i_5D~kW8x2@&;dm5^b!dHe=GAYClK2 z!zf#c3rizKNKRuota!_Ko4mu`y+uD}B2O5*V5lcO&l|)BiZH2%n~6)6#}G~uHR5f`7`GFL(e8Y z9Y0EC*~r-fa53^>-cyiqH2c!4#Tz?c9~nJoLJwtD2J=|X{hzIvCTPxbs1U~>n73CG?u%#0dtLCMx4H+ydg(7WFOfbyWpGUOCbkBO;svT*unHdUaQdipW9V zbEN@nFRRY!HL}=J$NM%1Bo$bL8T=~Rc#{_xtdkxra= zSK*JQ>$#zuwn(nLvFGvi+Qhe~V19;Ao%_={$O>z_3aE(cQk9&HXYh7?-< v6tSp(Wb$D1lt$j$4Vv7XJgL<%zIXB`nX%vjL%X-x-ZO literal 0 HcmV?d00001 diff --git a/components/common/HorizontalBar.tsx b/components/common/HorizontalBar.tsx index f9e615a..59b4c05 100644 --- a/components/common/HorizontalBar.tsx +++ b/components/common/HorizontalBar.tsx @@ -1,14 +1,16 @@ import * as React from 'react'; import { View } from "react-native"; +import { PaletteContext } from '../../context/PaletteContext'; export type ViewProps = View['props']; export default function HorizontalBar(props: ViewProps) { + const paletteContext = React.useContext(PaletteContext); const { style, ...otherProps } = props; return (['name']; iconText?: string; iconData?: string; + iconColor?: string; + iconTba?: boolean; title: string; subtitle: string; @@ -40,6 +44,16 @@ export default function StandardButton(props: ButtonProps) { : null} + {/* Color Icon */} + {props.iconColor ? + + : null} + + {/* TBA Icon */} + {props.iconTba ? + + : null} + {/* Titles */} @@ -62,14 +76,14 @@ const styles = StyleSheet.create({ alignSelf: 'stretch', padding: 10, paddingRight: 65, - marginBottom: 5, + marginBottom: 8, borderRadius: 5 }, buttonTitle: { fontSize: 18 }, buttonIconFA: { - marginRight: 12, + marginRight: 14, width: 50, alignItems: 'center', justifyContent: 'center' @@ -78,16 +92,30 @@ const styles = StyleSheet.create({ width: 60, height: 60, margin: -10, - marginRight: 12, - borderTopLeftRadius: 1, - borderBottomLeftRadius: 1, + marginRight: 14, + borderTopLeftRadius: 5, + borderBottomLeftRadius: 5, }, buttonIconTXT: { fontSize: 20, fontWeight: "bold", textAlign: "center", paddingTop: 5, - marginRight: 12, + marginRight: 14, width: 50, - } + }, + buttonIconTBA: { + paddingTop: 5, + marginRight: 14 + 15, + marginLeft: 15, + width: 20, + height: 32 + }, + buttonIconColor: { + width: 45, + height: 45, + marginRight: 14, + borderTopLeftRadius: 1, + borderBottomLeftRadius: 1, + }, }); \ No newline at end of file diff --git a/context/PaletteContext.tsx b/context/PaletteContext.tsx index 7197aaf..ba91636 100644 --- a/context/PaletteContext.tsx +++ b/context/PaletteContext.tsx @@ -24,15 +24,21 @@ export const LIGHT_PALETTE: Palette = { }; export const PaletteContext = React.createContext({ - palette: {} as Palette, + palette: {} as any, setPalette: (newTheme: Palette) => { } }); export function PaletteProvider(props: { children: React.ReactNode }) { const [palette, setPalette] = React.useState(DARK_PALETTE); + const [version, setVersion] = React.useState(0); + + const setPaletteFromContext = (newPalette: Palette) => { + setPalette(newPalette); + setVersion(v => v + 1); + } return ( - + {props.children} ) diff --git a/css.d.ts b/css.d.ts new file mode 100644 index 0000000..b8bae3e --- /dev/null +++ b/css.d.ts @@ -0,0 +1 @@ +declare module '*.png'; \ No newline at end of file diff --git a/hooks/useCompressedData.ts b/hooks/useCompressedData.ts index dbc1684..cd8d7aa 100644 --- a/hooks/useCompressedData.ts +++ b/hooks/useCompressedData.ts @@ -1,4 +1,3 @@ -import LZString from "lz-string"; import React, { useEffect, useState } from "react"; import { ToastAndroid } from "react-native"; import { ScoutingData } from "../types/TemplateTypes"; @@ -10,12 +9,19 @@ export interface ExportData { exportID: string, scoutingData: Record } +export function getChecksum(data: Record) { + const jsonData = JSON.stringify(data); + return jsonData.split("").reduce((a, b) => { + a = ((a << 5) - a) + b.charCodeAt(0); + return a & a; + }, 0); +} -export function useCompressedData() { +export function useJsonData() { const [event] = useEvent(); const [data, setData] = useState(""); - const compressData = async () => { + const compressJsonData = async () => { const teams = await Promise.all(event.teamIDs.map(getTeam)); let scoutingData: Record = {}; for (const team of teams) { @@ -24,66 +30,61 @@ export function useCompressedData() { scoutingData[team.id] = team.scoutingData; } const outputData: ExportData = { - exportID: Math.random().toString(36).slice(2), + exportID: getChecksum(scoutingData).toString(), eventID: event.id, scoutingData }; const jsonData = JSON.stringify(outputData); - const compressedData = LZString.compressToEncodedURIComponent(jsonData); - setData(compressedData); + setData(jsonData); } useEffect(() => { - compressData(); + compressJsonData(); }, [event]); - return data; -} - -export function decompressData(data: string): ExportData | undefined { - const decompressedData = LZString.decompressFromEncodedURIComponent(data); - if (decompressedData) { - const data = JSON.parse(decompressedData) as ExportData; - return data; - } - return undefined; + return event.id === "bogus" ? "" : data; } -export function useDecompressedData() { +export function useDataImporter() { const [event] = useEvent(); const [importedIDs, setImportedIDs] = React.useState([] as string[]); - const importCompressedData = async (data: string) => { - const decompressedData = decompressData(data); - if (!decompressedData) { - ToastAndroid.show("Invalid QR code", ToastAndroid.SHORT); - return; - } - if (importedIDs.includes(decompressedData.exportID)) { - return; - } - importedIDs.push(decompressedData.exportID); - setImportedIDs(importedIDs); - if (decompressedData.eventID !== event.id) { - ToastAndroid.show("Invalid Event", ToastAndroid.SHORT); - return; - } + const importJsonData = async (data: string) => { + try { + const decompressedData = JSON.parse(data) as ExportData; + if (!decompressedData) { + ToastAndroid.show("Invalid QR code", ToastAndroid.SHORT); + return; + } + if (importedIDs.includes(decompressedData.exportID)) { + return; + } + importedIDs.push(decompressedData.exportID); + setImportedIDs(importedIDs); + if (decompressedData.eventID !== event.id) { + ToastAndroid.show("Invalid Event", ToastAndroid.SHORT); + return; + } - const teamIDs = Object.keys(decompressedData.scoutingData); - for (const teamID of teamIDs) { - const team = await getTeam(teamID); - if (team) { - const scoutingData = decompressedData.scoutingData[teamID]; - for (let scout of scoutingData) { - if (team.scoutingData.findIndex(s => s.matchID === scout.matchID) === -1) { - team.scoutingData.push(scout); + const teamIDs = Object.keys(decompressedData.scoutingData); + for (const teamID of teamIDs) { + const team = await getTeam(teamID); + if (team) { + const scoutingData = decompressedData.scoutingData[teamID]; + for (let scout of scoutingData) { + if (team.scoutingData.findIndex(s => s.matchID === scout.matchID) === -1) { + team.scoutingData.push(scout); + } } + await setTeam(team); } - await setTeam(team); } + ToastAndroid.show("Imported " + teamIDs.length + " team(s).", ToastAndroid.SHORT); + } + catch (e) { + ToastAndroid.show("Invalid Data Import", ToastAndroid.SHORT); } - ToastAndroid.show("Imported " + teamIDs.length + " team(s).", ToastAndroid.SHORT); }; - return [importCompressedData]; + return importJsonData; } \ No newline at end of file diff --git a/hooks/useStorage.ts b/hooks/useStorage.ts index c2cf98f..889aaa3 100644 --- a/hooks/useStorage.ts +++ b/hooks/useStorage.ts @@ -20,6 +20,8 @@ export default function useStorage(id: string, defaultValue: Type): [Type, const jsonData = await AsyncStorage.getItem(id); if (jsonData) setData(JSON.parse(jsonData) as Type); + else + setData(defaultValue); }; useEffect(() => { getData(); @@ -74,9 +76,8 @@ export async function getStorage(id: string) { export async function clearStorage() { // AsyncStorage const keys = await AsyncStorage.getAllKeys(); - await AsyncStorage.clear(); for (let key of keys) { - console.log(key); + await AsyncStorage.removeItem(key); eventEmitter.emit(key); } diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx index 89915bd..a557eb4 100644 --- a/navigation/RootNavigator.tsx +++ b/navigation/RootNavigator.tsx @@ -1,5 +1,5 @@ import { DarkTheme, NavigationContainer } from "@react-navigation/native"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { createStackNavigator } from "@react-navigation/stack"; import * as React from 'react'; import MediaScreen from "../components/containers/MediaScreen"; import { PaletteContext } from "../context/PaletteContext"; @@ -8,6 +8,10 @@ import TeamSelectScreen from "../screens/DefaultTeam/TeamSelectScreen"; import MatchScreen from "../screens/Match/MatchScreen"; import ScoutingScreen from "../screens/Scout/ScoutingScreen"; import AboutScreen from "../screens/Settings/AboutScreen"; +import { ColorPickerScreen } from "../screens/Settings/ColorPicker"; +import DownloadScreen from "../screens/Settings/DownloadScreen"; +import OnboardingScreen from "../screens/Settings/OnboardingScreen"; +import { PaletteScreen } from "../screens/Settings/PaletteScreen"; import RegionalScreen from "../screens/Settings/RegionalScreen"; import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; @@ -15,28 +19,66 @@ import YearScreen from "../screens/Settings/YearScreen"; import ExportQRScreen from "../screens/Sharing/ExportQRScreen"; import ImportQRScreen from "../screens/Sharing/ImportQRScreen"; import TeamScreen from "../screens/Team/TeamScreen"; -import TabNavigator from "./TabNavigator"; -const Stack = createNativeStackNavigator(); +const horizontalAnimation = ({ current, layouts }: any) => { + return { + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + }, + }; +}; + +const Stack = createStackNavigator(); export default function RootNavigator() { const paletteContext = React.useContext(PaletteContext); + + const BlitzTheme = { + ...DarkTheme, + colors: { + ...DarkTheme.colors, + background: paletteContext.palette.background, + } + } + return ( - + - + - + + @@ -45,6 +87,8 @@ export default function RootNavigator() { + + diff --git a/navigation/RootParamList.tsx b/navigation/RootParamList.tsx index cfcf434..e61947d 100644 --- a/navigation/RootParamList.tsx +++ b/navigation/RootParamList.tsx @@ -3,19 +3,23 @@ import { TemplateType } from "../types/TemplateTypes"; // Params export type RootNavParamList = { Drawer: undefined, + Onboarding: undefined, Match: { matchID: string }, Team: { teamID: string }, Media: { mediaPath: string, onDelete: (mediaPath: string) => void }, Year: undefined, Regional: { year: number }, + Download: { eventID: string }, EditTemplate: { templateType: TemplateType }, ElementChooser: { templateType: TemplateType }, TeamSelect: { matchID: string }, Scout: { teamID: string, matchID: string, templateType: TemplateType }, DefaultTeam: undefined, - ExportQR: undefined, + ExportQR: { data: string }, ImportQR: undefined, - About: undefined + About: undefined, + Palette: undefined, + ColorPicker: { defaultColor: string, onPick: (color: string) => void } }; // Default diff --git a/navigation/TabNavigator.tsx b/navigation/TabNavigator.tsx index 3ccd31b..14cebdf 100644 --- a/navigation/TabNavigator.tsx +++ b/navigation/TabNavigator.tsx @@ -9,6 +9,7 @@ import SharingScreen from '../screens/Sharing/SharingScreen'; import TeamsScreen from '../screens/Teams/TeamsScreen'; const Tab = createBottomTabNavigator(); + export default function TabNavigator() { const paletteContext = React.useContext(PaletteContext); @@ -16,7 +17,7 @@ export default function TabNavigator() { + background={TouchableNativeFeedback.Ripple(paletteContext.palette.navigation, false)}> {children} ); @@ -42,7 +43,7 @@ export default function TabNavigator() { marginTop: -8 }, unmountOnBlur: false, - tabBarButton: buttonNativeFeedback, + tabBarButton: buttonNativeFeedback }}> { match ? TBA.openMatch(match.id) : null }} /> diff --git a/screens/Matches/MatchesScreen.tsx b/screens/Matches/MatchesScreen.tsx index 549bdd7..d2d0f7a 100644 --- a/screens/Matches/MatchesScreen.tsx +++ b/screens/Matches/MatchesScreen.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; -import { ActivityIndicator, Platform, ToastAndroid } from 'react-native'; +import { ActivityIndicator, Platform, ToastAndroid, View } from 'react-native'; import { DownloadMatches } from '../../api/TBAAdapter'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; import Text from '../../components/text/Text'; +import { PaletteContext } from '../../context/PaletteContext'; import useEvent from '../../hooks/useEvent'; import MatchBanner from './MatchBanner'; export default function MatchesScreen() { + const paletteContext = React.useContext(PaletteContext); const [event, setEvent] = useEvent(); - const isLoaded = true; // TODO: Update this to check if the event is loaded const onRefresh = async () => { if (Platform.OS !== "android") @@ -29,15 +30,21 @@ export default function MatchesScreen() { } }; - return ( - - Matches - {event.matchIDs.length > 0 ? - event.matchIDs.map((teamID) => ) : - isLoaded ? - There is no match data yet. Download it under the settings tab. : - - } - - ); + if (event.id === "bogus") + return ( + + + + ); + else + return ( + + Matches + {event.matchIDs.length <= 0 ? + This event has no matches posted yet. Pull down to refresh. + : + event.matchIDs.map((teamID) => ) + } + + ); } \ No newline at end of file diff --git a/screens/Settings/AboutScreen.tsx b/screens/Settings/AboutScreen.tsx index e0570a7..67a737c 100644 --- a/screens/Settings/AboutScreen.tsx +++ b/screens/Settings/AboutScreen.tsx @@ -48,7 +48,7 @@ export default function AboutScreen() { - Made with by Team 5148, New Berlin Blitz + Made with by Team 5148, New Berlin Blitz https://team5148.org/ diff --git a/screens/Settings/ColorPicker.tsx b/screens/Settings/ColorPicker.tsx new file mode 100644 index 0000000..e7cf06e --- /dev/null +++ b/screens/Settings/ColorPicker.tsx @@ -0,0 +1,56 @@ +import { useNavigation } from "@react-navigation/native"; +import * as React from "react"; +import { StyleSheet, TextInput, View } from "react-native"; +import Button from "../../components/common/Button"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import ScrollContainer from "../../components/containers/ScrollContainer"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; +import { PaletteContext } from "../../context/PaletteContext"; + +export function ColorPickerScreen({ route }: any) { + const paletteContext = React.useContext(PaletteContext); + const [color, setColor] = React.useState(route.params.defaultColor); + const navigator = useNavigation(); + const onPick = route.params.onPick as (color: string) => void; + + const onSubmit = () => { + onPick(color); + navigator.goBack(); + } + + return ( + + Color Picker + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + textInput: { + borderRadius: 10, + padding: 10, + marginBottom: 10, + }, + button: { + marginTop: 5, + borderRadius: 5 + } +}); \ No newline at end of file diff --git a/screens/Settings/DownloadScreen.tsx b/screens/Settings/DownloadScreen.tsx new file mode 100644 index 0000000..3790738 --- /dev/null +++ b/screens/Settings/DownloadScreen.tsx @@ -0,0 +1,61 @@ +import { useNavigation } from "@react-navigation/core"; +import React from "react"; +import { StyleSheet, View } from "react-native"; +import { DownloadEvent } from "../../api/TBAAdapter"; +import StandardButton from "../../components/common/StandardButton"; +import ScrollContainer from "../../components/containers/ScrollContainer"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; +import { PaletteContext } from "../../context/PaletteContext"; + +export default function DownloadScreen({ route }: any) { + const paletteContext = React.useContext(PaletteContext); + const navigator = useNavigation(); + const [downloadStatus, setDownloadStatus] = React.useState(""); + const eventID = route.params.eventID; + + const downloadEvent = (includeMedia: boolean) => { + DownloadEvent(eventID, includeMedia, setDownloadStatus).then(() => { + while (navigator.canGoBack()) + navigator.goBack(); + }); + } + + return ( + + + + {downloadStatus === "" ? + + Download Event + { downloadEvent(false); }} /> + { downloadEvent(true); }} /> + + : + + Downloading Event... + {downloadStatus} + + } + + + ); +} + +const styles = StyleSheet.create({ + container: { + width: "100%", + height: "100%" + }, + title: { + marginBottom: 15 + } +}); diff --git a/screens/Settings/DownloadingModal.tsx b/screens/Settings/DownloadingModal.tsx deleted file mode 100644 index 8e9be80..0000000 --- a/screens/Settings/DownloadingModal.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { Modal, StyleSheet, View } from "react-native"; -import DarkBackground from "../../components/common/DarkBackground"; -import Subtitle from "../../components/text/Subtitle"; -import Title from "../../components/text/Title"; - -interface ModalProps { - status: string; -} - -export default function DownloadingModal(props: ModalProps) { - - // Return Modal - return ( - - - - - - - Downloading - {props.status} - - - - ); -} - -const styles = StyleSheet.create({ - modal: { - backgroundColor: "#0e0e0e", - position: "absolute", - top: "40%", - width: "100%", - height: 100, - borderRadius: 10, - paddingTop: 30, - paddingLeft: 20, - paddingRight: 20, - paddingBottom: 80, - } -}); diff --git a/screens/Settings/OnboardingScreen.tsx b/screens/Settings/OnboardingScreen.tsx new file mode 100644 index 0000000..2dccb0a --- /dev/null +++ b/screens/Settings/OnboardingScreen.tsx @@ -0,0 +1,134 @@ +import { useNavigation } from "@react-navigation/native"; +import * as React from "react"; +import { Image, StyleSheet, View } from "react-native"; +import PagerView from 'react-native-pager-view'; +import Animated from "react-native-reanimated"; +import logo from "../../assets/images/logo.png"; +import tbalamp from "../../assets/images/tba_lamp.png"; +import Button from "../../components/common/Button"; +import Subtitle from "../../components/text/Subtitle"; +import Text from "../../components/text/Text"; +import Title from "../../components/text/Title"; +import useStorage from "../../hooks/useStorage"; +import TabNavigator from "../../navigation/TabNavigator"; + +const AnimatedPagerView = Animated.createAnimatedComponent(PagerView); + +export default function OnboardingScreen() { + const [hasOnboarded] = useStorage("onboard", false); + const scrollOffsetAnimatedValue = React.useRef(new Animated.Value(0)).current; + const positionAnimatedValue = React.useRef(new Animated.Value(0)).current; + const translateX = Animated.add( + scrollOffsetAnimatedValue.interpolate({ + inputRange: [0, 2], + outputRange: [0, 30], + }), + positionAnimatedValue.interpolate({ + inputRange: [0, 2], + outputRange: [0, 30], + }) + ); + const navigator = useNavigation(); + if (hasOnboarded) + return ( + + ); + else + return ( + + { + console.log(`Position: ${position} Offset: ${offset}`); + }, + useNativeDriver: false, + })}> + + + + Welcome to + Blitz Scouter + Blitz Scouter is a simple, easy to use scouting app for use at a FIRST{"\u00AE"} Robotics Competition. + + + + + Import From + The Blue Alliance + Import Teams and Event data while your online, and continue to use them offline. + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + pagerView: { + flex: 1 + }, + page: { + flex: 1, + marginTop: 50, + padding: 20, + justifyContent: "center", + alignContent: "center", + }, + subtitle: { + fontSize: 20, + marginTop: 30, + textAlign: "center" + }, + title: { + marginTop: 0, + textAlign: "center" + }, + text: { + marginTop: 10, + fontSize: 16, + textAlign: "center" + }, + img: { + alignSelf: "center", + }, + button: { + marginTop: 50, + borderRadius: 5, + borderColor: "white", + backgroundColor: "#2b3da1" + }, + pageDot: { + position: "absolute", + bottom: 10, + right: 10, + height: 8, + width: 8, + borderRadius: 4 + } +}); \ No newline at end of file diff --git a/screens/Settings/PaletteScreen.tsx b/screens/Settings/PaletteScreen.tsx new file mode 100644 index 0000000..e35b14a --- /dev/null +++ b/screens/Settings/PaletteScreen.tsx @@ -0,0 +1,80 @@ +import { useNavigation } from "@react-navigation/native"; +import * as React from "react"; +import HorizontalBar from "../../components/common/HorizontalBar"; +import StandardButton from "../../components/common/StandardButton"; +import ScrollContainer from "../../components/containers/ScrollContainer"; +import Subtitle from "../../components/text/Subtitle"; +import Title from "../../components/text/Title"; +import { PaletteContext } from "../../context/PaletteContext"; + +export function PaletteScreen() { + const navigator = useNavigation(); + const paletteContext = React.useContext(PaletteContext); + + const promptForColor = (swatch: string) => { + navigator.navigate("ColorPicker", { + defaultColor: paletteContext.palette[swatch], + onPick: (color: string) => { + paletteContext.palette[swatch] = color; + paletteContext.setPalette(paletteContext.palette); + } + }); + } + + return ( + + Color Palette + Change the color palette to match your team + + + { promptForColor("background"); }} + /> + { promptForColor("button"); }} + /> + { promptForColor("navigation"); }} + /> + { promptForColor("navigationSelected"); }} + /> + { promptForColor("navigationText"); }} + /> + { promptForColor("navigationTextSelected"); }} + /> + { promptForColor("textPrimary"); }} + /> + { promptForColor("textSecondary"); }} + /> + + ); +} \ No newline at end of file diff --git a/screens/Settings/RegionalScreen.tsx b/screens/Settings/RegionalScreen.tsx index fa42d44..22057a4 100644 --- a/screens/Settings/RegionalScreen.tsx +++ b/screens/Settings/RegionalScreen.tsx @@ -3,29 +3,19 @@ import React, { useEffect } from "react"; import { Alert, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; import TBA from "../../api/TBA"; -import { DownloadEvent } from "../../api/TBAAdapter"; import Button from "../../components/common/Button"; import ScrollContainer from "../../components/containers/ScrollContainer"; import Text from "../../components/text/Text"; import { PaletteContext } from "../../context/PaletteContext"; import { TBAEvent } from "../../types/TBAModels"; -import DownloadingModal from "./DownloadingModal"; export default function RegionalScreen({ route }: any) { const paletteContext = React.useContext(PaletteContext); const navigator = useNavigation(); const [searchTerm, updateSearch] = React.useState(""); const [regionalList, updateRegionals] = React.useState([] as TBAEvent[]); - const [downloadStatus, setDownloadStatus] = React.useState(""); const year = route.params.year; - const downloadEvent = (eventID: string) => { - DownloadEvent(eventID, setDownloadStatus).then(() => { - navigator.goBack(); - navigator.goBack(); - }); - } - useEffect(() => { TBA.getEvents(year).then((events) => { if (events) @@ -51,7 +41,7 @@ export default function RegionalScreen({ route }: any) { regionalsDisplay.push( ) @@ -95,6 +95,10 @@ const styles = StyleSheet.create({ height: "100%", width: "100%" }, + trashButton: { + alignSelf: "flex-end", + margin: 11 + }, addButton: { height: 50, width: 50, diff --git a/screens/Sharing/ExportQRScreen.tsx b/screens/Sharing/ExportQRScreen.tsx index 548d296..152b880 100644 --- a/screens/Sharing/ExportQRScreen.tsx +++ b/screens/Sharing/ExportQRScreen.tsx @@ -1,10 +1,11 @@ +import LZString from 'lz-string'; import * as React from 'react'; import { Dimensions, StyleSheet, View } from 'react-native'; import QRCode from 'react-qr-code'; -import { useCompressedData } from '../../hooks/useCompressedData'; -export default function ExportQRScreen() { - const data = useCompressedData(); +export default function ExportQRScreen({ route }: any) { + const jsonData = route.params.data; + const compressedData = LZString.compressToEncodedURIComponent(jsonData); const windowSize = Dimensions.get("window"); const qrSize = Math.min(windowSize.width, windowSize.height); @@ -12,10 +13,9 @@ export default function ExportQRScreen() { return ( - @@ -25,12 +25,8 @@ export default function ExportQRScreen() { const styles = StyleSheet.create({ container: { - position: "absolute", - justifyContent: "center", + flex: 1, backgroundColor: "black", - top: 0, - left: 0, - bottom: 0, - right: 0 + justifyContent: "center", } }); diff --git a/screens/Sharing/ImportQRScreen.tsx b/screens/Sharing/ImportQRScreen.tsx index a3e2383..e811044 100644 --- a/screens/Sharing/ImportQRScreen.tsx +++ b/screens/Sharing/ImportQRScreen.tsx @@ -1,14 +1,17 @@ import { BarCodeEvent, BarCodeScanner } from 'expo-barcode-scanner'; +import LZString from 'lz-string'; import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import { useDecompressedData } from '../../hooks/useCompressedData'; +import { useDataImporter } from '../../hooks/useCompressedData'; export default function ImportQRScreen() { - const [importCompressedData] = useDecompressedData(); + const importJsonData = useDataImporter(); const onScan = async (e: BarCodeEvent) => { const compressedData = e.data; - importCompressedData(compressedData); + const jsonData = LZString.decompressFromEncodedURIComponent(compressedData); + if (jsonData) + importJsonData(jsonData); }; return ( diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index c761caf..38975bc 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -1,16 +1,44 @@ import { useNavigation } from '@react-navigation/native'; +import * as DocumentPicker from 'expo-document-picker'; +import * as FileSystem from 'expo-file-system'; +import * as Sharing from 'expo-sharing'; import * as React from 'react'; import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; +import { useDataImporter, useJsonData } from '../../hooks/useCompressedData'; export default function SharingScreen() { const navigator = useNavigation(); + const data = useJsonData(); + const importJsonData = useDataImporter(); + + const exportJson = async () => { + const path = FileSystem.documentDirectory + "data.json"; + await FileSystem.writeAsStringAsync(path, data, { encoding: FileSystem.EncodingType.UTF8 }); + Sharing.shareAsync(path); + } + + const importJson = async () => { + const result = await DocumentPicker.getDocumentAsync({ type: 'application/json', copyToCacheDirectory: true }); + if (result.type === "success") { + /* + https://github.com/expo/expo/issues/14335 + + TL;DR: result.uri is not the correct file path. + The following code is a workaround for this issue. + */ + const fileName = result.uri.split("/").pop(); + const path = FileSystem.cacheDirectory + 'DocumentPicker/' + fileName; + + const jsonData = await FileSystem.readAsStringAsync(path, { encoding: FileSystem.EncodingType.UTF8 }); + importJsonData(jsonData); + } + } return ( - Sharing {/* QR Codes */} @@ -18,7 +46,7 @@ export default function SharingScreen() { iconType={"qr-code"} title={"Show QRCode"} subtitle={"Export Scouting Data"} - onPress={() => { navigator.navigate("ExportQR"); }} /> + onPress={() => { navigator.navigate("ExportQR", { data }); }} /> { }} /> + onPress={() => { exportJson(); }} /> { importJson(); }} /> + + {/* { }} /> - - {/* Cloud Save */} - {/* - {}} /> - - {}} /> - */} - - {/* Hardware Sync */} - { }} /> - { }} /> - { }} /> { }} /> + subtitle={"Import Scouting Data"} + onPress={() => { }} />*/} ); diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 4278cd2..2b65997 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -19,8 +19,6 @@ export default function TeamScreen({ route }: any) { const [team, setTeam] = useTeam(route.params.teamID); const stats = useStats(team.id); - console.log(stats); - const generateID = () => { return team.id + "_" + Math.random().toString(36).slice(2); } @@ -139,7 +137,7 @@ export default function TeamScreen({ route }: any) { onPress={() => { }} /> { team ? TBA.openTeam(team.number) : null }} /> diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index ce38c18..0bd32ca 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; -import { ActivityIndicator, Platform, ToastAndroid } from 'react-native'; +import { ActivityIndicator, Platform, ToastAndroid, View } from 'react-native'; import { DownloadTeams } from '../../api/TBAAdapter'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; import Text from '../../components/text/Text'; +import { PaletteContext } from '../../context/PaletteContext'; import useEvent from '../../hooks/useEvent'; import TeamBanner from './TeamBanner'; export default function TeamsScreen() { + const paletteContext = React.useContext(PaletteContext); const [event, setEvent] = useEvent(); - const isLoaded = true; // TODO: Update this to check if the event is loaded const onRefresh = async () => { if (Platform.OS !== "android") @@ -29,17 +30,21 @@ export default function TeamsScreen() { } }; - return ( - - Teams - {event.teamIDs.length > 0 ? - event.teamIDs.map((teamID) => ) : - isLoaded ? - There is no team data yet. Download it under the settings tab. : - - } - - - - ); + if (event.id === "bogus") + return ( + + + + ); + else + return ( + + Teams + {event.teamIDs.length <= 0 ? + This event has no teams posted yet. Pull down to refresh. + : + event.teamIDs.map((teamID) => ) + } + + ); } diff --git a/yarn-error.log b/yarn-error.log index 9ef239e..5b40c46 100644 --- a/yarn-error.log +++ b/yarn-error.log @@ -1,8 +1,8 @@ Arguments: - C:\Program Files\nodejs\node.exe C:\Users\aus43\AppData\Roaming\npm\node_modules\yarn\bin\yarn.js add @react-navigation/material-bottom-tabsyarn add react-native-paper + C:\Program Files\nodejs\node.exe C:\Users\aus43\AppData\Roaming\npm\node_modules\yarn\bin\yarn.js add @types/react-native-svg-animations PATH: - C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\VMware\VMware Player\bin\;C:\Program Files\National Instruments\Shared\OpenVINO\;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Azure Data Studio\bin;C:\Program Files\Git\cmd;C:\Users\Public\wpilib\2020\roborio\bin;C:\Users\Public\wpilib\2020\frccode;C:\ProgramData\chocolatey\bin;C:\Program Files\CMake\bin;C:\Program Files\PuTTY\;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;C:\Program Files\Wiimm\SZS;C:\Program Files\nodejs\;C:\Program Files\Amazon\AWSCLIV2\;C:\Ruby27-x64\bin;C:\Users\aus43\AppData\Local\Programs\Python\Python39\Scripts\;C:\Users\aus43\AppData\Local\Programs\Python\Python39\;C:\Users\aus43\AppData\Local\Microsoft\WindowsApps;C:\Users\aus43\.dotnet\tools;C:\Users\aus43\AppData\Local\Programs\Microsoft VS Code\bin;C:\opencv\build\x64\vc15\bin;C:\Program Files (x86)\Nmap;C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin;C:\Users\aus43\AppData\Local\gitkraken\bin;C:\Users\aus43\.dotnet\tools;C:\Users\aus43\AppData\Local\atom\bin;C:\Users\aus43\Documents\abmatt\bin;C:\Users\aus43\AppData\Roaming\npm;C:\Users\aus43\Desktop\Path + C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\VMware\VMware Player\bin\;C:\Program Files\National Instruments\Shared\OpenVINO\;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files\Azure Data Studio\bin;C:\Program Files\Git\cmd;C:\Users\Public\wpilib\2020\roborio\bin;C:\Users\Public\wpilib\2020\frccode;C:\ProgramData\chocolatey\bin;C:\Program Files\CMake\bin;C:\Program Files\PuTTY\;C:\Program Files (x86)\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\Tools\Binn\;C:\Program Files\Microsoft SQL Server\150\DTS\Binn\;C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\;C:\Program Files\Wiimm\SZS;C:\Program Files\nodejs\;C:\Program Files\Amazon\AWSCLIV2\;C:\Program Files\RedHat\java-11-openjdk-11.0.13-1\bin;C:\Users\aus43\AppData\Local\Programs\Python\Python39\Scripts\;C:\Users\aus43\AppData\Local\Programs\Python\Python39\;C:\Users\aus43\AppData\Local\Microsoft\WindowsApps;C:\Users\aus43\.dotnet\tools;C:\Users\aus43\AppData\Local\Programs\Microsoft VS Code\bin;C:\Program Files\Azure Data Studio\bin;C:\opencv\build\x64\vc15\bin;C:\Users\Public\wpilib\2020\roborio\bin;C:\Users\Public\wpilib\2020\frccode;C:\Program Files (x86)\Nmap;C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin;C:\Users\aus43\AppData\Local\gitkraken\bin;C:\Users\aus43\.dotnet\tools;C:\Program Files\Wiimm\SZS;C:\Users\aus43\Documents\abmatt\bin;C:\Users\aus43\AppData\Roaming\npm;C:\Users\aus43\Desktop\Path;C:\Users\aus43\AppData\Local\Android\Sdk\platform-tools;C:\Users\aus43\Documents\flutter\bin;C:\Users\aus43\AppData\Local\GitHubDesktop\bin Yarn version: 1.22.10 @@ -14,7 +14,7 @@ Platform: win32 x64 Trace: - Error: https://registry.yarnpkg.com/@react-navigation%2fmaterial-bottom-tabsyarn: Not found + Error: https://registry.yarnpkg.com/@types%2freact-native-svg-animations: Not found at Request.params.callback [as _callback] (C:\Users\aus43\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:66988:18) at Request.self.callback (C:\Users\aus43\AppData\Roaming\npm\node_modules\yarn\lib\cli.js:140662:22) at Request.emit (node:events:394:28) @@ -42,31 +42,47 @@ npm manifest: }, "dependencies": { "@expo/vector-icons": "^12.0.0", + "@react-native-async-storage/async-storage": "^1.15.8", "@react-native-picker/picker": "^2.1.0", - "@react-navigation/bottom-tabs": "^6.0.5", - "@react-navigation/material-bottom-tabs": "^6.0.7", + "@react-navigation/bottom-tabs": "^6.0.9", + "@react-navigation/drawer": "^6.1.8", "@react-navigation/native": "^6.0.2", "@react-navigation/native-stack": "^6.1.0", + "@types/lz-string": "^1.3.34", + "eventemitter3": "^4.0.7", "expo": "~42.0.1", + "expo-application": "~3.2.0", "expo-asset": "~8.3.2", + "expo-barcode-scanner": "~10.2.2", + "expo-checkbox": "~1.0.3", "expo-constants": "~11.0.1", + "expo-file-system": "~11.1.3", "expo-font": "~9.2.1", + "expo-image-picker": "~10.2.2", "expo-linking": "~2.3.1", + "expo-sensors": "~10.2.2", + "expo-sharing": "~9.2.1", "expo-splash-screen": "~0.11.2", "expo-status-bar": "~1.0.4", "expo-web-browser": "~9.2.0", + "lz-string": "^1.4.4", + "npm": "^8.3.0", "react": "16.13.1", "react-dom": "16.13.1", "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", "react-native-gesture-handler": "~1.10.2", - "react-native-qrcode-generator": "1.2.2", + "react-native-global-props": "^1.1.5", + "react-native-nfc-manager": "^3.11.4", "react-native-reanimated": "~2.2.0", "react-native-safe-area-context": "3.2.0", "react-native-screens": "~3.4.0", "react-native-svg": "^12.1.1", + "react-native-svg-animations": "^0.2.6", "react-native-web": "~0.13.12", "react-native-webview": "9.0.1", - "react-qr-code": "^2.0.2" + "react-navigation-tabs": "^2.11.1", + "react-qr-code": "^2.0.2", + "version": "^0.1.2" }, "devDependencies": { "@babel/core": "^7.9.0", @@ -1279,7 +1295,7 @@ Lockfile: xcode "^3.0.1" xml2js "^0.4.23" - "@expo/config-plugins@3.1.0", "@expo/config-plugins@^3.0.0": + "@expo/config-plugins@3.1.0", "@expo/config-plugins@^3.0.0", "@expo/config-plugins@^3.0.6": version "3.1.0" resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-3.1.0.tgz" integrity sha512-V5qxaxCAExBM0TXmbU1QKiZcAGP3ecu7KXede8vByT15cro5PkcWu2sSdJCYbHQ/gw6Vf/i8sr8gKlN8V8TSLg== @@ -1513,6 +1529,11 @@ Lockfile: lodash.pick "^4.4.0" lodash.template "^4.5.0" + "@gar/promisify@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" + integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz" @@ -1545,6 +1566,11 @@ Lockfile: dependencies: "@hapi/hoek" "^8.3.0" + "@isaacs/string-locale-compare@*", "@isaacs/string-locale-compare@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" + integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" @@ -2095,6 +2121,166 @@ Lockfile: "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" + "@npmcli/arborist@*", "@npmcli/arborist@^4.0.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-4.2.0.tgz#722b114376645ed3c78e1cef62969eb7993df2a0" + integrity sha512-uQmPnwuhNHkN8IgCwda6wXklUf3BUfuuIUFuJMT224frUS5u2AuEAeCr2fiRVsz7AHcW3iSDai2j3WhVFlfbRQ== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/installed-package-contents" "^1.0.7" + "@npmcli/map-workspaces" "^2.0.0" + "@npmcli/metavuln-calculator" "^2.0.0" + "@npmcli/move-file" "^1.1.0" + "@npmcli/name-from-folder" "^1.0.1" + "@npmcli/node-gyp" "^1.0.3" + "@npmcli/package-json" "^1.0.1" + "@npmcli/run-script" "^2.0.0" + bin-links "^2.3.0" + cacache "^15.0.3" + common-ancestor-path "^1.0.1" + json-parse-even-better-errors "^2.3.1" + json-stringify-nice "^1.1.4" + mkdirp "^1.0.4" + mkdirp-infer-owner "^2.0.0" + npm-install-checks "^4.0.0" + npm-package-arg "^8.1.5" + npm-pick-manifest "^6.1.0" + npm-registry-fetch "^11.0.0" + pacote "^12.0.2" + parse-conflict-json "^2.0.1" + proc-log "^1.0.0" + promise-all-reject-late "^1.0.0" + promise-call-limit "^1.0.1" + read-package-json-fast "^2.0.2" + readdir-scoped-modules "^1.1.0" + rimraf "^3.0.2" + semver "^7.3.5" + ssri "^8.0.1" + treeverse "^1.0.4" + walk-up-path "^1.0.0" + + "@npmcli/ci-detect@*", "@npmcli/ci-detect@^1.3.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz#18478bbaa900c37bfbd8a2006a6262c62e8b0fe1" + integrity sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q== + + "@npmcli/config@*": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@npmcli/config/-/config-2.4.0.tgz#1447b0274f9502871dabd3ab1d8302472d515b1f" + integrity sha512-fwxu/zaZnvBJohXM3igzqa3P1IVYWi5N343XcKvKkJbAx+rTqegS5tAul4NLiMPQh6WoS5a4er6oo/ieUx1f4g== + dependencies: + ini "^2.0.0" + mkdirp-infer-owner "^2.0.0" + nopt "^5.0.0" + semver "^7.3.4" + walk-up-path "^1.0.0" + + "@npmcli/disparity-colors@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/disparity-colors/-/disparity-colors-1.0.1.tgz#b23c864c9658f9f0318d5aa6d17986619989535c" + integrity sha512-kQ1aCTTU45mPXN+pdAaRxlxr3OunkyztjbbxDY/aIcPS5CnCUrx+1+NvA6pTcYR7wmLZe37+Mi5v3nfbwPxq3A== + dependencies: + ansi-styles "^4.3.0" + + "@npmcli/fs@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.0.tgz#bec1d1b89c170d40e1b73ad6c943b0b75e7d2951" + integrity sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + + "@npmcli/git@^2.0.7", "@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + npm-pick-manifest "^6.1.1" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + + "@npmcli/installed-package-contents@^1.0.6", "@npmcli/installed-package-contents@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + + "@npmcli/map-workspaces@*", "@npmcli/map-workspaces@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-2.0.0.tgz#e342efbbdd0dad1bba5d7723b674ca668bf8ac5a" + integrity sha512-QBJfpCY1NOAkkW3lFfru9VTdqvMB2TN0/vrevl5xBCv5Fi0XDVcA6rqqSau4Ysi4Iw3fBzyXV7hzyTBDfadf7g== + dependencies: + "@npmcli/name-from-folder" "^1.0.1" + glob "^7.1.6" + minimatch "^3.0.4" + read-package-json-fast "^2.0.1" + + "@npmcli/metavuln-calculator@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/metavuln-calculator/-/metavuln-calculator-2.0.0.tgz#70937b8b5a5cad5c588c8a7b38c4a8bd6f62c84c" + integrity sha512-VVW+JhWCKRwCTE+0xvD6p3uV4WpqocNYYtzyvenqL/u1Q3Xx6fGTJ+6UoIoii07fbuEO9U3IIyuGY0CYHDv1sg== + dependencies: + cacache "^15.0.5" + json-parse-even-better-errors "^2.3.1" + pacote "^12.0.0" + semver "^7.3.2" + + "@npmcli/move-file@^1.0.1", "@npmcli/move-file@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + + "@npmcli/name-from-folder@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-1.0.1.tgz#77ecd0a4fcb772ba6fe927e2e2e155fbec2e6b1a" + integrity sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA== + + "@npmcli/node-gyp@^1.0.2", "@npmcli/node-gyp@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz#a912e637418ffc5f2db375e93b85837691a43a33" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== + + "@npmcli/package-json@*", "@npmcli/package-json@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-1.0.1.tgz#1ed42f00febe5293c3502fd0ef785647355f6e89" + integrity sha512-y6jnu76E9C23osz8gEMBayZmaZ69vFOIk8vR1FJL/wbEJ54+9aVG9rLTjQKSXfgYZEr50nw1txBBFfBZZe+bYg== + dependencies: + json-parse-even-better-errors "^2.3.1" + + "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + + "@npmcli/run-script@*", "@npmcli/run-script@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-2.0.0.tgz#9949c0cab415b17aaac279646db4f027d6f1e743" + integrity sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^8.2.0" + read-package-json-fast "^2.0.1" + + "@react-native-async-storage/async-storage@^1.15.8": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.15.8.tgz#c2263646b7261803125b555cf1bbd48b72dedafc" + integrity sha512-SIpsnmUt2Af8f/In7wu/HMeIiWBx9+T14GL4VrwtZv8+RceMejPtOwRMP8kc6xifkgg0gxwwHJ5+pEG/cEt1Mw== + dependencies: + merge-options "^3.0.4" + "@react-native-community/cli-debugger-ui@^4.13.1": version "4.13.1" resolved "https://registry.npmjs.org/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.13.1.tgz" @@ -2220,12 +2406,12 @@ Lockfile: resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-2.1.0.tgz#1ef22d4e9b2e555d44b43453f51a46d8631f3182" integrity sha512-iJ/QaDrBMBaW6cFuQyR3DXzcn2h7c5O7mGgmNLCBQHTTtLNBZR+Sxogy6YleFPeToNdysG5mTTkXqBmlWHMQqg== - "@react-navigation/bottom-tabs@^6.0.5": - version "6.0.7" - resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-6.0.7.tgz" - integrity sha512-NfSb4Y5wSEPg3T7gHN25O133enSscJ808xEWqhGkR96BtSjcHh/oNMO+dqk6Avgh56uZEyL92Qm/CKFvaGkWow== + "@react-navigation/bottom-tabs@^6.0.9": + version "6.0.9" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.0.9.tgz#916b6a4b495ea8fdcace98dab727064876875d09" + integrity sha512-uRoq6Zd7lPNnLqNQkKC28eR62tpqcDeuakZU1sO8N46FtvrcTuNLoIlssrGty3GF7ALBIxCypn4A93t3nbmMrQ== dependencies: - "@react-navigation/elements" "^1.1.2" + "@react-navigation/elements" "^1.2.1" color "^3.1.3" warn-once "^0.1.0" @@ -2240,17 +2426,24 @@ Lockfile: query-string "^7.0.0" react-is "^16.13.0" + "@react-navigation/drawer@^6.1.8": + version "6.1.8" + resolved "https://registry.yarnpkg.com/@react-navigation/drawer/-/drawer-6.1.8.tgz#82e2a06d17166a82dd18233678ebf63ed523aaab" + integrity sha512-kYE2EO5dianUuUcaYmAlYBcgtmvGm2fxWTQ5sn103cgPNidp4KBUR9ClkhF+btfRaHKq+8Ul5M6qvL0mBAv/Lg== + dependencies: + "@react-navigation/elements" "^1.2.1" + color "^3.1.3" + warn-once "^0.1.0" + "@react-navigation/elements@^1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.1.2.tgz" integrity sha512-PbPCleC1HpUlXtuP0DFNCNTEhRLd6lmB0KxY0SGRGqCemS3HpG/PajEQ1LDe7S51M03a1tDby1MfKTkNanUXAg== - "@react-navigation/material-bottom-tabs@^6.0.7": - version "6.0.7" - resolved "https://registry.yarnpkg.com/@react-navigation/material-bottom-tabs/-/material-bottom-tabs-6.0.7.tgz#30f9d60e344eb4e3b1f68732715dc360755edbbd" - integrity sha512-EjaetcI+kgxtImLm+zA5SiNoLk2SKCqxEinCjYpBhlKRU5i/Mt+VXJbuvMFSJNuPnjZfD275N9Ql+YqyKnZMxg== - dependencies: - "@react-navigation/elements" "^1.1.2" + "@react-navigation/elements@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.2.1.tgz#86f19781c6f34a5c9dd25dca99915e0306f477d1" + integrity sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg== "@react-navigation/native-stack@^6.1.0": version "6.2.2" @@ -2283,6 +2476,11 @@ Lockfile: dependencies: type-detect "4.0.8" + "@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@types/babel__core@^7.1.7": version "7.1.16" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz" @@ -2355,6 +2553,11 @@ Lockfile: dependencies: "@types/istanbul-lib-report" "*" + "@types/lz-string@^1.3.34": + version "1.3.34" + resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5" + integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow== + "@types/node@*": version "16.10.1" resolved "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz" @@ -2439,6 +2642,11 @@ Lockfile: resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + abbrev@*, abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -2482,6 +2690,30 @@ Lockfile: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + + agentkeepalive@^4.1.3: + version "4.2.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.0.tgz#616ce94ccb41d1a39a45d203d8076fe98713062d" + integrity sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + + aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.12.3: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -2578,7 +2810,7 @@ Lockfile: dependencies: color-convert "^1.9.0" - ansi-styles@^4.0.0, ansi-styles@^4.1.0: + ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -2590,6 +2822,16 @@ Lockfile: resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + ansicolors@*: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= + + ansistyles@*: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" + integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= + any-base@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz" @@ -2616,6 +2858,24 @@ Lockfile: normalize-path "^3.0.0" picomatch "^2.0.4" + "aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + + archy@*: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + + are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" @@ -2686,7 +2946,7 @@ Lockfile: resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - asap@~2.0.3, asap@~2.0.6: + asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -2732,6 +2992,11 @@ Lockfile: dependencies: lodash "^4.17.14" + async@~0.2.7: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2954,6 +3219,23 @@ Lockfile: resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz" integrity sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw== + bin-links@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-2.3.0.tgz#1ff241c86d2c29b24ae52f49544db5d78a4eb967" + integrity sha512-JzrOLHLwX2zMqKdyYZjkDgQGT+kHDkIhv2/IK2lJ00qLxV4TmFoHi8drDBb6H5Zrz1YfgHkai4e2MGPqnoUhqA== + dependencies: + cmd-shim "^4.0.1" + mkdirp-infer-owner "^2.0.0" + npm-normalize-package-bin "^1.0.0" + read-cmd-shim "^2.0.0" + rimraf "^3.0.0" + write-file-atomic "^3.0.3" + + binary-extensions@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -3092,11 +3374,40 @@ Lockfile: base64-js "^1.3.1" ieee754 "^1.1.13" + builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + bytes@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + cacache@*, cacache@^15.0.3, cacache@^15.0.5, cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz" @@ -3166,6 +3477,11 @@ Lockfile: resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + chalk@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.0.0.tgz#bd96c6bb8e02b96e08c0c3ee2a9d90e050c7b832" + integrity sha512-/duVOqst+luxCQRKEo4bNxinsOQtMP80ZYm7mMqzuh5PociNL0PvmHFvREJ9ueYL2TxlHjBcmLCdmocx9Vg+IQ== + chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -3207,11 +3523,23 @@ Lockfile: resolved "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz" integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= + chownr@*, chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + cidr-regex@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-3.1.1.tgz#ba1972c57c66f61875f18fd7dd487469770b571d" + integrity sha512-RBqYd32aDwbCMFJRL6wHOlDNYJsPNTt8vC82ErHF5vKt8QQzxm1FrkW8s/R5pVrXMf17sba09Uoy91PKiddAsw== + dependencies: + ip-regex "^4.1.0" + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz" @@ -3222,6 +3550,19 @@ Lockfile: isobject "^3.0.0" static-extend "^0.1.1" + clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + + cli-columns@*: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-4.0.0.tgz#9fe4d65975238d55218c41bd2ed296a7fa555646" + integrity sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ== + dependencies: + string-width "^4.2.3" + strip-ansi "^6.0.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz" @@ -3234,6 +3575,15 @@ Lockfile: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== + cli-table3@*: + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== + dependencies: + string-width "^4.2.0" + optionalDependencies: + colors "1.4.0" + cli-width@^2.0.0: version "2.2.1" resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz" @@ -3271,6 +3621,13 @@ Lockfile: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + cmd-shim@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" @@ -3333,7 +3690,7 @@ Lockfile: color-name "^1.0.0" simple-swizzle "^0.2.2" - color-support@^1.1.3: + color-support@^1.1.2, color-support@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -3346,7 +3703,7 @@ Lockfile: color-convert "^0.5.0" color-string "^0.3.0" - color@^3.1.3: + color@^3.1.2, color@^3.1.3: version "3.2.1" resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== @@ -3364,6 +3721,11 @@ Lockfile: resolved "https://registry.yarnpkg.com/colornames/-/colornames-0.0.2.tgz#d811fd6c84f59029499a8ac4436202935b92be31" integrity sha1-2BH9bIT1kClJmorEQ2ICk1uSvjE= + colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + colorspace@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.0.1.tgz#c99c796ed31128b9876a52e1ee5ee03a4a719749" @@ -3372,6 +3734,14 @@ Lockfile: color "0.8.x" text-hex "0.0.x" + columnify@*: + version "1.5.4" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= + dependencies: + strip-ansi "^3.0.0" + wcwidth "^1.0.0" + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -3379,6 +3749,13 @@ Lockfile: dependencies: delayed-stream "~1.0.0" + combined-stream@~0.0.4: + version "0.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-0.0.7.tgz#0137e657baa5a7541c57ac37ac5fc07d73b4dc1f" + integrity sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8= + dependencies: + delayed-stream "0.0.5" + command-exists@^1.2.8: version "1.2.9" resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz" @@ -3409,6 +3786,11 @@ Lockfile: resolved "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz" integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== + common-ancestor-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" + integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" @@ -3476,6 +3858,11 @@ Lockfile: parseurl "~1.3.3" utils-merge "1.0.1" + console-control-strings@^1.0.0, console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" @@ -3526,7 +3913,7 @@ Lockfile: js-yaml "^3.13.1" parse-json "^4.0.0" - create-react-class@^15.6.2, create-react-class@^15.6.3: + create-react-class@^15.6.2: version "15.7.0" resolved "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz" integrity sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng== @@ -3666,6 +4053,13 @@ Lockfile: dependencies: ms "2.0.0" + debug@4: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" @@ -3673,6 +4067,11 @@ Lockfile: dependencies: ms "2.1.2" + debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" @@ -3741,17 +4140,27 @@ Lockfile: is-descriptor "^1.0.2" isobject "^3.0.1" + delayed-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.5.tgz#d4b1f43a93e8296dfe02694f4680bc37a313c73f" + integrity sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8= + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + denodeify@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= - depd@~1.1.2: + depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -3766,6 +4175,14 @@ Lockfile: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + diagnostics@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.0.1.tgz#accdb080c82bb25d0dd73430a9e6a87fbb431541" @@ -3780,6 +4197,11 @@ Lockfile: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz" integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== + diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -3868,7 +4290,7 @@ Lockfile: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - encoding@^0.1.11: + encoding@^0.1.11, encoding@^0.1.12: version "0.1.13" resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== @@ -3887,6 +4309,11 @@ Lockfile: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + env-variable@0.0.x: version "0.0.6" resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.6.tgz#74ab20b3786c545b62b4a4813ab8cf22726c9808" @@ -3897,6 +4324,11 @@ Lockfile: resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -3991,6 +4423,11 @@ Lockfile: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + eventemitter3@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + exec-sh@^0.3.2: version "0.3.6" resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz" @@ -4076,6 +4513,19 @@ Lockfile: path-browserify "^1.0.0" url-parse "^1.4.4" + expo-barcode-scanner@~10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/expo-barcode-scanner/-/expo-barcode-scanner-10.2.2.tgz#506256ce4aafae5b17da77dceeb52831de20971b" + integrity sha512-FWGyNB88kntUV1ckpEcCiq8iepu9EJO/cK0bDnp42ieydBhKlTPXH/d/3ipBFbE+Ia7MNfddjzmzbDCadJukcg== + dependencies: + "@expo/config-plugins" "^3.0.0" + expo-modules-core "~0.2.0" + + expo-checkbox@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/expo-checkbox/-/expo-checkbox-1.0.3.tgz#1d7510947814b19fc710fb7fc80a2433d98e3cc3" + integrity sha512-7w8H1yt7V1pdnbIAIoV3RvI+Rjzc8nlxy4eKdEtZhYupenLmBY173glAx+/eJcW/vlCqKYHxedQmRTqEUpAQ7A== + expo-constants@~11.0.1, expo-constants@~11.0.2: version "11.0.2" resolved "https://registry.npmjs.org/expo-constants/-/expo-constants-11.0.2.tgz" @@ -4107,6 +4557,15 @@ Lockfile: expo-modules-core "~0.2.0" fontfaceobserver "^2.1.0" + expo-image-picker@~10.2.2: + version "10.2.3" + resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-10.2.3.tgz#204c83ba0731f2ccdbc42e13f9bf6e37be21c354" + integrity sha512-8VXLYjclXoQJHbdNLI21rdbnxFisBpZ6TgIifHf9kZ/momFBegUNqEKCjosvxVGVM8f7qaQxJV/znNtW0rDM/w== + dependencies: + "@expo/config-plugins" "^3.0.0" + expo-modules-core "~0.2.0" + uuid "7.0.2" + expo-keep-awake@~9.2.0: version "9.2.0" resolved "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-9.2.0.tgz" @@ -4138,6 +4597,22 @@ Lockfile: resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-0.2.0.tgz" integrity sha512-inpfZ5X/BaTtbj2wG9PA9AC0MN8VyId6KSRlVuEg7+ziurHBy/kKDFxpOddUokhwiln2uhoYPSStJjR/tKypdw== + expo-sensors@~10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/expo-sensors/-/expo-sensors-10.2.2.tgz#882e25b3135995e383d88b21b17e5f1b1155d4e0" + integrity sha512-kZZobyfQQPA7PwuTQ2+HwunZ1o2emWLKqWP9jYOBPI4toQAproSLzhlhKmpm2vRDOL1ctC12Fb0g9elv7r9Omg== + dependencies: + "@expo/config-plugins" "^3.0.0" + expo-modules-core "~0.2.0" + invariant "^2.2.4" + + expo-sharing@~9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-9.2.1.tgz#50267cd66f54d29f0ab3f185a05d92b197e5b60c" + integrity sha512-L0OR7qq8GJRKEFDMPrStc9UxVdOr3XRGMuK3vO2qgQgU3CCSb5QE5lHRdp4DFyJd22ILZqwL+3ghiUtFQD0eig== + dependencies: + expo-modules-core "~0.2.0" + expo-splash-screen@~0.11.2: version "0.11.4" resolved "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.11.4.tgz" @@ -4296,6 +4771,11 @@ Lockfile: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + fastest-levenshtein@*: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + fastq@^1.6.0: version "1.13.0" resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" @@ -4499,6 +4979,15 @@ Lockfile: resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + form-data@~0.0.3: + version "0.0.10" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-0.0.10.tgz#db345a5378d86aeeb1ed5d553b869ac192d2f5ed" + integrity sha1-2zRaU3jYau6x7V1VO4aawZLS9e0= + dependencies: + async "~0.2.7" + combined-stream "~0.0.4" + mime "~1.2.2" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" @@ -4567,6 +5056,13 @@ Lockfile: jsonfile "^6.0.1" universalify "^2.0.0" + fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -4613,6 +5109,21 @@ Lockfile: emits "3.0.x" predefine "0.1.x" + gauge@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8" + integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw== + dependencies: + ansi-regex "^5.0.1" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -4684,6 +5195,18 @@ Lockfile: dependencies: is-glob "^4.0.1" + glob@*: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@7.1.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" @@ -4709,6 +5232,11 @@ Lockfile: resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + graceful-fs@*, graceful-fs@^4.2.6: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.8" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz" @@ -4754,6 +5282,11 @@ Lockfile: resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has-value@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz" @@ -4804,13 +5337,20 @@ Lockfile: dependencies: source-map "^0.7.3" - hoist-non-react-statics@^3.3.0: + hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== dependencies: react-is "^16.7.0" + hosted-git-info@*, hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" @@ -4828,6 +5368,11 @@ Lockfile: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + http-cache-semantics@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-errors@~1.7.2: version "1.7.3" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz" @@ -4839,6 +5384,15 @@ Lockfile: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" + http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" @@ -4848,11 +5402,26 @@ Lockfile: jsprim "^1.2.2" sshpk "^1.7.0" + https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz" @@ -4877,6 +5446,13 @@ Lockfile: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore-walk@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-4.0.1.tgz#fc840e8346cf88a3a9380c5b17933cd8f4d39fa3" + integrity sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw== + dependencies: + minimatch "^3.0.4" + image-size@^0.6.0: version "0.6.3" resolved "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz" @@ -4903,6 +5479,16 @@ Lockfile: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + + infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -4916,6 +5502,24 @@ Lockfile: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + ini@*, ini@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + + init-package-json@*: + version "2.0.5" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-2.0.5.tgz#78b85f3c36014db42d8f32117252504f68022646" + integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== + dependencies: + npm-package-arg "^8.1.5" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "^4.1.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + inline-style-prefixer@^5.1.0: version "5.1.2" resolved "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-5.1.2.tgz" @@ -4955,6 +5559,11 @@ Lockfile: resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + ip-regex@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + ip@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz" @@ -4996,6 +5605,13 @@ Lockfile: dependencies: ci-info "^2.0.0" + is-cidr@*: + version "4.0.2" + resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-4.0.2.tgz#94c7585e4c6c77ceabf920f8cde51b8c0fda8814" + integrity sha512-z4a1ENUajDbEl/Q6/pVBpTR1nBjjEE1X7qb7bmWYanNnPoKAvUCPFKeXV6Fe4mgTkWKBqiHIcwsI3SndiO5FeA== + dependencies: + cidr-regex "^3.1.1" + is-core-module@^2.2.0: version "2.7.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz" @@ -5003,6 +5619,13 @@ Lockfile: dependencies: has "^1.0.3" + is-core-module@^2.5.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz" @@ -5089,6 +5712,11 @@ Lockfile: dependencies: is-extglob "^2.1.1" + is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz" @@ -5111,6 +5739,11 @@ Lockfile: resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + 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== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" @@ -5810,7 +6443,7 @@ Lockfile: resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - json-parse-even-better-errors@^2.3.0: + json-parse-even-better-errors@*, json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -5832,6 +6465,11 @@ Lockfile: dependencies: jsonify "~0.0.0" + json-stringify-nice@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" + integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" @@ -5884,6 +6522,11 @@ Lockfile: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= + jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" @@ -5894,6 +6537,16 @@ Lockfile: json-schema "0.2.3" verror "1.10.0" + just-diff-apply@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-4.0.1.tgz#da89c5a4ccb14aa8873c70e2c3b6695cef45dab5" + integrity sha512-AKOkzB5P6FkfP21UlZVX/OPXx/sC2GagpLX9cBxqHqDuRjwmZ/AJRKSNrB9jHPpRW1W1ONs6gly1gW46t055nQ== + + just-diff@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.0.1.tgz#db8fe1cfeea1156f2374bfb289826dca28e7e390" + integrity sha512-X00TokkRIDotUIf3EV4xUm6ELc/IkqhS/vPSHdWnsM5y0HoNMfEqrazizI7g78lpHvnRSRt/PFfKtRqJCOGIuQ== + kind-of@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz" @@ -5955,16 +6608,126 @@ Lockfile: prelude-ls "~1.1.2" type-check "~0.3.2" - licenses@0.0.x: - version "0.0.20" - resolved "https://registry.yarnpkg.com/licenses/-/licenses-0.0.20.tgz#f18a57b26a78eaf28a873e2a378a33e81f59d136" - integrity sha1-8YpXsmp46vKKhz4qN4oz6B9Z0TY= + libnpmaccess@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-5.0.0.tgz#84bc9e66fe60fd8baab46477841497fd108d350d" + integrity sha512-iRaq1wBvTKvcyJHHhmC2lXX1YCaqNiPu4YDObWQRpubKGUjgStxDisZ94KGnF4q3L7EoaWYHOGWqxJWKUe1TKg== dependencies: - async "0.6.x" - debug "0.8.x" - fusing "0.2.x" - githulk "0.0.x" - npm-registry "0.1.x" + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + + libnpmdiff@*: + version "3.0.0" + resolved "https://registry.yarnpkg.com/libnpmdiff/-/libnpmdiff-3.0.0.tgz#4beb36cf9a8b91a99c373589e99822c18a798c0e" + integrity sha512-pnwUs96QpM7KzD4vOyxTZvrjxi61y/5u/nBUKih8+eKQ9H8DiIBcV1OGaj7OKhoxJk4D5s8Aw746rE49FARavQ== + dependencies: + "@npmcli/disparity-colors" "^1.0.1" + "@npmcli/installed-package-contents" "^1.0.7" + binary-extensions "^2.2.0" + diff "^5.0.0" + minimatch "^3.0.4" + npm-package-arg "^8.1.4" + pacote "^12.0.0" + tar "^6.1.0" + + libnpmexec@*: + version "3.0.2" + resolved "https://registry.yarnpkg.com/libnpmexec/-/libnpmexec-3.0.2.tgz#2c14d77254245c22d3ff0d6ff8f25aafe7d84ad0" + integrity sha512-VOXAeBAna2feIptY08UArQuoMr4SuioFgW57QpcLMAom8+pfWm9q0TazRGMuFO9urB/XG/Gx4APpQeTpdYKSkw== + dependencies: + "@npmcli/arborist" "^4.0.0" + "@npmcli/ci-detect" "^1.3.0" + "@npmcli/run-script" "^2.0.0" + chalk "^4.1.0" + mkdirp-infer-owner "^2.0.0" + npm-package-arg "^8.1.2" + pacote "^12.0.0" + proc-log "^1.0.0" + read "^1.0.7" + read-package-json-fast "^2.0.2" + walk-up-path "^1.0.0" + + libnpmfund@*: + version "2.0.2" + resolved "https://registry.yarnpkg.com/libnpmfund/-/libnpmfund-2.0.2.tgz#90a7aa26c8b9b4739a06e314f83decd198b3f9c6" + integrity sha512-7gznxLV71t9KsC9jyV6ILbLjfebettTzn13TVl29hwzDpiG+NkA7xZ7yT0L9e7DI8CVUVIxvvHKUl3Ny+TNQ8Q== + dependencies: + "@npmcli/arborist" "^4.0.0" + + libnpmhook@*: + version "7.0.0" + resolved "https://registry.yarnpkg.com/libnpmhook/-/libnpmhook-7.0.0.tgz#61ba6778aed080761b780b99d1a6e3424e8d8aa0" + integrity sha512-4ssUN06HZ33ig7lUFYslwqX9BhMtHDCmiRF/cnWqBgy1baz0WoOWYySh8wGEQbx3DXghWbgOo4Av/kvC+1Q4gw== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^11.0.0" + + libnpmorg@*: + version "3.0.0" + resolved "https://registry.yarnpkg.com/libnpmorg/-/libnpmorg-3.0.0.tgz#ad2dd66660b3eb27a5d186e4573fdb39ae1e5c26" + integrity sha512-9MZFr81gOfVQbm62yovTTTgflFYTM66O/tHFrftWtsK3KC7Hx+T7X7A2xMwbS3mCzg+zU0GMJxLstnOk5Nbf4w== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^11.0.0" + + libnpmpack@*: + version "3.0.1" + resolved "https://registry.yarnpkg.com/libnpmpack/-/libnpmpack-3.0.1.tgz#bad1930e57a415239ea4f0419b4796bc4bf8009b" + integrity sha512-xTE/nlvAZfh/Drm/jsSieGAhjKBo9uRjlU/i50IeFZKwKYwCQTPuyT3ZXiTgjhwWT+/FMVd2afRb6uGMdViFvQ== + dependencies: + "@npmcli/run-script" "^2.0.0" + npm-package-arg "^8.1.0" + pacote "^12.0.0" + + libnpmpublish@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-5.0.0.tgz#3d55e885659aa4e79a7beac9015801b734705ad7" + integrity sha512-I2ztr1ZIAqjbOOVfuskzZykxbTqbFvMADWz/VyiaSfSiiLBRtiFM/N4wK1YK+NtXJFLJM+kNX7oYW7XsOf/Kvw== + dependencies: + normalize-package-data "^3.0.2" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + semver "^7.1.3" + ssri "^8.0.1" + + libnpmsearch@*: + version "4.0.0" + resolved "https://registry.yarnpkg.com/libnpmsearch/-/libnpmsearch-4.0.0.tgz#3d65a0eace4356fe7230ff0dfdc6dfd44968ad99" + integrity sha512-bzr8L7nfJ1FtYJ9Cq4p2qRTLWR2EuxVXH0X4WBjuiAGDcORmvlL2+9ODaplcKGpbtvwSTSOvKnM9u0AbUVIgeg== + dependencies: + npm-registry-fetch "^11.0.0" + + libnpmteam@*: + version "3.0.0" + resolved "https://registry.yarnpkg.com/libnpmteam/-/libnpmteam-3.0.0.tgz#0f86dc0b9623a9c95e40e2f146d1cc2cf00de044" + integrity sha512-pxL/a19riZj1gHOuQqJ1KJvJ3qCRqXBe+P3QouZ+bE8B79C+oKXPe2wX2BIgdLd230zbBR+g9+4PPOsKpQJseQ== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^11.0.0" + + libnpmversion@*: + version "2.0.2" + resolved "https://registry.yarnpkg.com/libnpmversion/-/libnpmversion-2.0.2.tgz#9fc1b94f5a2d0ae8c6378af498f3f44248f467d6" + integrity sha512-Dg+ccHL/F8BQgEeE9n8OU3T1FXhbdAKaj5+OYYPUrcrXkMh9EhVQ8uIbxCl0FQUeQHeWW9XmfO2icZ5YcZQvbQ== + dependencies: + "@npmcli/git" "^2.0.7" + "@npmcli/run-script" "^2.0.0" + json-parse-even-better-errors "^2.3.1" + semver "^7.3.5" + stringify-package "^1.0.1" + + licenses@0.0.x: + version "0.0.20" + resolved "https://registry.yarnpkg.com/licenses/-/licenses-0.0.20.tgz#f18a57b26a78eaf28a873e2a378a33e81f59d136" + integrity sha1-8YpXsmp46vKKhz4qN4oz6B9Z0TY= + dependencies: + async "0.6.x" + debug "0.8.x" + fusing "0.2.x" + githulk "0.0.x" + npm-registry "0.1.x" lines-and-columns@^1.1.6: version "1.1.6" @@ -6125,6 +6888,11 @@ Lockfile: dependencies: yallist "^4.0.0" + lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" @@ -6140,6 +6908,28 @@ Lockfile: dependencies: semver "^6.0.0" + make-fetch-happen@*, make-fetch-happen@^9.0.1, make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz" @@ -6184,6 +6974,13 @@ Lockfile: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + 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" + merge-stream@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz" @@ -6485,6 +7282,11 @@ Lockfile: resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== + mime@~1.2.2, mime@~1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10" + integrity sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA= + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz" @@ -6514,6 +7316,68 @@ Lockfile: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + + minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + + minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + + minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + + minipass-pipeline@*, minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + + minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + + minipass@*, minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + + minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" @@ -6522,6 +7386,20 @@ Lockfile: for-in "^1.0.2" is-extendable "^1.0.1" + mkdirp-infer-owner@*, mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== + dependencies: + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" + + mkdirp@*, mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -6534,6 +7412,11 @@ Lockfile: resolved "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz" integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== + ms@*, ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -6554,6 +7437,11 @@ Lockfile: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" @@ -6600,7 +7488,7 @@ Lockfile: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - negotiator@0.6.2: + negotiator@0.6.2, negotiator@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== @@ -6635,6 +7523,22 @@ Lockfile: dependencies: whatwg-url "^5.0.0" + node-gyp@*, node-gyp@^8.2.0: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -6666,6 +7570,13 @@ Lockfile: resolved "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz" integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== + nopt@*, nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-css-color@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz" @@ -6681,6 +7592,16 @@ Lockfile: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" + normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" @@ -6702,6 +7623,92 @@ Lockfile: query-string "^5.0.1" sort-keys "^2.0.0" + npm-audit-report@*: + version "2.1.5" + resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-2.1.5.tgz#a5b8850abe2e8452fce976c8960dd432981737b5" + integrity sha512-YB8qOoEmBhUH1UJgh1xFAv7Jg1d+xoNhsDYiFQlEFThEBui0W1vIz2ZK6FVg4WZjwEdl7uBQlm1jy3MUfyHeEw== + dependencies: + chalk "^4.0.0" + + npm-bundled@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== + dependencies: + npm-normalize-package-bin "^1.0.1" + + npm-install-checks@*, npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + + npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + + npm-package-arg@*, npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.4, npm-package-arg@^8.1.5: + version "8.1.5" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== + dependencies: + hosted-git-info "^4.0.1" + semver "^7.3.4" + validate-npm-package-name "^3.0.0" + + npm-packlist@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-3.0.0.tgz#0370df5cfc2fcc8f79b8f42b37798dd9ee32c2a9" + integrity sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ== + dependencies: + glob "^7.1.6" + ignore-walk "^4.0.1" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + + npm-pick-manifest@*, npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.0, npm-pick-manifest@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" + integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== + dependencies: + npm-install-checks "^4.0.0" + npm-normalize-package-bin "^1.0.1" + npm-package-arg "^8.1.2" + semver "^7.3.4" + + npm-profile@*: + version "5.0.4" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-5.0.4.tgz#73e5bd1d808edc2c382d7139049cc367ac43161b" + integrity sha512-OKtU7yoAEBOnc8zJ+/uo5E4ugPp09sopo+6y1njPp+W99P8DvQon3BJYmpvyK2Bf1+3YV5LN1bvgXRoZ1LUJBA== + dependencies: + npm-registry-fetch "^11.0.0" + + npm-registry-fetch@*: + version "12.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-12.0.0.tgz#53d8c94f7c37293707b23728864710b76d3a3ca5" + integrity sha512-nd1I90UHoETjgWpo3GbcoM1l2S4JCUpzDcahU4x/GVCiDQ6yRiw2KyDoPVD8+MqODbPtWwHHGiyc4O5sgdEqPQ== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + + npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + npm-registry@0.1.x, npm-registry@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/npm-registry/-/npm-registry-0.1.13.tgz#9e5d8b2fdfc1ab5990d47f7debbe231d79a9e822" @@ -6727,6 +7734,98 @@ Lockfile: dependencies: path-key "^3.0.0" + npm-user-validate@*: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561" + integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw== + + npm@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/npm/-/npm-8.3.0.tgz#03d32b0ddb07a5865726baf7149bb0475023df4d" + integrity sha512-ug4xToae4Dh3yZh8Fp6MOnAPSS3fqCTANpJx1fXP2C4LTUzoZf7rEantHQR/ANPVYDBe5qQT4tGVsoPqqiYZMw== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/arborist" "^4.1.1" + "@npmcli/ci-detect" "^1.4.0" + "@npmcli/config" "^2.3.2" + "@npmcli/map-workspaces" "^2.0.0" + "@npmcli/package-json" "^1.0.1" + "@npmcli/run-script" "^2.0.0" + abbrev "~1.1.1" + ansicolors "~0.3.2" + ansistyles "~0.1.3" + archy "~1.0.0" + cacache "^15.3.0" + chalk "^4.1.2" + chownr "^2.0.0" + cli-columns "^4.0.0" + cli-table3 "^0.6.0" + columnify "~1.5.4" + fastest-levenshtein "^1.0.12" + glob "^7.2.0" + graceful-fs "^4.2.8" + hosted-git-info "^4.0.2" + ini "^2.0.0" + init-package-json "^2.0.5" + is-cidr "^4.0.2" + json-parse-even-better-errors "^2.3.1" + libnpmaccess "^4.0.2" + libnpmdiff "^2.0.4" + libnpmexec "^3.0.1" + libnpmfund "^2.0.1" + libnpmhook "^6.0.2" + libnpmorg "^2.0.2" + libnpmpack "^3.0.0" + libnpmpublish "^4.0.1" + libnpmsearch "^3.1.1" + libnpmteam "^2.0.3" + libnpmversion "^2.0.1" + make-fetch-happen "^9.1.0" + minipass "^3.1.6" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + mkdirp-infer-owner "^2.0.0" + ms "^2.1.2" + node-gyp "^8.4.1" + nopt "^5.0.0" + npm-audit-report "^2.1.5" + npm-install-checks "^4.0.0" + npm-package-arg "^8.1.5" + npm-pick-manifest "^6.1.1" + npm-profile "^5.0.3" + npm-registry-fetch "^11.0.0" + npm-user-validate "^1.0.1" + npmlog "^6.0.0" + opener "^1.5.2" + pacote "^12.0.2" + parse-conflict-json "^2.0.1" + proc-log "^1.0.0" + qrcode-terminal "^0.12.0" + read "~1.0.7" + read-package-json "^4.1.1" + read-package-json-fast "^2.0.3" + readdir-scoped-modules "^1.1.0" + rimraf "^3.0.2" + semver "^7.3.5" + ssri "^8.0.1" + tar "^6.1.11" + text-table "~0.2.0" + tiny-relative-date "^1.3.0" + treeverse "^1.0.4" + validate-npm-package-name "~3.0.0" + which "^2.0.2" + write-file-atomic "^3.0.3" + + npmlog@*, npmlog@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c" + integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.0" + set-blocking "^2.0.0" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -6847,6 +7946,11 @@ Lockfile: dependencies: is-wsl "^1.1.0" + opener@*: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" @@ -6945,6 +8049,13 @@ Lockfile: dependencies: p-limit "^3.0.2" + p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" @@ -6955,6 +8066,31 @@ Lockfile: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + pacote@*, pacote@^12.0.0, pacote@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-12.0.2.tgz#14ae30a81fe62ec4fc18c071150e6763e932527c" + integrity sha512-Ar3mhjcxhMzk+OVZ8pbnXdb0l8+pimvlsqBGRNkble2NVgyqOGE3yrCGi/lAYq7E7NRDMz89R1Wx5HIMCGgeYg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^2.0.0" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^3.0.0" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + pako@^1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" @@ -6978,6 +8114,15 @@ Lockfile: xml-parse-from-string "^1.0.0" xml2js "^0.4.5" + parse-conflict-json@*, parse-conflict-json@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/parse-conflict-json/-/parse-conflict-json-2.0.1.tgz#76647dd072e6068bcaff20be6ccea68a18e1fb58" + integrity sha512-Y7nYw+QaSGBto1LB9lgwOR05Rtz5SbuTf+Oe7HJ6SYQ/DHsvRjQ8O03oWdJbvkt6GzDWospgyZbGmjDYL0sDgA== + dependencies: + json-parse-even-better-errors "^2.3.1" + just-diff "^5.0.1" + just-diff-apply "^4.0.1" + parse-headers@^2.0.0: version "2.0.4" resolved "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz" @@ -7209,6 +8354,11 @@ Lockfile: ansi-styles "^4.0.0" react-is "^17.0.1" + proc-log@*, proc-log@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77" + integrity sha512-aCk8AO51s+4JyuYGg3Q/a6gnrlDO09NpVWePtjp7xwphcoQ04x5WAfCyugcsbLooWcMJ87CLkD4+604IckEdhg== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -7219,6 +8369,29 @@ Lockfile: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + promise-all-reject-late@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" + integrity sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw== + + promise-call-limit@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-call-limit/-/promise-call-limit-1.0.1.tgz#4bdee03aeb85674385ca934da7114e9bcd3c6e24" + integrity sha512-3+hgaa19jzCGLuSCbieeRsu5C2joKfYn8pY6JAuXFRVfF4IO+L7UPpFWNTeWT9pM7uhskvbPPd/oEOktCn317Q== + + promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + + promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + promise@^7.1.1: version "7.3.1" resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" @@ -7241,7 +8414,14 @@ Lockfile: kleur "^3.0.3" sisteransi "^1.0.5" - prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: + promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= + dependencies: + read "1" + + prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7273,11 +8453,16 @@ Lockfile: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - qr.js@0.0.0, qr.js@^0.0.0: + qr.js@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= + qrcode-terminal@*: + version "0.12.0" + resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" + integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== + qs@^6.5.0: version "6.10.1" resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz" @@ -7352,6 +8537,11 @@ Lockfile: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-native-gesture-handler@~1.10.2: version "1.10.3" resolved "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.10.3.tgz" @@ -7363,14 +8553,22 @@ Lockfile: invariant "^2.2.4" prop-types "^15.7.2" - react-native-qrcode-generator@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/react-native-qrcode-generator/-/react-native-qrcode-generator-1.2.2.tgz#5e475ba2bbe4496eb78bc37c2f5b0c1c8ff7f247" - integrity sha512-M75uyU3zL8yuL1ppL6OJsKBhL+VDZE3jNI5PAvDv+BCKa3MiSTTpXbGEqu7Y4xlhvFUozes/C0Ia7aS3mQ5w6Q== + react-native-global-props@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/react-native-global-props/-/react-native-global-props-1.1.5.tgz#443e0ffc89d5402fa20ebedf37bcbca6d861c34d" + integrity sha512-QDeAdRel6zyJfbgyFxZi9QXZe78OdlANxJae0rJn76uTqdt/A+iWBVjJy3NmaN/fpKy0uV0HhW6Hu4xM0QCisQ== + + react-native-iphone-x-helper@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010" + integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== + + react-native-nfc-manager@^3.11.4: + version "3.11.4" + resolved "https://registry.yarnpkg.com/react-native-nfc-manager/-/react-native-nfc-manager-3.11.4.tgz#28206e6fa6f733281ce495a6a6c55e060eaf7292" + integrity sha512-aHuoOWLet9vmEIUeD18T8V2rug6t70UzkrWbNQMU86y/lROyI88W0thjctqrFPwlDP+MvYBUOnN/hXO33Mv7xg== dependencies: - create-react-class "^15.6.3" - prop-types "^15.5.10" - qr.js "^0.0.0" + "@expo/config-plugins" "^3.0.6" react-native-reanimated@~2.2.0: version "2.2.2" @@ -7394,6 +8592,14 @@ Lockfile: dependencies: warn-once "^0.1.0" + react-native-svg-animations@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/react-native-svg-animations/-/react-native-svg-animations-0.2.6.tgz#230a8829911ca5c4bfb9de205e16a3c926a68d73" + integrity sha512-moApbBqFfFthKFtfgT4EEuVqB3w/HPkBpPcBCFKl2v7fo8FKMDmIJFnlqk81IYHtneYYRv5fTy+OT2U0Pdlpfg== + dependencies: + color "^3.1.2" + svg-path-properties "^1.0.3" + react-native-svg@^12.1.1: version "12.1.1" resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.1.1.tgz#5f292410b8bcc07bbc52b2da7ceb22caf5bcaaee" @@ -7402,6 +8608,11 @@ Lockfile: css-select "^2.1.0" css-tree "^1.0.0-alpha.39" + react-native-tab-view@^2.15.2: + version "2.16.0" + resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz#cae72c7084394bd328fac5fefb86cd966df37a86" + integrity sha512-ac2DmT7+l13wzIFqtbfXn4wwfgtPoKzWjjZyrK1t+T8sdemuUvD4zIt+UImg03fu3s3VD8Wh/fBrIdcqQyZJWg== + react-native-web@~0.13.12: version "0.13.18" resolved "https://registry.npmjs.org/react-native-web/-/react-native-web-0.13.18.tgz" @@ -7459,6 +8670,16 @@ Lockfile: use-subscription "^1.0.0" whatwg-fetch "^3.0.0" + react-navigation-tabs@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/react-navigation-tabs/-/react-navigation-tabs-2.11.1.tgz#dd2ccb04c540b4439aadc4bc8f5a776dfc90439f" + integrity sha512-wq2wR3awu6PKimmVOycBf+iTPA9FWThbJwcaDBQEhQiiviXQzAtT3lw3nV9oqNIg4v65tdPhL1Dg8ptTJ03NjQ== + dependencies: + hoist-non-react-statics "^3.3.2" + react-lifecycles-compat "^3.0.4" + react-native-iphone-x-helper "^1.3.0" + react-native-tab-view "^2.15.2" + react-qr-code@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8" @@ -7496,6 +8717,29 @@ Lockfile: object-assign "^4.1.1" prop-types "^15.6.2" + read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== + + read-package-json-fast@*, read-package-json-fast@^2.0.1, read-package-json-fast@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + + read-package-json@*, read-package-json@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-4.1.1.tgz#153be72fce801578c1c86b8ef2b21188df1b9eea" + integrity sha512-P82sbZJ3ldDrWCOSKxJT0r/CXMWR0OR3KRh55SgKo3p91GSIEEC32v3lSHAvO/UcH3/IoL7uqhOFBduAnwdldw== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" @@ -7515,6 +8759,13 @@ Lockfile: parse-json "^5.0.0" type-fest "^0.6.0" + read@*, read@1, read@^1.0.7, read@~1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= + dependencies: + mute-stream "~0.0.4" + readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" @@ -7528,6 +8779,25 @@ Lockfile: string_decoder "~1.1.1" util-deprecate "~1.0.1" + readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + + readdir-scoped-modules@*, readdir-scoped-modules@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + realpath-native@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz" @@ -7620,6 +8890,14 @@ Lockfile: stealthy-require "^1.1.1" tough-cookie "^2.3.3" + request@2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.12.0.tgz#11f46f20b3d0f4848c6383991c80790af16c8e48" + integrity sha1-EfRvILPQ9ISMY4OZHIB5CvFsjkg= + dependencies: + form-data "~0.0.3" + mime "~1.2.7" + request@2.x.x, request@^2.88.0: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" @@ -7719,11 +8997,23 @@ Lockfile: resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@*, rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@^2.5.4: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" @@ -7731,13 +9021,6 @@ Lockfile: dependencies: glob "^7.1.3" - rimraf@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rimraf@~2.2.6: version "2.2.8" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz" @@ -7791,6 +9074,11 @@ Lockfile: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" @@ -7846,6 +9134,13 @@ Lockfile: loose-envify "^1.1.0" object-assign "^4.1.1" + semver@*, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + "semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" @@ -7871,13 +9166,6 @@ Lockfile: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - semver@^7.3.5: - version "7.3.5" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - send@0.17.1: version "0.17.1" resolved "https://registry.npmjs.org/send/-/send-0.17.1.tgz" @@ -8042,6 +9330,11 @@ Lockfile: resolved "https://registry.npmjs.org/slugify/-/slugify-1.6.0.tgz" integrity sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang== + smart-buffer@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz" @@ -8072,6 +9365,23 @@ Lockfile: source-map-resolve "^0.5.0" use "^3.1.0" + socks-proxy-agent@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + + socks@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.1.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz" @@ -8176,6 +9486,13 @@ Lockfile: safer-buffer "^2.0.2" tweetnacl "~0.14.0" + ssri@*, ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stack-utils@^1.0.1: version "1.0.5" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz" @@ -8241,6 +9558,15 @@ Lockfile: astral-regex "^1.0.0" strip-ansi "^5.2.0" + "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.1.0: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" @@ -8258,14 +9584,12 @@ Lockfile: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" - string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" + safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" @@ -8274,6 +9598,11 @@ Lockfile: dependencies: safe-buffer "~5.1.0" + stringify-package@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" + integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== + strip-ansi@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -8368,11 +9697,28 @@ Lockfile: has-flag "^4.0.0" supports-color "^7.0.0" + svg-path-properties@^1.0.3: + version "1.0.11" + resolved "https://registry.yarnpkg.com/svg-path-properties/-/svg-path-properties-1.0.11.tgz#d804b77dea286ddd56bd182548b9c4f5980dcf83" + integrity sha512-Wo6SjzONZPL9UAgrnwcCkDGRYP9CbHJGkNcPFIgEVRjiOiJxSd/AtwnGk/4N4iOLGUoas57TMxY0xASDeb9YJg== + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + tar@*, tar@^6.0.2, tar@^6.1.0, tar@^6.1.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" @@ -8417,6 +9763,11 @@ Lockfile: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-0.0.0.tgz#578fbc85a6a92636e42dd17b41d0218cce9eb2b3" integrity sha1-V4+8haapJjbkLdF7QdAhjM6esrM= + text-table@*: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" @@ -8464,6 +9815,11 @@ Lockfile: resolved "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz" integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== + tiny-relative-date@*: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" + integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== + tinycolor2@^1.4.1: version "1.4.2" resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz" @@ -8552,6 +9908,11 @@ Lockfile: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + treeverse@*, treeverse@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" + integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" @@ -8674,6 +10035,20 @@ Lockfile: is-extendable "^0.1.1" set-value "^2.0.1" + unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + + unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + unique-string@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz" @@ -8748,7 +10123,7 @@ Lockfile: dependencies: pako "^1.0.5" - util-deprecate@~1.0.1: + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -8758,6 +10133,11 @@ Lockfile: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + uuid@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6" + integrity sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw== + uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" @@ -8782,7 +10162,7 @@ Lockfile: resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= - validate-npm-package-license@^3.0.1: + validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -8790,6 +10170,13 @@ Lockfile: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" + validate-npm-package-name@*, validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" @@ -8804,6 +10191,13 @@ Lockfile: core-util-is "1.0.2" extsprintf "^1.2.0" + version@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/version/-/version-0.1.2.tgz#ab071b0e39c9a34e9308dd8cd7845795deeca70f" + integrity sha1-qwcbDjnJo06TCN2M14RXld7spw8= + dependencies: + request "2.12.0" + vlq@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz" @@ -8825,6 +10219,11 @@ Lockfile: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" + walk-up-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e" + integrity sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz" @@ -8837,7 +10236,7 @@ Lockfile: resolved "https://registry.npmjs.org/warn-once/-/warn-once-0.1.0.tgz" integrity sha512-recZTSvuaH/On5ZU5ywq66y99lImWqzP93+AiUo9LUwG8gXHW+LJjhOd6REJHm7qb0niYqrEQJvbHSQfuJtTqA== - wcwidth@^1.0.1: + wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= @@ -8893,6 +10292,13 @@ Lockfile: resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + which@*, which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" @@ -8900,12 +10306,12 @@ Lockfile: dependencies: isexe "^2.0.0" - which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: - isexe "^2.0.0" + string-width "^1.0.2 || 2 || 3 || 4" word-wrap@~1.2.3: version "1.2.3" @@ -8940,16 +10346,7 @@ Lockfile: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - write-file-atomic@^2.3.0: - version "2.4.3" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz" - integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - - write-file-atomic@^3.0.0: + write-file-atomic@*, write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== @@ -8959,6 +10356,15 @@ Lockfile: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" + write-file-atomic@^2.3.0: + version "2.4.3" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + ws@^1.1.0, ws@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz" diff --git a/yarn.lock b/yarn.lock index 59f7efe..65df573 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1158,11 +1158,6 @@ dependencies: "@types/hammerjs" "^2.0.36" -"@expo-google-fonts/righteous@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@expo-google-fonts/righteous/-/righteous-0.2.0.tgz#c81bd43dfaf05c03c9ebeab7ee7126a8babda027" - integrity sha512-fZXVTfVutI0LGYWh/4s0Nu0HfrQnuOJYs11I38UU8Vn9yCFLzpREhJE/m3rF7CPC5iFg2eSRGJ9aj5X9cPjY5g== - "@expo/config-plugins@1.0.33": version "1.0.33" resolved "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-1.0.33.tgz" @@ -1434,6 +1429,11 @@ lodash.pick "^4.4.0" lodash.template "^4.5.0" +"@gar/promisify@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" + integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== + "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz" @@ -1466,6 +1466,11 @@ dependencies: "@hapi/hoek" "^8.3.0" +"@isaacs/string-locale-compare@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" + integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" @@ -2016,6 +2021,169 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/arborist@^4.0.0", "@npmcli/arborist@^4.1.1": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-4.2.0.tgz#722b114376645ed3c78e1cef62969eb7993df2a0" + integrity sha512-uQmPnwuhNHkN8IgCwda6wXklUf3BUfuuIUFuJMT224frUS5u2AuEAeCr2fiRVsz7AHcW3iSDai2j3WhVFlfbRQ== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/installed-package-contents" "^1.0.7" + "@npmcli/map-workspaces" "^2.0.0" + "@npmcli/metavuln-calculator" "^2.0.0" + "@npmcli/move-file" "^1.1.0" + "@npmcli/name-from-folder" "^1.0.1" + "@npmcli/node-gyp" "^1.0.3" + "@npmcli/package-json" "^1.0.1" + "@npmcli/run-script" "^2.0.0" + bin-links "^2.3.0" + cacache "^15.0.3" + common-ancestor-path "^1.0.1" + json-parse-even-better-errors "^2.3.1" + json-stringify-nice "^1.1.4" + mkdirp "^1.0.4" + mkdirp-infer-owner "^2.0.0" + npm-install-checks "^4.0.0" + npm-package-arg "^8.1.5" + npm-pick-manifest "^6.1.0" + npm-registry-fetch "^11.0.0" + pacote "^12.0.2" + parse-conflict-json "^2.0.1" + proc-log "^1.0.0" + promise-all-reject-late "^1.0.0" + promise-call-limit "^1.0.1" + read-package-json-fast "^2.0.2" + readdir-scoped-modules "^1.1.0" + rimraf "^3.0.2" + semver "^7.3.5" + ssri "^8.0.1" + treeverse "^1.0.4" + walk-up-path "^1.0.0" + +"@npmcli/ci-detect@^1.3.0", "@npmcli/ci-detect@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz#18478bbaa900c37bfbd8a2006a6262c62e8b0fe1" + integrity sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q== + +"@npmcli/config@^2.3.2": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@npmcli/config/-/config-2.4.0.tgz#1447b0274f9502871dabd3ab1d8302472d515b1f" + integrity sha512-fwxu/zaZnvBJohXM3igzqa3P1IVYWi5N343XcKvKkJbAx+rTqegS5tAul4NLiMPQh6WoS5a4er6oo/ieUx1f4g== + dependencies: + ini "^2.0.0" + mkdirp-infer-owner "^2.0.0" + nopt "^5.0.0" + semver "^7.3.4" + walk-up-path "^1.0.0" + +"@npmcli/disparity-colors@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/disparity-colors/-/disparity-colors-1.0.1.tgz#b23c864c9658f9f0318d5aa6d17986619989535c" + integrity sha512-kQ1aCTTU45mPXN+pdAaRxlxr3OunkyztjbbxDY/aIcPS5CnCUrx+1+NvA6pTcYR7wmLZe37+Mi5v3nfbwPxq3A== + dependencies: + ansi-styles "^4.3.0" + +"@npmcli/fs@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.0.tgz#bec1d1b89c170d40e1b73ad6c943b0b75e7d2951" + integrity sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/git@^2.0.7", "@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + npm-pick-manifest "^6.1.1" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6", "@npmcli/installed-package-contents@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +"@npmcli/map-workspaces@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/map-workspaces/-/map-workspaces-2.0.0.tgz#e342efbbdd0dad1bba5d7723b674ca668bf8ac5a" + integrity sha512-QBJfpCY1NOAkkW3lFfru9VTdqvMB2TN0/vrevl5xBCv5Fi0XDVcA6rqqSau4Ysi4Iw3fBzyXV7hzyTBDfadf7g== + dependencies: + "@npmcli/name-from-folder" "^1.0.1" + glob "^7.1.6" + minimatch "^3.0.4" + read-package-json-fast "^2.0.1" + +"@npmcli/metavuln-calculator@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/metavuln-calculator/-/metavuln-calculator-2.0.0.tgz#70937b8b5a5cad5c588c8a7b38c4a8bd6f62c84c" + integrity sha512-VVW+JhWCKRwCTE+0xvD6p3uV4WpqocNYYtzyvenqL/u1Q3Xx6fGTJ+6UoIoii07fbuEO9U3IIyuGY0CYHDv1sg== + dependencies: + cacache "^15.0.5" + json-parse-even-better-errors "^2.3.1" + pacote "^12.0.0" + semver "^7.3.2" + +"@npmcli/move-file@^1.0.1", "@npmcli/move-file@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@npmcli/name-from-folder@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/name-from-folder/-/name-from-folder-1.0.1.tgz#77ecd0a4fcb772ba6fe927e2e2e155fbec2e6b1a" + integrity sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA== + +"@npmcli/node-gyp@^1.0.2", "@npmcli/node-gyp@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz#a912e637418ffc5f2db375e93b85837691a43a33" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== + +"@npmcli/package-json@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-1.0.1.tgz#1ed42f00febe5293c3502fd0ef785647355f6e89" + integrity sha512-y6jnu76E9C23osz8gEMBayZmaZ69vFOIk8vR1FJL/wbEJ54+9aVG9rLTjQKSXfgYZEr50nw1txBBFfBZZe+bYg== + dependencies: + json-parse-even-better-errors "^2.3.1" + +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^1.8.2": + version "1.8.6" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.6.tgz#18314802a6660b0d4baa4c3afe7f1ad39d8c28b7" + integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^7.1.0" + read-package-json-fast "^2.0.1" + +"@npmcli/run-script@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-2.0.0.tgz#9949c0cab415b17aaac279646db4f027d6f1e743" + integrity sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^8.2.0" + read-package-json-fast "^2.0.1" + "@react-native-async-storage/async-storage@^1.15.8": version "1.15.8" resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.15.8.tgz#c2263646b7261803125b555cf1bbd48b72dedafc" @@ -2177,24 +2345,11 @@ color "^3.1.3" warn-once "^0.1.0" -"@react-navigation/elements@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.1.2.tgz" - integrity sha512-PbPCleC1HpUlXtuP0DFNCNTEhRLd6lmB0KxY0SGRGqCemS3HpG/PajEQ1LDe7S51M03a1tDby1MfKTkNanUXAg== - "@react-navigation/elements@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.2.1.tgz#86f19781c6f34a5c9dd25dca99915e0306f477d1" integrity sha512-EnmAbKMsptrliRKf95rdgS6BhMjML+mIns06+G1Vdih6BrEo7/0iytThUv3WBf99AI76dyEq/cqLUwHPiFzXWg== -"@react-navigation/native-stack@^6.1.0": - version "6.2.2" - resolved "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.2.2.tgz" - integrity sha512-OhWW1R0ZgAnWAvuhPZaBiEuMF13j7+BaNB8KmxQTTjG41EdzUi5yvz5MbHvzLjhF3yJ3qHxr6HjxFRMbugWvjw== - dependencies: - "@react-navigation/elements" "^1.1.2" - warn-once "^0.1.0" - "@react-navigation/native@^6.0.2": version "6.0.4" resolved "https://registry.npmjs.org/@react-navigation/native/-/native-6.0.4.tgz" @@ -2211,6 +2366,15 @@ dependencies: nanoid "^3.1.23" +"@react-navigation/stack@^6.0.11": + version "6.0.11" + resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-6.0.11.tgz#9a94089da7a6bb6b41084c2b9e88ef542d41345d" + integrity sha512-Osc2mXi0Zh/u92HRCceDqVfVnypTa2sZgYMJDU+vDhHz38negtbCG+cjje6nApSjwC5WTVhYP4OoD5WBSh51+g== + dependencies: + "@react-navigation/elements" "^1.2.1" + color "^3.1.3" + warn-once "^0.1.0" + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" @@ -2218,6 +2382,11 @@ dependencies: type-detect "4.0.8" +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@types/babel__core@^7.1.7": version "7.1.16" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz" @@ -2379,6 +2548,11 @@ abab@^2.0.0: resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== +abbrev@1, abbrev@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -2422,6 +2596,30 @@ acorn@^7.1.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.1.3: + version "4.2.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.0.tgz#616ce94ccb41d1a39a45d203d8076fe98713062d" + integrity sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv@^6.12.3: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -2518,7 +2716,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -2530,6 +2728,16 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= +ansicolors@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" + integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= + +ansistyles@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" + integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= + any-base@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz" @@ -2556,6 +2764,37 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +archy@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" @@ -2626,7 +2865,7 @@ array-unique@^0.3.2: resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -asap@~2.0.3, asap@~2.0.6: +asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= @@ -2672,6 +2911,11 @@ async@^2.4.0: dependencies: lodash "^4.17.14" +async@~0.2.7: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -2894,6 +3138,23 @@ big-integer@^1.6.44: resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.49.tgz" integrity sha512-KJ7VhqH+f/BOt9a3yMwJNmcZjG53ijWMTjSAGMveQWyLwqIiwkjNP5PFgDob3Snnx86SjDj6I89fIbv0dkQeNw== +bin-links@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-2.3.0.tgz#1ff241c86d2c29b24ae52f49544db5d78a4eb967" + integrity sha512-JzrOLHLwX2zMqKdyYZjkDgQGT+kHDkIhv2/IK2lJ00qLxV4TmFoHi8drDBb6H5Zrz1YfgHkai4e2MGPqnoUhqA== + dependencies: + cmd-shim "^4.0.1" + mkdirp-infer-owner "^2.0.0" + npm-normalize-package-bin "^1.0.0" + read-cmd-shim "^2.0.0" + rimraf "^3.0.0" + write-file-atomic "^3.0.3" + +binary-extensions@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -3032,11 +3293,40 @@ buffer@^5.2.0: base64-js "^1.3.1" ieee754 "^1.1.13" +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + bytes@3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +cacache@^15.0.3, cacache@^15.0.5, cacache@^15.2.0, cacache@^15.3.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz" @@ -3147,11 +3437,23 @@ chardet@^0.4.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz" integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +cidr-regex@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-3.1.1.tgz#ba1972c57c66f61875f18fd7dd487469770b571d" + integrity sha512-RBqYd32aDwbCMFJRL6wHOlDNYJsPNTt8vC82ErHF5vKt8QQzxm1FrkW8s/R5pVrXMf17sba09Uoy91PKiddAsw== + dependencies: + ip-regex "^4.1.0" + class-utils@^0.3.5: version "0.3.6" resolved "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz" @@ -3162,6 +3464,19 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-columns@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-4.0.0.tgz#9fe4d65975238d55218c41bd2ed296a7fa555646" + integrity sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ== + dependencies: + string-width "^4.2.3" + strip-ansi "^6.0.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz" @@ -3174,6 +3489,15 @@ cli-spinners@^2.0.0: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.0.tgz" integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== +cli-table3@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8" + integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA== + dependencies: + string-width "^4.2.0" + optionalDependencies: + colors "1.4.0" + cli-width@^2.0.0: version "2.2.1" resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz" @@ -3211,11 +3535,23 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +cmd-shim@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" @@ -3273,7 +3609,7 @@ color-string@^1.5.3, color-string@^1.6.0: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-support@^1.1.3: +color-support@^1.1.2, color-support@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -3304,6 +3640,11 @@ colornames@0.0.2: resolved "https://registry.yarnpkg.com/colornames/-/colornames-0.0.2.tgz#d811fd6c84f59029499a8ac4436202935b92be31" integrity sha1-2BH9bIT1kClJmorEQ2ICk1uSvjE= +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + colorspace@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.0.1.tgz#c99c796ed31128b9876a52e1ee5ee03a4a719749" @@ -3312,6 +3653,14 @@ colorspace@1.0.x: color "0.8.x" text-hex "0.0.x" +columnify@~1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" + integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= + dependencies: + strip-ansi "^3.0.0" + wcwidth "^1.0.0" + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -3319,6 +3668,13 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +combined-stream@~0.0.4: + version "0.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-0.0.7.tgz#0137e657baa5a7541c57ac37ac5fc07d73b4dc1f" + integrity sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8= + dependencies: + delayed-stream "0.0.5" + command-exists@^1.2.8: version "1.2.9" resolved "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz" @@ -3349,6 +3705,11 @@ commander@~2.13.0: resolved "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz" integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== +common-ancestor-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz#4f7d2d1394d91b7abdf51871c62f71eadb0182a7" + integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" @@ -3416,6 +3777,11 @@ connect@^3.6.5: parseurl "~1.3.3" utils-merge "1.0.1" +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" @@ -3606,6 +3972,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@4: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" @@ -3613,6 +3986,11 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "2.1.2" +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" @@ -3681,17 +4059,27 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +delayed-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.5.tgz#d4b1f43a93e8296dfe02694f4680bc37a313c73f" + integrity sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8= + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + denodeify@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz" integrity sha1-OjYof1A05pnnV3kBBSwubJQlFjE= -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -3706,6 +4094,14 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + diagnostics@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.0.1.tgz#accdb080c82bb25d0dd73430a9e6a87fbb431541" @@ -3720,6 +4116,11 @@ diff-sequences@^25.2.6: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz" integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -3808,7 +4209,7 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding@^0.1.11: +encoding@^0.1.11, encoding@^0.1.12: version "0.1.13" resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== @@ -3827,6 +4228,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + env-variable@0.0.x: version "0.0.6" resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.6.tgz#74ab20b3786c545b62b4a4813ab8cf22726c9808" @@ -3837,6 +4243,11 @@ envinfo@^7.7.2: resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -4043,6 +4454,15 @@ expo-constants@~11.0.1, expo-constants@~11.0.2: expo-modules-core "~0.2.0" uuid "^3.3.2" +expo-document-picker@~9.2.4: + version "9.2.4" + resolved "https://registry.yarnpkg.com/expo-document-picker/-/expo-document-picker-9.2.4.tgz#8323ba2fcc62039ba2b31d0314aefd42ad48b7e7" + integrity sha512-j8hmk1LqGVRpYbjMVsg26DOcBo8B1r5/SpLfNNb9X8AiPx7ceQ5HC3kC0BFwk+Ad2/NHHRQ4jFQFnkvMuDkhEg== + dependencies: + "@expo/config-plugins" "^3.0.0" + expo-modules-core "~0.2.0" + uuid "^3.3.2" + expo-error-recovery@~2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/expo-error-recovery/-/expo-error-recovery-2.2.0.tgz" @@ -4279,6 +4699,11 @@ fast-levenshtein@~2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastest-levenshtein@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" + integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + fastq@^1.6.0: version "1.13.0" resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" @@ -4482,6 +4907,15 @@ forever-agent@~0.6.1: resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@~0.0.3: + version "0.0.10" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-0.0.10.tgz#db345a5378d86aeeb1ed5d553b869ac192d2f5ed" + integrity sha1-2zRaU3jYau6x7V1VO4aawZLS9e0= + dependencies: + async "~0.2.7" + combined-stream "~0.0.4" + mime "~1.2.2" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" @@ -4550,6 +4984,13 @@ fs-extra@^9.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -4596,6 +5037,35 @@ fusing@1.0.x: emits "3.0.x" predefine "0.1.x" +gauge@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8" + integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw== + dependencies: + ansi-regex "^5.0.1" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" @@ -4679,6 +5149,18 @@ glob@7.1.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global@~4.4.0: version "4.4.0" resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz" @@ -4697,6 +5179,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3 resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +graceful-fs@^4.2.3, graceful-fs@^4.2.6, graceful-fs@^4.2.8: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + growly@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz" @@ -4737,6 +5224,11 @@ has-symbols@^1.0.1: resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + has-value@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz" @@ -4799,6 +5291,13 @@ hosted-git-info@^2.1.4: resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +hosted-git-info@^4.0.1, hosted-git-info@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz" @@ -4811,6 +5310,11 @@ html-escaper@^2.0.0: resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-cache-semantics@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + http-errors@~1.7.2: version "1.7.3" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz" @@ -4822,6 +5326,15 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" @@ -4831,11 +5344,26 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: version "1.0.4" resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz" @@ -4860,6 +5388,20 @@ ieee754@^1.1.13: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== +ignore-walk@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== + dependencies: + minimatch "^3.0.4" + +ignore-walk@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-4.0.1.tgz#fc840e8346cf88a3a9380c5b17933cd8f4d39fa3" + integrity sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw== + dependencies: + minimatch "^3.0.4" + image-size@^0.6.0: version "0.6.3" resolved "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz" @@ -4886,6 +5428,16 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -4899,6 +5451,24 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +init-package-json@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/init-package-json/-/init-package-json-2.0.5.tgz#78b85f3c36014db42d8f32117252504f68022646" + integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== + dependencies: + npm-package-arg "^8.1.5" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "^4.1.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + inline-style-prefixer@^5.1.0: version "5.1.2" resolved "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-5.1.2.tgz" @@ -4938,6 +5508,11 @@ ip-regex@^2.1.0: resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= +ip-regex@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + ip@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz" @@ -4979,6 +5554,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-cidr@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-4.0.2.tgz#94c7585e4c6c77ceabf920f8cde51b8c0fda8814" + integrity sha512-z4a1ENUajDbEl/Q6/pVBpTR1nBjjEE1X7qb7bmWYanNnPoKAvUCPFKeXV6Fe4mgTkWKBqiHIcwsI3SndiO5FeA== + dependencies: + cidr-regex "^3.1.1" + is-core-module@^2.2.0: version "2.7.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz" @@ -4986,6 +5568,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-core-module@^2.5.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz" @@ -5045,6 +5634,13 @@ is-extglob@^2.1.1: resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" @@ -5072,6 +5668,11 @@ is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + is-number@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz" @@ -5798,7 +6399,7 @@ json-parse-better-errors@^1.0.1: resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== -json-parse-even-better-errors@^2.3.0: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -5820,6 +6421,11 @@ json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" +json-stringify-nice@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz#2c937962b80181d3f317dd39aa323e14f5a60a67" + integrity sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw== + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" @@ -5872,6 +6478,11 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" @@ -5882,6 +6493,16 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +just-diff-apply@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-4.0.1.tgz#da89c5a4ccb14aa8873c70e2c3b6695cef45dab5" + integrity sha512-AKOkzB5P6FkfP21UlZVX/OPXx/sC2GagpLX9cBxqHqDuRjwmZ/AJRKSNrB9jHPpRW1W1ONs6gly1gW46t055nQ== + +just-diff@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-5.0.1.tgz#db8fe1cfeea1156f2374bfb289826dca28e7e390" + integrity sha512-X00TokkRIDotUIf3EV4xUm6ELc/IkqhS/vPSHdWnsM5y0HoNMfEqrazizI7g78lpHvnRSRt/PFfKtRqJCOGIuQ== + kind-of@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz" @@ -5943,6 +6564,116 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libnpmaccess@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-4.0.3.tgz#dfb0e5b0a53c315a2610d300e46b4ddeb66e7eec" + integrity sha512-sPeTSNImksm8O2b6/pf3ikv4N567ERYEpeKRPSmqlNt1dTZbvgpJIzg5vAhXHpw2ISBsELFRelk0jEahj1c6nQ== + dependencies: + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + +libnpmdiff@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/libnpmdiff/-/libnpmdiff-2.0.4.tgz#bb1687992b1a97a8ea4a32f58ad7c7f92de53b74" + integrity sha512-q3zWePOJLHwsLEUjZw3Kyu/MJMYfl4tWCg78Vl6QGSfm4aXBUSVzMzjJ6jGiyarsT4d+1NH4B1gxfs62/+y9iQ== + dependencies: + "@npmcli/disparity-colors" "^1.0.1" + "@npmcli/installed-package-contents" "^1.0.7" + binary-extensions "^2.2.0" + diff "^5.0.0" + minimatch "^3.0.4" + npm-package-arg "^8.1.1" + pacote "^11.3.0" + tar "^6.1.0" + +libnpmexec@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/libnpmexec/-/libnpmexec-3.0.2.tgz#2c14d77254245c22d3ff0d6ff8f25aafe7d84ad0" + integrity sha512-VOXAeBAna2feIptY08UArQuoMr4SuioFgW57QpcLMAom8+pfWm9q0TazRGMuFO9urB/XG/Gx4APpQeTpdYKSkw== + dependencies: + "@npmcli/arborist" "^4.0.0" + "@npmcli/ci-detect" "^1.3.0" + "@npmcli/run-script" "^2.0.0" + chalk "^4.1.0" + mkdirp-infer-owner "^2.0.0" + npm-package-arg "^8.1.2" + pacote "^12.0.0" + proc-log "^1.0.0" + read "^1.0.7" + read-package-json-fast "^2.0.2" + walk-up-path "^1.0.0" + +libnpmfund@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/libnpmfund/-/libnpmfund-2.0.2.tgz#90a7aa26c8b9b4739a06e314f83decd198b3f9c6" + integrity sha512-7gznxLV71t9KsC9jyV6ILbLjfebettTzn13TVl29hwzDpiG+NkA7xZ7yT0L9e7DI8CVUVIxvvHKUl3Ny+TNQ8Q== + dependencies: + "@npmcli/arborist" "^4.0.0" + +libnpmhook@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/libnpmhook/-/libnpmhook-6.0.3.tgz#1d7f0d7e6a7932fbf7ce0881fdb0ed8bf8748a30" + integrity sha512-3fmkZJibIybzmAvxJ65PeV3NzRc0m4xmYt6scui5msocThbEp4sKFT80FhgrCERYDjlUuFahU6zFNbJDHbQ++g== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^11.0.0" + +libnpmorg@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/libnpmorg/-/libnpmorg-2.0.3.tgz#4e605d4113dfa16792d75343824a0625c76703bc" + integrity sha512-JSGl3HFeiRFUZOUlGdiNcUZOsUqkSYrg6KMzvPZ1WVZ478i47OnKSS0vkPmX45Pai5mTKuwIqBMcGWG7O8HfdA== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^11.0.0" + +libnpmpack@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/libnpmpack/-/libnpmpack-3.0.1.tgz#bad1930e57a415239ea4f0419b4796bc4bf8009b" + integrity sha512-xTE/nlvAZfh/Drm/jsSieGAhjKBo9uRjlU/i50IeFZKwKYwCQTPuyT3ZXiTgjhwWT+/FMVd2afRb6uGMdViFvQ== + dependencies: + "@npmcli/run-script" "^2.0.0" + npm-package-arg "^8.1.0" + pacote "^12.0.0" + +libnpmpublish@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/libnpmpublish/-/libnpmpublish-4.0.2.tgz#be77e8bf5956131bcb45e3caa6b96a842dec0794" + integrity sha512-+AD7A2zbVeGRCFI2aO//oUmapCwy7GHqPXFJh3qpToSRNU+tXKJ2YFUgjt04LPPAf2dlEH95s6EhIHM1J7bmOw== + dependencies: + normalize-package-data "^3.0.2" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + semver "^7.1.3" + ssri "^8.0.1" + +libnpmsearch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/libnpmsearch/-/libnpmsearch-3.1.2.tgz#aee81b9e4768750d842b627a3051abc89fdc15f3" + integrity sha512-BaQHBjMNnsPYk3Bl6AiOeVuFgp72jviShNBw5aHaHNKWqZxNi38iVNoXbo6bG/Ccc/m1To8s0GtMdtn6xZ1HAw== + dependencies: + npm-registry-fetch "^11.0.0" + +libnpmteam@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/libnpmteam/-/libnpmteam-2.0.4.tgz#9dbe2e18ae3cb97551ec07d2a2daf9944f3edc4c" + integrity sha512-FPrVJWv820FZFXaflAEVTLRWZrerCvfe7ZHSMzJ/62EBlho2KFlYKjyNEsPW3JiV7TLSXi3vo8u0gMwIkXSMTw== + dependencies: + aproba "^2.0.0" + npm-registry-fetch "^11.0.0" + +libnpmversion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/libnpmversion/-/libnpmversion-2.0.2.tgz#9fc1b94f5a2d0ae8c6378af498f3f44248f467d6" + integrity sha512-Dg+ccHL/F8BQgEeE9n8OU3T1FXhbdAKaj5+OYYPUrcrXkMh9EhVQ8uIbxCl0FQUeQHeWW9XmfO2icZ5YcZQvbQ== + dependencies: + "@npmcli/git" "^2.0.7" + "@npmcli/run-script" "^2.0.0" + json-parse-even-better-errors "^2.3.1" + semver "^7.3.5" + stringify-package "^1.0.1" + licenses@0.0.x: version "0.0.20" resolved "https://registry.yarnpkg.com/licenses/-/licenses-0.0.20.tgz#f18a57b26a78eaf28a873e2a378a33e81f59d136" @@ -6133,6 +6864,28 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" +make-fetch-happen@^9.0.1, make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + makeerror@1.0.x: version "1.0.11" resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz" @@ -6485,6 +7238,11 @@ mime@^2.4.1, mime@^2.4.4: resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== +mime@~1.2.2, mime@~1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10" + integrity sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA= + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz" @@ -6514,6 +7272,68 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz" @@ -6522,6 +7342,15 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== + dependencies: + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" + mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" @@ -6529,6 +7358,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.4: dependencies: minimist "^1.2.5" +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mockdate@^3.0.2: version "3.0.5" resolved "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz" @@ -6549,11 +7383,21 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.0.0, ms@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" @@ -6600,7 +7444,7 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -negotiator@0.6.2: +negotiator@0.6.2, negotiator@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== @@ -6635,6 +7479,38 @@ node-fetch@^2.2.0, node-fetch@^2.6.0: dependencies: whatwg-url "^5.0.0" +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.3" + nopt "^5.0.0" + npmlog "^4.1.2" + request "^2.88.2" + rimraf "^3.0.2" + semver "^7.3.2" + tar "^6.0.2" + which "^2.0.2" + +node-gyp@^8.2.0, node-gyp@^8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -6666,6 +7542,13 @@ node-stream-zip@^1.9.1: resolved "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz" integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + normalize-css-color@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz" @@ -6681,6 +7564,16 @@ normalize-package-data@^2.5.0: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" @@ -6702,6 +7595,90 @@ normalize-url@^2.0.1: query-string "^5.0.1" sort-keys "^2.0.0" +npm-audit-report@^2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-2.1.5.tgz#a5b8850abe2e8452fce976c8960dd432981737b5" + integrity sha512-YB8qOoEmBhUH1UJgh1xFAv7Jg1d+xoNhsDYiFQlEFThEBui0W1vIz2ZK6FVg4WZjwEdl7uBQlm1jy3MUfyHeEw== + dependencies: + chalk "^4.0.0" + +npm-bundled@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.1, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: + version "8.1.5" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== + dependencies: + hosted-git-info "^4.0.1" + semver "^7.3.4" + validate-npm-package-name "^3.0.0" + +npm-packlist@^2.1.4: + version "2.2.2" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8" + integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== + dependencies: + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-packlist@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-3.0.0.tgz#0370df5cfc2fcc8f79b8f42b37798dd9ee32c2a9" + integrity sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ== + dependencies: + glob "^7.1.6" + ignore-walk "^4.0.1" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.0, npm-pick-manifest@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" + integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== + dependencies: + npm-install-checks "^4.0.0" + npm-normalize-package-bin "^1.0.1" + npm-package-arg "^8.1.2" + semver "^7.3.4" + +npm-profile@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/npm-profile/-/npm-profile-5.0.4.tgz#73e5bd1d808edc2c382d7139049cc367ac43161b" + integrity sha512-OKtU7yoAEBOnc8zJ+/uo5E4ugPp09sopo+6y1njPp+W99P8DvQon3BJYmpvyK2Bf1+3YV5LN1bvgXRoZ1LUJBA== + dependencies: + npm-registry-fetch "^11.0.0" + +npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + npm-registry@0.1.x, npm-registry@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/npm-registry/-/npm-registry-0.1.13.tgz#9e5d8b2fdfc1ab5990d47f7debbe231d79a9e822" @@ -6727,6 +7704,108 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" +npm-user-validate@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561" + integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw== + +npm@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/npm/-/npm-8.3.0.tgz#03d32b0ddb07a5865726baf7149bb0475023df4d" + integrity sha512-ug4xToae4Dh3yZh8Fp6MOnAPSS3fqCTANpJx1fXP2C4LTUzoZf7rEantHQR/ANPVYDBe5qQT4tGVsoPqqiYZMw== + dependencies: + "@isaacs/string-locale-compare" "^1.1.0" + "@npmcli/arborist" "^4.1.1" + "@npmcli/ci-detect" "^1.4.0" + "@npmcli/config" "^2.3.2" + "@npmcli/map-workspaces" "^2.0.0" + "@npmcli/package-json" "^1.0.1" + "@npmcli/run-script" "^2.0.0" + abbrev "~1.1.1" + ansicolors "~0.3.2" + ansistyles "~0.1.3" + archy "~1.0.0" + cacache "^15.3.0" + chalk "^4.1.2" + chownr "^2.0.0" + cli-columns "^4.0.0" + cli-table3 "^0.6.0" + columnify "~1.5.4" + fastest-levenshtein "^1.0.12" + glob "^7.2.0" + graceful-fs "^4.2.8" + hosted-git-info "^4.0.2" + ini "^2.0.0" + init-package-json "^2.0.5" + is-cidr "^4.0.2" + json-parse-even-better-errors "^2.3.1" + libnpmaccess "^4.0.2" + libnpmdiff "^2.0.4" + libnpmexec "^3.0.1" + libnpmfund "^2.0.1" + libnpmhook "^6.0.2" + libnpmorg "^2.0.2" + libnpmpack "^3.0.0" + libnpmpublish "^4.0.1" + libnpmsearch "^3.1.1" + libnpmteam "^2.0.3" + libnpmversion "^2.0.1" + make-fetch-happen "^9.1.0" + minipass "^3.1.6" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + mkdirp-infer-owner "^2.0.0" + ms "^2.1.2" + node-gyp "^8.4.1" + nopt "^5.0.0" + npm-audit-report "^2.1.5" + npm-install-checks "^4.0.0" + npm-package-arg "^8.1.5" + npm-pick-manifest "^6.1.1" + npm-profile "^5.0.3" + npm-registry-fetch "^11.0.0" + npm-user-validate "^1.0.1" + npmlog "^6.0.0" + opener "^1.5.2" + pacote "^12.0.2" + parse-conflict-json "^2.0.1" + proc-log "^1.0.0" + qrcode-terminal "^0.12.0" + read "~1.0.7" + read-package-json "^4.1.1" + read-package-json-fast "^2.0.3" + readdir-scoped-modules "^1.1.0" + rimraf "^3.0.2" + semver "^7.3.5" + ssri "^8.0.1" + tar "^6.1.11" + text-table "~0.2.0" + tiny-relative-date "^1.3.0" + treeverse "^1.0.4" + validate-npm-package-name "~3.0.0" + which "^2.0.2" + write-file-atomic "^3.0.3" + +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +npmlog@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.0.tgz#ba9ef39413c3d936ea91553db7be49c34ad0520c" + integrity sha512-03ppFRGlsyUaQFbGC2C8QWJN/C/K7PsfyD9aQdhVKAQIH4sQBc8WASqFBP7O+Ut4d2oo5LoeoboB3cGdBZSp6Q== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.0" + set-blocking "^2.0.0" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -6739,6 +7818,11 @@ nullthrows@^1.1.1: resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + nwsapi@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz" @@ -6847,6 +7931,11 @@ open@^6.2.0: dependencies: is-wsl "^1.1.0" +opener@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" + integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" @@ -6945,6 +8034,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" @@ -6955,6 +8051,56 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pacote@^11.3.0: + version "11.3.5" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + +pacote@^12.0.0, pacote@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-12.0.2.tgz#14ae30a81fe62ec4fc18c071150e6763e932527c" + integrity sha512-Ar3mhjcxhMzk+OVZ8pbnXdb0l8+pimvlsqBGRNkble2NVgyqOGE3yrCGi/lAYq7E7NRDMz89R1Wx5HIMCGgeYg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^2.0.0" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^3.0.0" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + pako@^1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" @@ -6978,6 +8124,15 @@ parse-bmfont-xml@^1.1.4: xml-parse-from-string "^1.0.0" xml2js "^0.4.5" +parse-conflict-json@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/parse-conflict-json/-/parse-conflict-json-2.0.1.tgz#76647dd072e6068bcaff20be6ccea68a18e1fb58" + integrity sha512-Y7nYw+QaSGBto1LB9lgwOR05Rtz5SbuTf+Oe7HJ6SYQ/DHsvRjQ8O03oWdJbvkt6GzDWospgyZbGmjDYL0sDgA== + dependencies: + json-parse-even-better-errors "^2.3.1" + just-diff "^5.0.1" + just-diff-apply "^4.0.1" + parse-headers@^2.0.0: version "2.0.4" resolved "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.4.tgz" @@ -7209,6 +8364,11 @@ pretty-format@^26.4.0: ansi-styles "^4.0.0" react-is "^17.0.1" +proc-log@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-1.0.0.tgz#0d927307401f69ed79341e83a0b2c9a13395eb77" + integrity sha512-aCk8AO51s+4JyuYGg3Q/a6gnrlDO09NpVWePtjp7xwphcoQ04x5WAfCyugcsbLooWcMJ87CLkD4+604IckEdhg== + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -7219,6 +8379,29 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +promise-all-reject-late@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz#f8ebf13483e5ca91ad809ccc2fcf25f26f8643c2" + integrity sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw== + +promise-call-limit@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-call-limit/-/promise-call-limit-1.0.1.tgz#4bdee03aeb85674385ca934da7114e9bcd3c6e24" + integrity sha512-3+hgaa19jzCGLuSCbieeRsu5C2joKfYn8pY6JAuXFRVfF4IO+L7UPpFWNTeWT9pM7uhskvbPPd/oEOktCn317Q== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + promise@^7.1.1: version "7.3.1" resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz" @@ -7241,6 +8424,13 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.0: kleur "^3.0.3" sisteransi "^1.0.5" +promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee" + integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= + dependencies: + read "1" + prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz" @@ -7278,6 +8468,11 @@ qr.js@0.0.0: resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= +qrcode-terminal@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" + integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== + qs@^6.5.0: version "6.10.1" resolved "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz" @@ -7385,10 +8580,10 @@ react-native-nfc-manager@^3.11.4: dependencies: "@expo/config-plugins" "^3.0.6" -react-native-pager-view@5.0.12: - version "5.0.12" - resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-5.0.12.tgz#5106735d944e7f876b006377ab6a18859bf7730c" - integrity sha512-QmHUnQeP2qcxDofEOnKRmoUue0RaT55lhNJDfcQ1/SNuxif4Q2UyvDfqeItm1+toaE5tVnXqoreZh82FqUqnvw== +react-native-pager-view@^5.4.9: + version "5.4.9" + resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-5.4.9.tgz#c0d40847cfeda5a4e729b53271b0ee0fedff3eb5" + integrity sha512-D6tzxpwMGdl6CXgtskGWhKRc5cJakCazESRGt7PkqnpyiH3N35ft1KmR82pCSQetAFlytFiToeu3a/dG5CELvA== react-native-reanimated@~2.2.0: version "2.2.2" @@ -7529,6 +8724,29 @@ react@16.13.1: object-assign "^4.1.1" prop-types "^15.6.2" +read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== + +read-package-json-fast@^2.0.1, read-package-json-fast@^2.0.2, read-package-json-fast@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + +read-package-json@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-4.1.1.tgz#153be72fce801578c1c86b8ef2b21188df1b9eea" + integrity sha512-P82sbZJ3ldDrWCOSKxJT0r/CXMWR0OR3KRh55SgKo3p91GSIEEC32v3lSHAvO/UcH3/IoL7uqhOFBduAnwdldw== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" @@ -7548,7 +8766,14 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@~2.3.6: +read@1, read@^1.0.7, read@~1.0.1, read@~1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" + integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= + dependencies: + mute-stream "~0.0.4" + +readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7561,6 +8786,25 @@ readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdir-scoped-modules@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + realpath-native@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz" @@ -7653,9 +8897,17 @@ request-promise-native@^1.0.7: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@2.x.x, request@^2.88.0: +request@2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.12.0.tgz#11f46f20b3d0f4848c6383991c80790af16c8e48" + integrity sha1-EfRvILPQ9ISMY4OZHIB5CvFsjkg= + dependencies: + form-data "~0.0.3" + mime "~1.2.7" + +request@2.x.x, request@^2.88.0, request@^2.88.2: version "2.88.2" - resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" @@ -7752,6 +9004,11 @@ ret@~0.1.10: resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -7764,7 +9021,7 @@ rimraf@^2.5.4: dependencies: glob "^7.1.3" -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -7824,6 +9081,11 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, s resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz" @@ -7904,7 +9166,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.5: +semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.5" resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== @@ -7945,7 +9207,7 @@ serve-static@^1.13.1: parseurl "~1.3.3" send "0.17.1" -set-blocking@^2.0.0: +set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -8075,6 +9337,11 @@ slugify@^1.3.4: resolved "https://registry.npmjs.org/slugify/-/slugify-1.6.0.tgz" integrity sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang== +smart-buffer@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz" @@ -8105,6 +9372,23 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socks-proxy-agent@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + +socks@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.1.0" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz" @@ -8209,6 +9493,13 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stack-utils@^1.0.1: version "1.0.5" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz" @@ -8274,6 +9565,24 @@ string-length@^3.1.0: astral-regex "^1.0.0" strip-ansi "^5.2.0" +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.1.0: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" @@ -8291,14 +9600,12 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" + safe-buffer "~5.2.0" string_decoder@~1.1.1: version "1.1.1" @@ -8307,7 +9614,12 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^3.0.0: +stringify-package@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.1.tgz#e5aa3643e7f74d0f28628b72f3dad5cecfc3ba85" + integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg== + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= @@ -8406,6 +9718,18 @@ symbol-tree@^3.2.2: resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11, tar@^6.1.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" @@ -8450,6 +9774,11 @@ text-hex@0.0.x: resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-0.0.0.tgz#578fbc85a6a92636e42dd17b41d0218cce9eb2b3" integrity sha1-V4+8haapJjbkLdF7QdAhjM6esrM= +text-table@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz" @@ -8497,6 +9826,11 @@ timm@^1.6.1: resolved "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz" integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== +tiny-relative-date@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" + integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== + tinycolor2@^1.4.1: version "1.4.2" resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz" @@ -8585,6 +9919,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +treeverse@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" + integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== + ts-interface-checker@^0.1.9: version "0.1.13" resolved "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" @@ -8707,6 +10046,20 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + unique-string@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz" @@ -8781,7 +10134,7 @@ utif@^2.0.1: dependencies: pako "^1.0.5" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -8820,7 +10173,7 @@ valid-url@^1.0.9: resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= -validate-npm-package-license@^3.0.1: +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -8828,6 +10181,13 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" @@ -8842,6 +10202,13 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +version@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/version/-/version-0.1.2.tgz#ab071b0e39c9a34e9308dd8cd7845795deeca70f" + integrity sha1-qwcbDjnJo06TCN2M14RXld7spw8= + dependencies: + request "2.12.0" + vlq@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz" @@ -8863,6 +10230,11 @@ w3c-xmlserializer@^1.1.2: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" +walk-up-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-1.0.0.tgz#d4745e893dd5fd0dbb58dd0a4c6a33d9c9fec53e" + integrity sha512-hwj/qMDUEjCU5h0xr90KGCf0tg0/LgJbmOWgrWKYlcJZM7XvquvUJZ0G/HMGr7F7OQMOUuPHWP9JpriinkAlkg== + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz" @@ -8875,7 +10247,7 @@ warn-once@^0.1.0: resolved "https://registry.npmjs.org/warn-once/-/warn-once-0.1.0.tgz" integrity sha512-recZTSvuaH/On5ZU5ywq66y99lImWqzP93+AiUo9LUwG8gXHW+LJjhOd6REJHm7qb0niYqrEQJvbHSQfuJtTqA== -wcwidth@^1.0.1: +wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= @@ -8945,6 +10317,13 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +wide-align@^1.1.0, wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" @@ -8987,7 +10366,7 @@ write-file-atomic@^2.3.0: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write-file-atomic@^3.0.0: +write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== From 273756348662fade9052b01f5c38e284a99b35a5 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 19 Jan 2022 06:08:29 -0600 Subject: [PATCH 24/38] Readme File --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c86587 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +![Blitz Scouter](https://i.imgur.com/47DqYax.png "Blitz Scouter") + +# Features +- Easy to use +- Import from [The Blue Alliance](https://www.thebluealliance.com/) +- Import & Export data without internet access +- Customizable color palette + +# Download +.APKs can be downloaded and installed under the [Releases](https://github.com/NB-Blitz/BlitzScouter-Offline/releases) tab + +# Building +Requires [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) +1. Install Expo: +``` +npm install --global expo-cli +``` +2. Run Dev Client: +``` +expo start +``` +3. Scan the QR-Code that appears on screen \ No newline at end of file From 56da1d5bf809736204eb709429258aa5e7eef6ab Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 19 Jan 2022 06:11:46 -0600 Subject: [PATCH 25/38] Readme File --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c86587..d9ee3f1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Blitz Scouter](https://i.imgur.com/47DqYax.png "Blitz Scouter") +![Blitz Scouter](https://i.imgur.com/eANWZcA.png "Blitz Scouter") # Features - Easy to use From 5230f58dee42ce0061344672e9ebf63980a80467 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Thu, 27 Jan 2022 03:37:31 -0600 Subject: [PATCH 26/38] Custom Themes / Code Cleanup --- api/TBA.ts | 12 +++- api/TBAAdapter.ts | 17 ++++- app.json | 9 +-- assets/images/logo.png | Bin 18201 -> 19602 bytes context/PaletteContext.tsx | 6 +- hooks/useStats.ts | 24 ++++--- hooks/useTeam.ts | 4 ++ navigation/RootNavigator.tsx | 8 +-- screens/DefaultTeam/TeamSelectScreen.tsx | 2 +- .../{ => Download}/DownloadScreen.tsx | 12 ++-- .../{ => Download}/OnboardingScreen.tsx | 24 ++++--- .../{ => Download}/RegionalScreen.tsx | 12 ++-- .../Settings/{ => Download}/YearScreen.tsx | 14 ++-- screens/Settings/SettingsScreen.tsx | 16 +++-- screens/Sharing/ExportQRScreen.tsx | 1 - screens/{Teams => Team}/TeamBanner.tsx | 4 +- screens/Team/TeamScreen.tsx | 55 +++++++++++---- screens/Teams/TeamsScreen.tsx | 66 +++++++++++++----- types/DBTypes.ts | 4 ++ types/OtherTypes.ts | 2 +- types/TBAModels.ts | 25 +++++++ 21 files changed, 223 insertions(+), 94 deletions(-) rename screens/Settings/{ => Download}/DownloadScreen.tsx (82%) rename screens/Settings/{ => Download}/OnboardingScreen.tsx (87%) rename screens/Settings/{ => Download}/RegionalScreen.tsx (89%) rename screens/Settings/{ => Download}/YearScreen.tsx (86%) rename screens/{Teams => Team}/TeamBanner.tsx (87%) diff --git a/api/TBA.ts b/api/TBA.ts index beb1b4f..62d64e6 100644 --- a/api/TBA.ts +++ b/api/TBA.ts @@ -1,5 +1,5 @@ import { Linking } from 'react-native'; -import { TBAEvent, TBAMatch, TBAMedia, TBAStatus, TBATeam } from '../types/TBAModels'; +import { TBAEvent, TBAMatch, TBAMedia, TBARankings, TBAStatus, TBATeam } from '../types/TBAModels'; const API_KEY = "i90dAcKHXvQ9havypHJKeGY8O1tfymFpaW1Po3RGYpvoMTRVwtiUsUFaLmstCDp3"; const URL_PREFIX = "https://www.thebluealliance.com/api/v3/"; @@ -16,6 +16,15 @@ export default class TBA { return TBA._fetch("event/" + eventID + "/matches/simple"); } + /** + * Fetches team stats and rankings at a given event + * @param eventID - ID of the event + * @returns Ranking data + */ + static getRankings(eventID: string) { + return TBA._fetch("event/" + eventID + "/rankings"); + } + /** * Fetches all events in a current year * @returns An array of TBAEvent @@ -93,6 +102,7 @@ export default class TBA { headers: headers }; + console.log("GET: " + URL); fetch(URL, REQUEST_DATA).then((result) => { result.json().then((json) => { resolve(json); diff --git a/api/TBAAdapter.ts b/api/TBAAdapter.ts index db37592..5eac97a 100644 --- a/api/TBAAdapter.ts +++ b/api/TBAAdapter.ts @@ -102,7 +102,8 @@ export async function DownloadMatches(eventID: string, callback: (matchNumber: n export async function DownloadTeams(eventID: string, downloadMedia: boolean, callback: (teamNumber: number) => void) { const tbaTeams = await TBA.getTeams(eventID); - if (!tbaTeams) + const tbaRanks = await TBA.getRankings(eventID); + if (!tbaTeams || !tbaRanks) return undefined; const year = parseInt(eventID.substring(0, 4)); @@ -115,6 +116,7 @@ export async function DownloadTeams(eventID: string, downloadMedia: boolean, cal callback(team.team_number); teamIDs.push(team.key); + // Media let mediaPaths: string[] = []; if (downloadMedia) mediaPaths = await DownloadMedia(team.key, year); @@ -124,10 +126,21 @@ export async function DownloadTeams(eventID: string, downloadMedia: boolean, cal mediaPaths = JSON.parse(currentTeam).mediaPaths; } + // Ranks + const rankingInfo = tbaRanks.rankings.find(rank => rank.team_key === team.key); + const rank = rankingInfo ? rankingInfo.rank : -1; + const wins = rankingInfo ? rankingInfo.record.wins : -1; + const losses = rankingInfo ? rankingInfo.record.losses : -1; + const ties = rankingInfo ? rankingInfo.record.ties : -1; + await putStorage(team.key, { id: team.key, name: team.nickname, number: team.team_number, + rank, + wins, + losses, + ties, mediaPaths, scoutingData: [] }); @@ -156,7 +169,7 @@ export async function DownloadMedia(teamID: string, year: number) { } else if (media.direct_url) { const path = FileSystem.documentDirectory + mediaID + ".jpg"; - console.log(media.direct_url + " --> " + path); + console.log("DOWNLOAD: " + media.direct_url); await FileSystem.downloadAsync(media.direct_url, path); mediaPaths.push(path); } diff --git a/app.json b/app.json index 25da716..11c5f18 100644 --- a/app.json +++ b/app.json @@ -4,8 +4,8 @@ "slug": "BlitzScouter", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/icon.png", "scheme": "blitz", + "icon": "./assets/images/icon.png", "userInterfaceStyle": "light", "primaryColor": "#856a00", "splash": { @@ -22,12 +22,13 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "com.nbblitz.blitzscouter", - "buildNumber": "1.0.0" + "buildNumber": "1.0.0", + "icon": "./assets/images/icon.png" }, "android": { "adaptiveIcon": { - "foregroundImage": "./assets/images/icon.png", - "backgroundColor": "#1e1e1e" + "foregroundImage": "./assets/images/logo.png", + "backgroundColor": "#141416" }, "package": "com.nbblitz.blitzscouter", "versionCode": 1 diff --git a/assets/images/logo.png b/assets/images/logo.png index f0496613331aa5f9d380876e7aa4017a2c852ac1..059eb4259036b968cef46c9c433a110c09003a5f 100644 GIT binary patch delta 16002 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 delta 14751 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 Ul([] as TeamStats); + const [teamStats, setTeamStats] = useState({} as TeamStats); useEffect(() => { if (team.scoutingData.length === 0 || template.length === 0) { return; } - let newStats: TeamStats = []; + let newStats: TeamStats = { + id: team.id, + metrics: [] + }; template.forEach(element => { if (typeof element.value === "number") { @@ -32,22 +38,22 @@ export default function useStats(teamID: string) { min: Number.MAX_SAFE_INTEGER }; - newStats.push(metric); + newStats.metrics.push(metric); } }); team.scoutingData.forEach(scout => { scout.values.forEach((value, index) => { if (typeof value === "number") { - newStats[index].average += value; - newStats[index].max = Math.max(newStats[index].max, value); - newStats[index].min = Math.min(newStats[index].min, value); + newStats.metrics[index].average += value; + newStats.metrics[index].max = Math.max(newStats.metrics[index].max, value); + newStats.metrics[index].min = Math.min(newStats.metrics[index].min, value); } }); }); - newStats.forEach((stat, index) => { - newStats[index].average /= team.scoutingData.length; + newStats.metrics.forEach((stat, index) => { + newStats.metrics[index].average /= team.scoutingData.length; }); setTeamStats(newStats); diff --git a/hooks/useTeam.ts b/hooks/useTeam.ts index aef683b..2d2dc7c 100644 --- a/hooks/useTeam.ts +++ b/hooks/useTeam.ts @@ -5,6 +5,10 @@ const DEFAULT_TEAM = { id: "", name: "", number: 0, + rank: -1, + wins: -1, + losses: -1, + ties: -1, mediaPaths: [], scoutingData: [] } as Team; diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx index a557eb4..fb52730 100644 --- a/navigation/RootNavigator.tsx +++ b/navigation/RootNavigator.tsx @@ -9,13 +9,13 @@ import MatchScreen from "../screens/Match/MatchScreen"; import ScoutingScreen from "../screens/Scout/ScoutingScreen"; import AboutScreen from "../screens/Settings/AboutScreen"; import { ColorPickerScreen } from "../screens/Settings/ColorPicker"; -import DownloadScreen from "../screens/Settings/DownloadScreen"; -import OnboardingScreen from "../screens/Settings/OnboardingScreen"; +import DownloadScreen from "../screens/Settings/Download/DownloadScreen"; +import OnboardingScreen from "../screens/Settings/Download/OnboardingScreen"; +import RegionalScreen from "../screens/Settings/Download/RegionalScreen"; +import YearScreen from "../screens/Settings/Download/YearScreen"; import { PaletteScreen } from "../screens/Settings/PaletteScreen"; -import RegionalScreen from "../screens/Settings/RegionalScreen"; import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen"; import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; -import YearScreen from "../screens/Settings/YearScreen"; import ExportQRScreen from "../screens/Sharing/ExportQRScreen"; import ImportQRScreen from "../screens/Sharing/ImportQRScreen"; import TeamScreen from "../screens/Team/TeamScreen"; diff --git a/screens/DefaultTeam/TeamSelectScreen.tsx b/screens/DefaultTeam/TeamSelectScreen.tsx index bab0c98..c5008d2 100644 --- a/screens/DefaultTeam/TeamSelectScreen.tsx +++ b/screens/DefaultTeam/TeamSelectScreen.tsx @@ -6,7 +6,7 @@ import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; import useMatch from '../../hooks/useMatch'; import { TemplateType } from '../../types/TemplateTypes'; -import TeamBanner from '../Teams/TeamBanner'; +import TeamBanner from '../Team/TeamBanner'; export default function TeamSelectScreen({ route }: any) { const navigator = useNavigation(); diff --git a/screens/Settings/DownloadScreen.tsx b/screens/Settings/Download/DownloadScreen.tsx similarity index 82% rename from screens/Settings/DownloadScreen.tsx rename to screens/Settings/Download/DownloadScreen.tsx index 3790738..c1cb26a 100644 --- a/screens/Settings/DownloadScreen.tsx +++ b/screens/Settings/Download/DownloadScreen.tsx @@ -1,12 +1,12 @@ import { useNavigation } from "@react-navigation/core"; import React from "react"; import { StyleSheet, View } from "react-native"; -import { DownloadEvent } from "../../api/TBAAdapter"; -import StandardButton from "../../components/common/StandardButton"; -import ScrollContainer from "../../components/containers/ScrollContainer"; -import Text from "../../components/text/Text"; -import Title from "../../components/text/Title"; -import { PaletteContext } from "../../context/PaletteContext"; +import { DownloadEvent } from "../../../api/TBAAdapter"; +import StandardButton from "../../../components/common/StandardButton"; +import ScrollContainer from "../../../components/containers/ScrollContainer"; +import Text from "../../../components/text/Text"; +import Title from "../../../components/text/Title"; +import { PaletteContext } from "../../../context/PaletteContext"; export default function DownloadScreen({ route }: any) { const paletteContext = React.useContext(PaletteContext); diff --git a/screens/Settings/OnboardingScreen.tsx b/screens/Settings/Download/OnboardingScreen.tsx similarity index 87% rename from screens/Settings/OnboardingScreen.tsx rename to screens/Settings/Download/OnboardingScreen.tsx index 2dccb0a..f70c0ed 100644 --- a/screens/Settings/OnboardingScreen.tsx +++ b/screens/Settings/Download/OnboardingScreen.tsx @@ -3,19 +3,21 @@ import * as React from "react"; import { Image, StyleSheet, View } from "react-native"; import PagerView from 'react-native-pager-view'; import Animated from "react-native-reanimated"; -import logo from "../../assets/images/logo.png"; -import tbalamp from "../../assets/images/tba_lamp.png"; -import Button from "../../components/common/Button"; -import Subtitle from "../../components/text/Subtitle"; -import Text from "../../components/text/Text"; -import Title from "../../components/text/Title"; -import useStorage from "../../hooks/useStorage"; -import TabNavigator from "../../navigation/TabNavigator"; +import logo from "../../../assets/images/logo.png"; +import tbalamp from "../../../assets/images/tba_lamp.png"; +import Button from "../../../components/common/Button"; +import Subtitle from "../../../components/text/Subtitle"; +import Text from "../../../components/text/Text"; +import Title from "../../../components/text/Title"; +import useStorage from "../../../hooks/useStorage"; +import TabNavigator from "../../../navigation/TabNavigator"; const AnimatedPagerView = Animated.createAnimatedComponent(PagerView); export default function OnboardingScreen() { const [hasOnboarded] = useStorage("onboard", false); + + // Animations const scrollOffsetAnimatedValue = React.useRef(new Animated.Value(0)).current; const positionAnimatedValue = React.useRef(new Animated.Value(0)).current; const translateX = Animated.add( @@ -28,6 +30,8 @@ export default function OnboardingScreen() { outputRange: [0, 30], }) ); + + // Navigation const navigator = useNavigation(); if (hasOnboarded) return ( @@ -52,14 +56,14 @@ export default function OnboardingScreen() { })}> - + Welcome to Blitz Scouter Blitz Scouter is a simple, easy to use scouting app for use at a FIRST{"\u00AE"} Robotics Competition. - + Import From The Blue Alliance Import Teams and Event data while your online, and continue to use them offline. diff --git a/screens/Settings/RegionalScreen.tsx b/screens/Settings/Download/RegionalScreen.tsx similarity index 89% rename from screens/Settings/RegionalScreen.tsx rename to screens/Settings/Download/RegionalScreen.tsx index 22057a4..891b696 100644 --- a/screens/Settings/RegionalScreen.tsx +++ b/screens/Settings/Download/RegionalScreen.tsx @@ -2,12 +2,12 @@ import { useNavigation } from "@react-navigation/core"; import React, { useEffect } from "react"; import { Alert, StyleSheet, View } from "react-native"; import { TextInput } from "react-native-gesture-handler"; -import TBA from "../../api/TBA"; -import Button from "../../components/common/Button"; -import ScrollContainer from "../../components/containers/ScrollContainer"; -import Text from "../../components/text/Text"; -import { PaletteContext } from "../../context/PaletteContext"; -import { TBAEvent } from "../../types/TBAModels"; +import TBA from "../../../api/TBA"; +import Button from "../../../components/common/Button"; +import ScrollContainer from "../../../components/containers/ScrollContainer"; +import Text from "../../../components/text/Text"; +import { PaletteContext } from "../../../context/PaletteContext"; +import { TBAEvent } from "../../../types/TBAModels"; export default function RegionalScreen({ route }: any) { const paletteContext = React.useContext(PaletteContext); diff --git a/screens/Settings/YearScreen.tsx b/screens/Settings/Download/YearScreen.tsx similarity index 86% rename from screens/Settings/YearScreen.tsx rename to screens/Settings/Download/YearScreen.tsx index 503fa04..1ec1242 100644 --- a/screens/Settings/YearScreen.tsx +++ b/screens/Settings/Download/YearScreen.tsx @@ -1,13 +1,13 @@ import { useNavigation } from "@react-navigation/core"; import React from "react"; import { StyleSheet, ToastAndroid } from "react-native"; -import TBA from "../../api/TBA"; -import HorizontalBar from "../../components/common/HorizontalBar"; -import StandardButton from "../../components/common/StandardButton"; -import ScrollContainer from "../../components/containers/ScrollContainer"; -import Subtitle from "../../components/text/Subtitle"; -import Text from "../../components/text/Text"; -import Title from "../../components/text/Title"; +import TBA from "../../../api/TBA"; +import HorizontalBar from "../../../components/common/HorizontalBar"; +import StandardButton from "../../../components/common/StandardButton"; +import ScrollContainer from "../../../components/containers/ScrollContainer"; +import Subtitle from "../../../components/text/Subtitle"; +import Text from "../../../components/text/Text"; +import Title from "../../../components/text/Title"; /* While hard-coding season names isn't best practice, diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index c15efb8..351cb15 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -5,7 +5,7 @@ import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; -import { DARK_PALETTE, PaletteContext } from '../../context/PaletteContext'; +import { DARK_PALETTE, LIGHT_PALETTE, PaletteContext } from '../../context/PaletteContext'; import useEvent from '../../hooks/useEvent'; import { clearStorage } from '../../hooks/useStorage'; import { TemplateType } from '../../types/TemplateTypes'; @@ -46,10 +46,16 @@ export default function SettingsScreen() { onPress={() => { navigator.navigate("Palette"); }} /> { paletteContext.setPalette(DARK_PALETTE); ToastAndroid.show("Palette Reset!", ToastAndroid.SHORT); }} /> + iconType={"lightbulb"} + title={"Light Mode"} + subtitle={"Resets to the default light palette"} + onPress={() => { paletteContext.setPalette(LIGHT_PALETTE); ToastAndroid.show("Light Mode!", ToastAndroid.SHORT); }} /> + + { paletteContext.setPalette(DARK_PALETTE); ToastAndroid.show("Dark Mode!", ToastAndroid.SHORT); }} /> diff --git a/screens/Sharing/ExportQRScreen.tsx b/screens/Sharing/ExportQRScreen.tsx index 152b880..9f44ebb 100644 --- a/screens/Sharing/ExportQRScreen.tsx +++ b/screens/Sharing/ExportQRScreen.tsx @@ -11,7 +11,6 @@ export default function ExportQRScreen({ route }: any) { const qrSize = Math.min(windowSize.width, windowSize.height); return ( - void }) { const navigator = useNavigation(); - const [team, setTeam] = useTeam(props.teamID); + const [team] = useTeam(props.teamID); + const stats = useStats(props.teamID); const onClick = () => { if (props.onClick) diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 2b65997..1100f92 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -46,6 +46,10 @@ export default function TeamScreen({ route }: any) { id: team.id, name: team.name, number: team.number, + rank: team.rank, + wins: team.wins, + losses: team.losses, + ties: team.ties, mediaPaths, scoutingData: team.scoutingData }); @@ -74,6 +78,10 @@ export default function TeamScreen({ route }: any) { id: team.id, name: team.name, number: team.number, + rank: team.rank, + wins: team.wins, + losses: team.losses, + ties: team.ties, mediaPaths, scoutingData: team.scoutingData }); @@ -86,11 +94,26 @@ export default function TeamScreen({ route }: any) { id: team.id, name: team.name, number: team.number, + rank: team.rank, + wins: team.wins, + losses: team.losses, + ties: team.ties, mediaPaths, scoutingData: team.scoutingData }); } + const printStat = (name: string, ...values: string[]) => { + return ( + {name} + + {values.map((value, index) => { + return ({value}); + })} + + ); + } + return ( @@ -110,7 +133,7 @@ export default function TeamScreen({ route }: any) { style={styles.imageButton} onPress={async () => { takePhoto(); }}> @@ -119,7 +142,7 @@ export default function TeamScreen({ route }: any) { style={styles.imageButton} onPress={async () => { uploadPhoto(); }}> @@ -145,7 +168,14 @@ export default function TeamScreen({ route }: any) { - {stats.length > 0 ? + {printStat("Rank", team.rank.toString())} + {printStat("Wins", team.wins.toString())} + {printStat("Losses", team.losses.toString())} + {printStat("Ties", team.ties.toString())} + + + + {stats.metrics.length > 0 ? Min @@ -157,15 +187,8 @@ export default function TeamScreen({ route }: any) { null } - {stats.map((stat, index) => - - {stat.label} - - {stat.min} - {stat.average} - {stat.max} - - + {stats.metrics.map((stat, index) => + printStat(stat.label, stat.min.toString(), stat.average.toString(), stat.max.toString()) )} @@ -188,14 +211,16 @@ const styles = StyleSheet.create({ thumbnail: { height: 200, width: 200, - borderRadius: 1 + borderRadius: 5 }, imageButton: { height: 200, width: 200, + marginLeft: 6, justifyContent: "center", flexDirection: "row", - backgroundColor: "#444" + backgroundColor: "#444", + borderRadius: 5 }, statLabel: { @@ -230,5 +255,7 @@ const styles = StyleSheet.create({ statsContainer: { backgroundColor: "#1b1b1b", borderRadius: 5, + marginTop: 5, + marginBottom: 5 } }); diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index 0bd32ca..81c922d 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -1,20 +1,23 @@ +import { MaterialIcons } from '@expo/vector-icons'; import * as React from 'react'; -import { ActivityIndicator, Platform, ToastAndroid, View } from 'react-native'; +import { ActivityIndicator, StyleSheet, ToastAndroid, View } from 'react-native'; import { DownloadTeams } from '../../api/TBAAdapter'; +import Button from '../../components/common/Button'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; import Text from '../../components/text/Text'; import { PaletteContext } from '../../context/PaletteContext'; import useEvent from '../../hooks/useEvent'; -import TeamBanner from './TeamBanner'; +import TeamBanner from '../Team/TeamBanner'; + +const TeamBannerHeight = 190; export default function TeamsScreen() { const paletteContext = React.useContext(PaletteContext); const [event, setEvent] = useEvent(); + // Download Teams const onRefresh = async () => { - if (Platform.OS !== "android") - return; const teamIDs = await DownloadTeams(event.id, false, () => { }); if (teamIDs) { await setEvent({ @@ -30,21 +33,46 @@ export default function TeamsScreen() { } }; - if (event.id === "bogus") - return ( - - - - ); - else - return ( - + const onFilter = () => { + + }; + + return ( + + Teams - {event.teamIDs.length <= 0 ? - This event has no teams posted yet. Pull down to refresh. - : + + + + + + + {event.id === "bogus" ? + : + event.teamIDs.length <= 0 ? + This event has no teams posted yet. Pull down to refresh. : event.teamIDs.map((teamID) => ) - } - - ); + } + + ); } + +const styles = StyleSheet.create({ + filterButton: { + width: 42, + height: 42, + marginBottom: 12, + marginRight: 5, + alignSelf: "flex-end" + }, + filterContainer: { + flexDirection: "column", + alignSelf: "flex-end", + flex: 1 + } +}) \ No newline at end of file diff --git a/types/DBTypes.ts b/types/DBTypes.ts index 88ea52e..31176ea 100644 --- a/types/DBTypes.ts +++ b/types/DBTypes.ts @@ -11,6 +11,10 @@ export interface Team { id: string; name: string; number: number; + rank: number; + wins: number; + losses: number; + ties: number; mediaPaths: string[]; scoutingData: ScoutingData[]; } diff --git a/types/OtherTypes.ts b/types/OtherTypes.ts index 8071f2b..15d8657 100644 --- a/types/OtherTypes.ts +++ b/types/OtherTypes.ts @@ -7,4 +7,4 @@ export interface Palette { navigationTextSelected: string; textPrimary: string; textSecondary: string; -} \ No newline at end of file +} diff --git a/types/TBAModels.ts b/types/TBAModels.ts index 715831f..99ddcf6 100644 --- a/types/TBAModels.ts +++ b/types/TBAModels.ts @@ -37,4 +37,29 @@ export interface TBAStatus { is_datafeed_down: boolean; max_season: number; current_season: number; +} + +export interface TBARankings { + extra_stats_info: { + name: string; + precision: number; + }[]; + rankings: { + dq: number; + extra_stats: number[]; + matches_played: number; + qual_average: number; + rank: number; + record: { + losses: number; + wins: number; + ties: number; + }; + sort_orders: number[]; + team_key: string; + }[]; + sort_order_info: { + name: string; + precision: number; + }[]; } \ No newline at end of file From 038a5c0dfe9bcb6e3c49146e6077f4db40deb79f Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Mon, 7 Feb 2022 03:06:25 -0600 Subject: [PATCH 27/38] Stat Visualization & UI Changes --- api/TBAAdapter.ts | 24 +++--- components/common/HorizontalBar.tsx | 2 +- components/common/StandardButton.tsx | 13 ++- components/elements/TextBoxElement.tsx | 2 +- components/text/NavTitle.tsx | 2 +- context/PaletteContext.tsx | 4 +- hooks/useMatch.ts | 10 ++- hooks/useStats.ts | 98 ++++++++++++--------- hooks/useTemplate.ts | 10 ++- navigation/RootNavigator.tsx | 8 +- navigation/RootParamList.tsx | 1 + package.json | 1 + screens/Match/MatchScreen.tsx | 30 +++++-- screens/Matches/MatchBanner.tsx | 2 +- screens/Matches/TeamPreview.tsx | 15 ++-- screens/Scout/ScoutingScreen.tsx | 11 ++- screens/Settings/SettingsScreen.tsx | 75 +++++++++++++++- screens/Sharing/PrintSummaryScreen.tsx | 98 +++++++++++++++++++++ screens/Sharing/SharingScreen.tsx | 8 +- screens/Team/Stats/StatSquare.tsx | 53 ++++++++++++ screens/Team/Stats/StatTable.tsx | 79 +++++++++++++++++ screens/Team/TeamBanner.tsx | 15 ++-- screens/Team/TeamScreen.tsx | 113 ++++++------------------- screens/Teams/TeamsScreen.tsx | 14 ++- yarn.lock | 7 ++ 25 files changed, 508 insertions(+), 187 deletions(-) create mode 100644 screens/Sharing/PrintSummaryScreen.tsx create mode 100644 screens/Team/Stats/StatSquare.tsx create mode 100644 screens/Team/Stats/StatTable.tsx diff --git a/api/TBAAdapter.ts b/api/TBAAdapter.ts index 5eac97a..89870b3 100644 --- a/api/TBAAdapter.ts +++ b/api/TBAAdapter.ts @@ -103,7 +103,7 @@ export async function DownloadMatches(eventID: string, callback: (matchNumber: n export async function DownloadTeams(eventID: string, downloadMedia: boolean, callback: (teamNumber: number) => void) { const tbaTeams = await TBA.getTeams(eventID); const tbaRanks = await TBA.getRankings(eventID); - if (!tbaTeams || !tbaRanks) + if (!tbaTeams) return undefined; const year = parseInt(eventID.substring(0, 4)); @@ -116,22 +116,22 @@ export async function DownloadTeams(eventID: string, downloadMedia: boolean, cal callback(team.team_number); teamIDs.push(team.key); + const currentTeamJSON = await AsyncStorage.getItem(team.key); + const currentTeam = currentTeamJSON !== null ? (JSON.parse(currentTeamJSON) as Team) : undefined; + // Media let mediaPaths: string[] = []; if (downloadMedia) mediaPaths = await DownloadMedia(team.key, year); - else { - const currentTeam = await AsyncStorage.getItem(team.key); - if (currentTeam !== null) - mediaPaths = JSON.parse(currentTeam).mediaPaths; - } + else + mediaPaths = currentTeam ? currentTeam.mediaPaths : []; // Ranks - const rankingInfo = tbaRanks.rankings.find(rank => rank.team_key === team.key); - const rank = rankingInfo ? rankingInfo.rank : -1; - const wins = rankingInfo ? rankingInfo.record.wins : -1; - const losses = rankingInfo ? rankingInfo.record.losses : -1; - const ties = rankingInfo ? rankingInfo.record.ties : -1; + const rankingInfo = tbaRanks ? tbaRanks.rankings.find(rank => rank.team_key === team.key) : undefined; + const rank = rankingInfo ? rankingInfo.rank : 0; + const wins = rankingInfo ? rankingInfo.record.wins : 0; + const losses = rankingInfo ? rankingInfo.record.losses : 0; + const ties = rankingInfo ? rankingInfo.record.ties : 0; await putStorage(team.key, { id: team.key, @@ -142,7 +142,7 @@ export async function DownloadTeams(eventID: string, downloadMedia: boolean, cal losses, ties, mediaPaths, - scoutingData: [] + scoutingData: currentTeam ? currentTeam.scoutingData : [] }); } diff --git a/components/common/HorizontalBar.tsx b/components/common/HorizontalBar.tsx index 59b4c05..9970d6e 100644 --- a/components/common/HorizontalBar.tsx +++ b/components/common/HorizontalBar.tsx @@ -12,7 +12,7 @@ export default function HorizontalBar(props: ViewProps) { style={[{ borderBottomColor: paletteContext.palette.textSecondary, borderBottomWidth: 1, - marginBottom: 15, + marginBottom: 12, marginTop: 15 }, style]} diff --git a/components/common/StandardButton.tsx b/components/common/StandardButton.tsx index 34583de..8cf4195 100644 --- a/components/common/StandardButton.tsx +++ b/components/common/StandardButton.tsx @@ -15,6 +15,7 @@ export interface ButtonProps { title: string; subtitle: string; + sidetitle?: string; onPress: (event: GestureResponderEvent) => void; } @@ -60,6 +61,7 @@ export default function StandardButton(props: ButtonProps) { {props.title} {props.subtitle} + {props.sidetitle} @@ -75,7 +77,7 @@ const styles = StyleSheet.create({ alignItems: "center", alignSelf: 'stretch', padding: 10, - paddingRight: 65, + paddingRight: 100, marginBottom: 8, borderRadius: 5 }, @@ -93,8 +95,7 @@ const styles = StyleSheet.create({ height: 60, margin: -10, marginRight: 14, - borderTopLeftRadius: 5, - borderBottomLeftRadius: 5, + borderRadius: 6 }, buttonIconTXT: { fontSize: 20, @@ -118,4 +119,10 @@ const styles = StyleSheet.create({ borderTopLeftRadius: 1, borderBottomLeftRadius: 1, }, + sidetitle: { + position: "absolute", + top: 10, + right: 10, + fontSize: 12 + } }); \ No newline at end of file diff --git a/components/elements/TextBoxElement.tsx b/components/elements/TextBoxElement.tsx index de9c4bd..408421e 100644 --- a/components/elements/TextBoxElement.tsx +++ b/components/elements/TextBoxElement.tsx @@ -42,7 +42,7 @@ const styles = StyleSheet.create({ backgroundColor: "#222222", borderRadius: 10, padding: 10, - margin: 5, + margin: 10, fontSize: 15, height: 100, textAlignVertical: "top" diff --git a/components/text/NavTitle.tsx b/components/text/NavTitle.tsx index 44b3810..7b98171 100644 --- a/components/text/NavTitle.tsx +++ b/components/text/NavTitle.tsx @@ -13,6 +13,6 @@ export default function NavTitle(props: TextProps) { fontSize: 30, fontWeight: 'bold', marginTop: 60, - marginBottom: 15 + marginBottom: 15, }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/context/PaletteContext.tsx b/context/PaletteContext.tsx index 4e7aae9..bd947b9 100644 --- a/context/PaletteContext.tsx +++ b/context/PaletteContext.tsx @@ -3,7 +3,7 @@ import { Palette } from '../types/OtherTypes'; export const DARK_PALETTE: Palette = { background: "#141416", - button: "#1b1b1b", + button: "#141416", navigation: "#2d2e30", navigationText: "#ffffff", navigationSelected: "#c89f00", @@ -14,7 +14,7 @@ export const DARK_PALETTE: Palette = { export const LIGHT_PALETTE: Palette = { background: "#ffffff", - button: "#f7f7f7", + button: "#ffffff", navigation: "#d0d0d0", navigationText: "#000000", navigationSelected: "#008bc7", diff --git a/hooks/useMatch.ts b/hooks/useMatch.ts index d189f6f..97a1a58 100644 --- a/hooks/useMatch.ts +++ b/hooks/useMatch.ts @@ -1,5 +1,5 @@ import { Match } from "../types/DBTypes"; -import useStorage from "./useStorage"; +import useStorage, { getStorage, putStorage } from "./useStorage"; const DEFAULT_MATCH = { id: "", @@ -20,4 +20,12 @@ const DEFAULT_MATCH = { export default function useMatch(matchID: string): [Match, (match: Match) => Promise] { const [matchData, setMatchData] = useStorage(matchID, DEFAULT_MATCH); return [matchData, setMatchData]; +} + +export async function getMatch(matchID: string) { + return await getStorage(matchID); +} + +export async function setMatch(match: Match) { + return await putStorage(match.id, match); } \ No newline at end of file diff --git a/hooks/useStats.ts b/hooks/useStats.ts index 1603114..fa99b0d 100644 --- a/hooks/useStats.ts +++ b/hooks/useStats.ts @@ -1,63 +1,77 @@ import { useEffect, useState } from "react"; import { TemplateType } from "../types/TemplateTypes"; -import useTeam from "./useTeam"; -import useTemplate from "./useTemplate"; +import { getEvent } from "./useEvent"; +import useTeam, { getTeam } from "./useTeam"; +import useTemplate, { getTemplate } from "./useTemplate"; -export type TeamStats = { - id: string, - metrics: TeamMetric[] -}; +export type TeamStats = TeamMetric[]; export interface TeamMetric { label: string, average: number, max: number, - min: number + min: number, + percentile: number } export default function useStats(teamID: string) { const [team] = useTeam(teamID); const [template] = useTemplate(TemplateType.Match); - const [teamStats, setTeamStats] = useState({} as TeamStats); + const [teamStats, setTeamStats] = useState([] as TeamStats); + + const getNewStats = async () => { + const newStats = await getStats(teamID); + setTeamStats(newStats); + } useEffect(() => { - if (team.scoutingData.length === 0 || template.length === 0) { - return; - } + getNewStats(); + }, [team, template, setTeamStats]); - let newStats: TeamStats = { - id: team.id, - metrics: [] - }; - - template.forEach(element => { - if (typeof element.value === "number") { - const metric: TeamMetric = { - label: element.label, - average: 0, - max: Number.MIN_SAFE_INTEGER, - min: Number.MAX_SAFE_INTEGER - }; - - newStats.metrics.push(metric); - } - }); + return teamStats; +} - team.scoutingData.forEach(scout => { - scout.values.forEach((value, index) => { - if (typeof value === "number") { - newStats.metrics[index].average += value; - newStats.metrics[index].max = Math.max(newStats.metrics[index].max, value); - newStats.metrics[index].min = Math.min(newStats.metrics[index].min, value); - } - }); - }); +export async function getStats(teamID: string) { + console.log(teamID); + const team = await getTeam(teamID); + const event = await getEvent(); + const template = await getTemplate(TemplateType.Match); - newStats.metrics.forEach((stat, index) => { - newStats.metrics[index].average /= team.scoutingData.length; + if (team === undefined || event === undefined || template === undefined) { + return [] as TeamStats; + } + if (team.scoutingData.length === 0 || template.length === 0) { + return [] as TeamStats; + } + let newStats: TeamStats = []; + + template.forEach(element => { + if (typeof element.value === "number") { + const metric: TeamMetric = { + label: element.label, + average: 0, + max: Number.MIN_SAFE_INTEGER, + min: Number.MAX_SAFE_INTEGER, + percentile: 0 + }; + + newStats.push(metric); + } + }); + + team.scoutingData.forEach(scout => { + scout.values.forEach((value, index) => { + if (typeof value === "number") { + newStats[index].average += value; + newStats[index].max = Math.max(newStats[index].max, value); + newStats[index].min = Math.min(newStats[index].min, value); + } }); + }); - setTeamStats(newStats); - }, [team, template, setTeamStats]); + newStats.forEach((stat, index) => { + newStats[index].average /= team.scoutingData.length; + newStats[index].percentile = (newStats[index].average - newStats[index].min) / (newStats[index].max - newStats[index].min); + }); - return teamStats; + return newStats; } \ No newline at end of file diff --git a/hooks/useTemplate.ts b/hooks/useTemplate.ts index 3f86445..f38086f 100644 --- a/hooks/useTemplate.ts +++ b/hooks/useTemplate.ts @@ -1,5 +1,5 @@ import { ScoutingTemplate, TemplateType } from "../types/TemplateTypes"; -import useStorage from "./useStorage"; +import useStorage, { getStorage, putStorage } from "./useStorage"; /** * Grabs template data as a react hook @@ -9,4 +9,12 @@ import useStorage from "./useStorage"; export default function useTemplate(templateType: TemplateType): [ScoutingTemplate, (template: ScoutingTemplate) => Promise] { const [templateData, setTemplateData] = useStorage("template-" + templateType, []); return [templateData, setTemplateData]; +} + +export async function getTemplate(templateType: TemplateType) { + return await getStorage("template-" + templateType); +} + +export async function setTemplate(type: TemplateType, template: ScoutingTemplate) { + return await putStorage("template-" + type, template); } \ No newline at end of file diff --git a/navigation/RootNavigator.tsx b/navigation/RootNavigator.tsx index fb52730..54acfa0 100644 --- a/navigation/RootNavigator.tsx +++ b/navigation/RootNavigator.tsx @@ -18,6 +18,7 @@ import EditTemplateScreen from "../screens/Settings/Template/EditTemplateScreen" import ElementChooserScreen from "../screens/Settings/Template/ElementChooserScreen"; import ExportQRScreen from "../screens/Sharing/ExportQRScreen"; import ImportQRScreen from "../screens/Sharing/ImportQRScreen"; +import PrintSummaryScreen from "../screens/Sharing/PrintSummaryScreen"; import TeamScreen from "../screens/Team/TeamScreen"; const horizontalAnimation = ({ current, layouts }: any) => { @@ -31,6 +32,10 @@ const horizontalAnimation = ({ current, layouts }: any) => { }), }, ], + opacity: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 1], + }) }, }; }; @@ -59,7 +64,7 @@ export default function RootNavigator() { open: { animation: "timing", config: { - duration: 250, + duration: 200, }, }, close: { @@ -86,6 +91,7 @@ export default function RootNavigator() { + diff --git a/navigation/RootParamList.tsx b/navigation/RootParamList.tsx index e61947d..0b6c80a 100644 --- a/navigation/RootParamList.tsx +++ b/navigation/RootParamList.tsx @@ -17,6 +17,7 @@ export type RootNavParamList = { DefaultTeam: undefined, ExportQR: { data: string }, ImportQR: undefined, + PrintSummaryScreen: undefined, About: undefined, Palette: undefined, ColorPicker: { defaultColor: string, onPick: (color: string) => void } diff --git a/package.json b/package.json index 1b8d5d0..24ff012 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "expo-font": "~9.2.1", "expo-image-picker": "~10.2.2", "expo-linking": "~2.3.1", + "expo-print": "~10.2.1", "expo-sensors": "~10.2.2", "expo-sharing": "~9.2.1", "expo-splash-screen": "~0.11.2", diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index c32ba0e..21d1055 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -1,21 +1,41 @@ +import { MaterialIcons } from "@expo/vector-icons"; import { useNavigation } from "@react-navigation/native"; import React from "react"; import { ScrollView, StyleSheet, View } from "react-native"; import TBA from "../../api/TBA"; +import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import StandardButton from "../../components/common/StandardButton"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; +import { PaletteContext } from "../../context/PaletteContext"; import useMatch from "../../hooks/useMatch"; import useTemplate from "../../hooks/useTemplate"; import { TemplateType } from "../../types/TemplateTypes"; import TeamPreview from "../Matches/TeamPreview"; export default function MatchScreen({ route }: any) { + const paletteContext = React.useContext(PaletteContext); const navigator = useNavigation(); const [match, setMatch] = useMatch(route.params.matchID); const [template, setTemplate] = useTemplate(TemplateType.Match); + // Browser Button + const onBrowserButton = () => { + TBA.openMatch(match.id); + } + React.useLayoutEffect(() => { + navigator.setOptions({ + headerRight: () => ( + + + + ) + }); + }); + return ( @@ -32,12 +52,6 @@ export default function MatchScreen({ route }: any) { onPress={() => { navigator.navigate("TeamSelect", { matchID: match.id }); }} /> : undefined} - { match ? TBA.openMatch(match.id) : null }} /> - Red Alliance @@ -71,5 +85,9 @@ const styles = StyleSheet.create({ }, allianceHeader: { marginBottom: 15 + }, + headerButtons: { + alignSelf: "flex-end", + flexDirection: "row" } }); diff --git a/screens/Matches/MatchBanner.tsx b/screens/Matches/MatchBanner.tsx index 3cda68d..56658c3 100644 --- a/screens/Matches/MatchBanner.tsx +++ b/screens/Matches/MatchBanner.tsx @@ -6,7 +6,7 @@ import useMatch from "../../hooks/useMatch"; export default function MatchBanner(props: { matchID: string }) { const navigator = useNavigation(); - const [match, setMatch] = useMatch(props.matchID); + const [match] = useMatch(props.matchID); return ( diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index 66c880b..cd9dca3 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -15,6 +15,11 @@ export default function TeamPreview(props: { teamID: string }) { const [team, setTeam] = useTeam(props.teamID); const stats = useStats(props.teamID); + const decToString = (num: number) => { + return (Math.round(num * 10) / 10).toString() + } + + // Media let mediaIcon: JSX.Element; if (team.mediaPaths.length > 0) { mediaIcon = ( @@ -51,8 +56,8 @@ export default function TeamPreview(props: { teamID: string }) { {stats.map((element, index) => - {element.label} - {element.average} + {decToString(element.average)} + {element.label} )} @@ -69,8 +74,8 @@ const styles = StyleSheet.create({ flexDirection: "row" }, subContainer: { - marginRight: 20, - width: 120, + margin: 5, + width: 150, justifyContent: "center", alignItems: "center" }, @@ -92,7 +97,7 @@ const styles = StyleSheet.create({ borderRadius: 5 }, title: { - fontSize: 18, + fontSize: 22, fontWeight: "bold", textAlign: "center" }, diff --git a/screens/Scout/ScoutingScreen.tsx b/screens/Scout/ScoutingScreen.tsx index 3c48810..0ab7e99 100644 --- a/screens/Scout/ScoutingScreen.tsx +++ b/screens/Scout/ScoutingScreen.tsx @@ -2,7 +2,6 @@ import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Alert, StyleSheet, View } from 'react-native'; import Button from '../../components/common/Button'; -import HorizontalBar from '../../components/common/HorizontalBar'; import ScrollContainer from '../../components/containers/ScrollContainer'; import ScoutingElement from '../../components/elements/ScoutingElement'; import Text from '../../components/text/Text'; @@ -37,6 +36,12 @@ export default function ScoutingScreen({ route }: any) { Alert.alert("Success", "Data has been saved to storage"); } + React.useLayoutEffect(() => { + navigator.setOptions({ + title: team.number.toString() + }); + }) + return ( @@ -48,8 +53,6 @@ export default function ScoutingScreen({ route }: any) { onChange={onChange} /> )} - - + + ) + }); + }); return ( @@ -151,46 +156,8 @@ export default function TeamScreen({ route }: any) { {team.name} {team.number} - - - { }} /> - - { team ? TBA.openTeam(team.number) : null }} /> - - - - - {printStat("Rank", team.rank.toString())} - {printStat("Wins", team.wins.toString())} - {printStat("Losses", team.losses.toString())} - {printStat("Ties", team.ties.toString())} - - - - {stats.metrics.length > 0 ? - - - Min - Avg - Max - - - : - null - } + - {stats.metrics.map((stat, index) => - printStat(stat.label, stat.min.toString(), stat.average.toString(), stat.max.toString()) - )} - @@ -222,40 +189,8 @@ const styles = StyleSheet.create({ backgroundColor: "#444", borderRadius: 5 }, - - statLabel: { - fontWeight: "bold", - fontSize: 18 - }, - statType: { - color: "#bbb" - }, - statValue: { - color: "#bbb", - fontSize: 16, - textAlign: "center", - width: 50 - }, - statHeader: { - fontSize: 16, - textAlign: "center", - fontWeight: "bold", - width: 50 - }, - valueContainer: { - flexDirection: "row", - justifyContent: "flex-end", - flex: 1, - }, - statContainer: { - flexDirection: "row", - flex: 1, - padding: 10, - }, - statsContainer: { - backgroundColor: "#1b1b1b", - borderRadius: 5, - marginTop: 5, - marginBottom: 5 + headerButtons: { + alignSelf: "flex-end", + flexDirection: "row" } }); diff --git a/screens/Teams/TeamsScreen.tsx b/screens/Teams/TeamsScreen.tsx index 81c922d..e874fdc 100644 --- a/screens/Teams/TeamsScreen.tsx +++ b/screens/Teams/TeamsScreen.tsx @@ -33,18 +33,14 @@ export default function TeamsScreen() { } }; - const onFilter = () => { - - }; - return ( Teams - @@ -63,9 +59,9 @@ export default function TeamsScreen() { } const styles = StyleSheet.create({ - filterButton: { - width: 42, - height: 42, + searchButton: { + width: 46, + height: 46, marginBottom: 12, marginRight: 5, alignSelf: "flex-end" diff --git a/yarn.lock b/yarn.lock index 65df573..82df32e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4525,6 +4525,13 @@ expo-modules-core@~0.2.0: resolved "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-0.2.0.tgz" integrity sha512-inpfZ5X/BaTtbj2wG9PA9AC0MN8VyId6KSRlVuEg7+ziurHBy/kKDFxpOddUokhwiln2uhoYPSStJjR/tKypdw== +expo-print@~10.2.1: + version "10.2.1" + resolved "https://registry.yarnpkg.com/expo-print/-/expo-print-10.2.1.tgz#007b32aa8d02efa0b96e8d930d66002160161845" + integrity sha512-StLZI95srt9fbRFBWwZpvAUvCVOxHwFHP4I3q6ACxl2i81AQaNUK9soXsL7z+5gugOl9VLV3p/vCw23t6eAvBw== + dependencies: + expo-modules-core "~0.2.0" + expo-sensors@~10.2.2: version "10.2.2" resolved "https://registry.yarnpkg.com/expo-sensors/-/expo-sensors-10.2.2.tgz#882e25b3135995e383d88b21b17e5f1b1155d4e0" From 9ccd42ada6cb4ca8c94fce9a2109ec9705a57107 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Tue, 8 Feb 2022 16:02:16 -0600 Subject: [PATCH 28/38] Match Data Visualization & Bug Fixes --- components/elements/CheckboxElement.tsx | 7 ++++ hooks/useStats.ts | 37 +++++++++++++++-- screens/Match/MatchScreen.tsx | 32 ++++++--------- screens/Matches/TeamPreview.tsx | 54 +++++++++++++------------ screens/Scout/ScoutingScreen.tsx | 26 +++++++++++- screens/Settings/SettingsScreen.tsx | 3 +- screens/Sharing/PrintSummaryScreen.tsx | 6 +-- screens/Team/Stats/StatSquare.tsx | 13 +++--- screens/Team/Stats/StatTable.tsx | 15 +++---- screens/Team/TeamScreen.tsx | 4 +- 10 files changed, 129 insertions(+), 68 deletions(-) diff --git a/components/elements/CheckboxElement.tsx b/components/elements/CheckboxElement.tsx index 96a9eb3..9b48127 100644 --- a/components/elements/CheckboxElement.tsx +++ b/components/elements/CheckboxElement.tsx @@ -9,6 +9,13 @@ export default function CheckboxElement(props: ElementProps) { const defaultValue = elementData.options.defaultValue; const [isChecked, setChecked] = React.useState(defaultValue === undefined ? defaultValue as boolean : false); + // Default Value + if (elementData.value === undefined && props.onChange) { + elementData.value = false; + props.onChange(elementData); + } + + // Value State Change const changeChecked = (isChecked: boolean) => { // Value diff --git a/hooks/useStats.ts b/hooks/useStats.ts index fa99b0d..6a921e7 100644 --- a/hooks/useStats.ts +++ b/hooks/useStats.ts @@ -31,7 +31,6 @@ export default function useStats(teamID: string) { } export async function getStats(teamID: string) { - console.log(teamID); const team = await getTeam(teamID); const event = await getEvent(); const template = await getTemplate(TemplateType.Match); @@ -42,10 +41,35 @@ export async function getStats(teamID: string) { if (team.scoutingData.length === 0 || template.length === 0) { return [] as TeamStats; } - let newStats: TeamStats = []; + // Calculate Max Values + const maxs: number[] = []; + for (const teamID of event.teamIDs) { + const team = await getTeam(teamID); + if (team) { + for (const data of team.scoutingData) { + let index = 0; + for (const value of data.values) { + if (typeof value == "number") { + if (index >= maxs.length) + maxs.push(Number.MIN_SAFE_INTEGER); + maxs[index] = Math.max(value, maxs[index]); + index++; + } + else if (typeof value == "boolean") { + if (index >= maxs.length) + maxs.push(Number.MIN_SAFE_INTEGER); + maxs[index] = Math.max(value ? 1 : 0, maxs[index]); + index++; + } + } + } + } + } + + let newStats: TeamStats = []; template.forEach(element => { - if (typeof element.value === "number") { + if (typeof element.value === "number" || typeof element.value === "boolean") { const metric: TeamMetric = { label: element.label, average: 0, @@ -65,12 +89,17 @@ export async function getStats(teamID: string) { newStats[index].max = Math.max(newStats[index].max, value); newStats[index].min = Math.min(newStats[index].min, value); } + else if (typeof value === "boolean") { + newStats[index].average += value ? 1 : 0; + newStats[index].max = Math.max(newStats[index].max, value ? 1 : 0); + newStats[index].min = Math.min(newStats[index].min, value ? 1 : 0); + } }); }); newStats.forEach((stat, index) => { newStats[index].average /= team.scoutingData.length; - newStats[index].percentile = (newStats[index].average - newStats[index].min) / (newStats[index].max - newStats[index].min); + newStats[index].percentile = newStats[index].average / maxs[index]; }); return newStats; diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index 21d1055..c9948cf 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -54,27 +54,17 @@ export default function MatchScreen({ route }: any) { - Red Alliance - - - - {match.redTeamIDs.map(teamID => )} - - - - - - Blue Alliance - - - {match.blueTeamIDs.map(teamID => )} - + + + {match.redTeamIDs.map(teamID => )} + + {match.blueTeamIDs.map(teamID => )} + + - - - - + + ); } @@ -89,5 +79,9 @@ const styles = StyleSheet.create({ headerButtons: { alignSelf: "flex-end", flexDirection: "row" + }, + previewContainer: { + flex: 1, + flexDirection: "row", } }); diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index cd9dca3..54a4cc3 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -7,6 +7,7 @@ import Text from '../../components/text/Text'; import { PaletteContext } from '../../context/PaletteContext'; import useStats from '../../hooks/useStats'; import useTeam from '../../hooks/useTeam'; +import StatTable from '../Team/Stats/StatTable'; export default function TeamPreview(props: { teamID: string }) { @@ -23,10 +24,12 @@ export default function TeamPreview(props: { teamID: string }) { let mediaIcon: JSX.Element; if (team.mediaPaths.length > 0) { mediaIcon = ( - + + + ); } else { @@ -34,7 +37,7 @@ export default function TeamPreview(props: { teamID: string }) { ); @@ -50,16 +53,12 @@ export default function TeamPreview(props: { teamID: string }) { {team.name} {team.number} - - {stats.map((element, index) => - - {decToString(element.average)} - {element.label} - - )} + + + ); } @@ -67,15 +66,16 @@ export default function TeamPreview(props: { teamID: string }) { const styles = StyleSheet.create({ container: { marginBottom: 10, - flexDirection: "row", - height: 200 + width: 100, + flexDirection: "column" }, button: { - flexDirection: "row" + flexDirection: "column", + padding: 2 }, subContainer: { - margin: 5, - width: 150, + height: 100, + overflow: "hidden", justifyContent: "center", alignItems: "center" }, @@ -88,21 +88,25 @@ const styles = StyleSheet.create({ backgroundColor: "black" }, thumbnail: { - height: 200, - width: 200, - justifyContent: "center", - alignItems: "center", + width: "100%", + aspectRatio: 1, backgroundColor: "#444", - marginRight: 15, - borderRadius: 5 + borderRadius: 5, + padding: 0, + margin: 0, + justifyContent: "center", + alignItems: "center" }, title: { - fontSize: 22, + fontSize: 16, fontWeight: "bold", - textAlign: "center" + textAlign: "center", }, subtitle: { fontWeight: "bold", textAlign: "center" }, + statTable: { + flex: 1 + } }); diff --git a/screens/Scout/ScoutingScreen.tsx b/screens/Scout/ScoutingScreen.tsx index 0ab7e99..98332ea 100644 --- a/screens/Scout/ScoutingScreen.tsx +++ b/screens/Scout/ScoutingScreen.tsx @@ -33,7 +33,29 @@ export default function ScoutingScreen({ route }: any) { setTeam(team); navigator.goBack(); navigator.goBack(); - Alert.alert("Success", "Data has been saved to storage"); + Alert.alert("Success", "Data has been saved to storage", [ + { + text: "Undo", + onPress: () => { + Alert.alert("Are you sure?", "This will delete last round's scouting data", [ + { + text: "Confirm", + onPress: () => { + Alert.alert("Success!", "Last round's scouting data has been cleared"); + } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true }); + } + }, + { + text: "OK", + style: "cancel" + } + ], { cancelable: true }); } React.useLayoutEffect(() => { @@ -75,7 +97,7 @@ const styles = StyleSheet.create({ borderRadius: 5, padding: 10, margin: 10, - marginTop: 0, + marginTop: 20, backgroundColor: "#c89f00" }, diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 3042364..201ca33 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -79,6 +79,8 @@ export default function SettingsScreen() { if (!team) continue; + if (team.scoutingData.length >= 2) + continue; const values = template.map(() => Math.round(Math.random() * 10)); team.scoutingData.push({ @@ -86,7 +88,6 @@ export default function SettingsScreen() { values }); - await setTeam(team); } } diff --git a/screens/Sharing/PrintSummaryScreen.tsx b/screens/Sharing/PrintSummaryScreen.tsx index f977bf4..de1c21d 100644 --- a/screens/Sharing/PrintSummaryScreen.tsx +++ b/screens/Sharing/PrintSummaryScreen.tsx @@ -55,7 +55,7 @@ export default function PrintSummaryScreen() { printData += `


`; printData += `` - printData += ``; + printData += ``; @@ -69,10 +69,10 @@ export default function PrintSummaryScreen() { continue; printData += ``; - printData += `` + printData += `` for (const stat of stats) { - printData += `` + printData += `` } printData += `
`; diff --git a/screens/Team/Stats/StatSquare.tsx b/screens/Team/Stats/StatSquare.tsx index 10c7752..2ae4afb 100644 --- a/screens/Team/Stats/StatSquare.tsx +++ b/screens/Team/Stats/StatSquare.tsx @@ -1,11 +1,10 @@ -import React, { useRef } from "react"; +import React from "react"; import { StyleSheet, View } from "react-native"; import Text from "../../../components/text/Text"; import { PaletteContext } from "../../../context/PaletteContext"; export default function StatSquare(props: { name: string, value: string, percentile: number }) { const paletteContext = React.useContext(PaletteContext); - const square = useRef(null); const percentToRGB = (percent: number) => { var hue = percent * 120; @@ -13,7 +12,7 @@ export default function StatSquare(props: { name: string, value: string, percent } return ( - + {props.value} {props.name} { @@ -63,17 +63,18 @@ export default function StatTable(props: { teamID: string, cols: number }) { const styles = StyleSheet.create({ tableContainer: { + flex: 1, flexDirection: "row", - flexWrap: "wrap", - marginTop: 15 + flexWrap: "wrap" }, break: { flexBasis: "100%", - height: 0 + height: 0, + margin: 0 }, blank: { flex: 1, margin: 2, - padding: 5 + padding: 5, } }); diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 616fec6..722b02c 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -156,7 +156,9 @@ export default function TeamScreen({ route }: any) { {team.name} {team.number} - + + + From cce622f7b9c79e89d6275ce74c55773ad7d99096 Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Wed, 9 Feb 2022 01:18:56 -0600 Subject: [PATCH 29/38] ScoutingDB Changes & Bug Fixes --- components/common/HorizontalBar.tsx | 6 +- components/containers/ScrollContainer.tsx | 31 ++--- components/elements/ScoutingElement.tsx | 4 - components/elements/TextBoxElement.tsx | 50 --------- components/text/NavTitle.tsx | 2 +- hooks/useCompressedData.ts | 62 +++------- hooks/useEvent.ts | 4 +- hooks/useMatch.ts | 7 +- hooks/useScoutingData.ts | 27 +++++ hooks/useStats.ts | 106 ------------------ hooks/useTeam.ts | 7 +- navigation/RootParamList.tsx | 2 +- screens/DefaultTeam/TeamSelectScreen.tsx | 34 ++++-- screens/Match/MatchScreen.tsx | 43 +++++-- screens/Matches/TeamPreview.tsx | 15 +-- screens/Scout/ScoutingScreen.tsx | 49 ++++---- screens/Settings/SettingsScreen.tsx | 27 ++--- .../Template/ElementChooserScreen.tsx | 6 - screens/Sharing/ExportQRScreen.tsx | 87 ++++++++++++-- screens/Sharing/PrintSummaryScreen.tsx | 47 ++++++-- screens/Sharing/SharingScreen.tsx | 9 +- screens/Team/Stats/StatSquare.tsx | 8 +- screens/Team/Stats/StatTable.tsx | 82 ++++++++++---- screens/Team/TeamScreen.tsx | 17 +-- types/DBTypes.ts | 2 - types/TemplateTypes.ts | 8 +- 26 files changed, 366 insertions(+), 376 deletions(-) delete mode 100644 components/elements/TextBoxElement.tsx create mode 100644 hooks/useScoutingData.ts delete mode 100644 hooks/useStats.ts diff --git a/components/common/HorizontalBar.tsx b/components/common/HorizontalBar.tsx index 9970d6e..7096f94 100644 --- a/components/common/HorizontalBar.tsx +++ b/components/common/HorizontalBar.tsx @@ -10,10 +10,10 @@ export default function HorizontalBar(props: ViewProps) { return ( - : undefined - }> + + : undefined + }> - - {props.children} - + + {props.children} + - + + ); } \ No newline at end of file diff --git a/components/elements/ScoutingElement.tsx b/components/elements/ScoutingElement.tsx index 062f733..2994989 100644 --- a/components/elements/ScoutingElement.tsx +++ b/components/elements/ScoutingElement.tsx @@ -7,7 +7,6 @@ import CheckboxElement from "./CheckboxElement"; import CounterElement from "./CounterElement"; import HRElement from "./HRElement"; import SubtitleElement from "./SubtitleElement"; -import TextBoxElement from "./TextBoxElement"; import TextElement from "./TextElement"; import TitleElement from "./TitleElement"; @@ -32,9 +31,6 @@ export default function ScoutingElement(props: ElementProps) { case ElementType.checkbox: element = (); break; - case ElementType.textbox: - element = (); - break; } diff --git a/components/elements/TextBoxElement.tsx b/components/elements/TextBoxElement.tsx deleted file mode 100644 index 408421e..0000000 --- a/components/elements/TextBoxElement.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from "react"; -import { StyleSheet, TextInput } from "react-native"; -import { ElementProps } from "../../types/TemplateTypes"; - -export default function TextBoxElement(props: ElementProps) { - let elementData = props.data; - - // Default Value - if (elementData.value === undefined && props.onChange) { - elementData.value = ""; - props.onChange(elementData); - } - - // On Edit - const onEdit = (text: string) => { - elementData.label = text; - if (props.onChange) - props.onChange(elementData); - }; - - // On Scout - const onScoutingEdit = (text: string) => { - elementData.value = text; - if (props.onChange) - props.onChange(elementData); - } - - return ( - - ); -} - -const styles = StyleSheet.create({ - textInput: { - color: "#fff", - backgroundColor: "#222222", - borderRadius: 10, - padding: 10, - margin: 10, - fontSize: 15, - height: 100, - textAlignVertical: "top" - } -}); diff --git a/components/text/NavTitle.tsx b/components/text/NavTitle.tsx index 7b98171..365a203 100644 --- a/components/text/NavTitle.tsx +++ b/components/text/NavTitle.tsx @@ -12,7 +12,7 @@ export default function NavTitle(props: TextProps) { color: paletteContext.palette.textPrimary, fontSize: 30, fontWeight: 'bold', - marginTop: 60, + marginTop: 20, marginBottom: 15, }, style]} {...otherProps} />; } \ No newline at end of file diff --git a/hooks/useCompressedData.ts b/hooks/useCompressedData.ts index cd8d7aa..b0826d8 100644 --- a/hooks/useCompressedData.ts +++ b/hooks/useCompressedData.ts @@ -1,85 +1,53 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { ToastAndroid } from "react-native"; import { ScoutingData } from "../types/TemplateTypes"; import useEvent from "./useEvent"; -import { getTeam, setTeam } from "./useTeam"; +import useScoutingData from "./useScoutingData"; export interface ExportData { eventID: string, exportID: string, - scoutingData: Record + scoutingData: ScoutingData[] } -export function getChecksum(data: Record) { +export function getChecksum(data: ScoutingData[]) { const jsonData = JSON.stringify(data); return jsonData.split("").reduce((a, b) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; - }, 0); -} - -export function useJsonData() { - const [event] = useEvent(); - const [data, setData] = useState(""); - - const compressJsonData = async () => { - const teams = await Promise.all(event.teamIDs.map(getTeam)); - let scoutingData: Record = {}; - for (const team of teams) { - if (team) - if (team.scoutingData.length > 0) - scoutingData[team.id] = team.scoutingData; - } - const outputData: ExportData = { - exportID: getChecksum(scoutingData).toString(), - eventID: event.id, - scoutingData - }; - const jsonData = JSON.stringify(outputData); - setData(jsonData); - } - - useEffect(() => { - compressJsonData(); - }, [event]); - - return event.id === "bogus" ? "" : data; + }, 0).toString(); } export function useDataImporter() { const [event] = useEvent(); + const [scoutingData, setScoutingData] = useScoutingData(); const [importedIDs, setImportedIDs] = React.useState([] as string[]); const importJsonData = async (data: string) => { try { + // Decompress const decompressedData = JSON.parse(data) as ExportData; if (!decompressedData) { ToastAndroid.show("Invalid QR code", ToastAndroid.SHORT); return; } + + // Check Duplicates if (importedIDs.includes(decompressedData.exportID)) { return; } importedIDs.push(decompressedData.exportID); setImportedIDs(importedIDs); + + // Check Event if (decompressedData.eventID !== event.id) { ToastAndroid.show("Invalid Event", ToastAndroid.SHORT); return; } - const teamIDs = Object.keys(decompressedData.scoutingData); - for (const teamID of teamIDs) { - const team = await getTeam(teamID); - if (team) { - const scoutingData = decompressedData.scoutingData[teamID]; - for (let scout of scoutingData) { - if (team.scoutingData.findIndex(s => s.matchID === scout.matchID) === -1) { - team.scoutingData.push(scout); - } - } - await setTeam(team); - } - } - ToastAndroid.show("Imported " + teamIDs.length + " team(s).", ToastAndroid.SHORT); + // Append Data + scoutingData.push(...decompressedData.scoutingData); + setScoutingData(scoutingData); + ToastAndroid.show("Imported " + scoutingData.length + " matches.", ToastAndroid.SHORT); } catch (e) { ToastAndroid.show("Invalid Data Import", ToastAndroid.SHORT); diff --git a/hooks/useEvent.ts b/hooks/useEvent.ts index 65392f8..fee8941 100644 --- a/hooks/useEvent.ts +++ b/hooks/useEvent.ts @@ -1,12 +1,12 @@ import { Event } from "../types/DBTypes"; import useStorage, { getStorage } from "./useStorage"; -const DEFAULT_EVENT = { +const DEFAULT_EVENT: Event = { id: "bogus", matchIDs: [], teamIDs: [], year: 0 -} as Event; +}; /** * Grabs the current event data as a react hook diff --git a/hooks/useMatch.ts b/hooks/useMatch.ts index 97a1a58..07133ca 100644 --- a/hooks/useMatch.ts +++ b/hooks/useMatch.ts @@ -1,16 +1,15 @@ import { Match } from "../types/DBTypes"; import useStorage, { getStorage, putStorage } from "./useStorage"; -const DEFAULT_MATCH = { +const DEFAULT_MATCH: Match = { id: "", name: "", description: "", number: 0, compLevel: "", blueTeamIDs: [], - redTeamIDs: [], - scoutingData: [] -} as Match; + redTeamIDs: [] +}; /** * Grabs match data as a react hook diff --git a/hooks/useScoutingData.ts b/hooks/useScoutingData.ts new file mode 100644 index 0000000..ab5c0b4 --- /dev/null +++ b/hooks/useScoutingData.ts @@ -0,0 +1,27 @@ +import { ScoutingData } from "../types/TemplateTypes"; +import useStorage, { getStorage, putStorage } from "./useStorage"; + +/** + * Grabs all of the scouting data as a react hook + * @returns all scouting data + */ +export default function useScoutingData(): [ScoutingData[], (scoutingData: ScoutingData[]) => Promise] { + const [scoutingData, setScoutingData] = useStorage("scouting-data", []); + return [scoutingData, setScoutingData]; +} + +/** + * Grabs all of the scouting data from storage + * @returns all scouting data + */ +export async function getScoutingData() { + return getStorage("scouting-data"); +} + +/** + * Grabs all of the scouting data from storage + * @returns all scouting data + */ +export async function setScoutingData(scoutingData: ScoutingData[]) { + return putStorage("scouting-data", scoutingData); +} \ No newline at end of file diff --git a/hooks/useStats.ts b/hooks/useStats.ts deleted file mode 100644 index 6a921e7..0000000 --- a/hooks/useStats.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { useEffect, useState } from "react"; -import { TemplateType } from "../types/TemplateTypes"; -import { getEvent } from "./useEvent"; -import useTeam, { getTeam } from "./useTeam"; -import useTemplate, { getTemplate } from "./useTemplate"; - -export type TeamStats = TeamMetric[]; -export interface TeamMetric { - label: string, - average: number, - max: number, - min: number, - percentile: number -} - -export default function useStats(teamID: string) { - const [team] = useTeam(teamID); - const [template] = useTemplate(TemplateType.Match); - const [teamStats, setTeamStats] = useState([] as TeamStats); - - const getNewStats = async () => { - const newStats = await getStats(teamID); - setTeamStats(newStats); - } - - useEffect(() => { - getNewStats(); - }, [team, template, setTeamStats]); - - return teamStats; -} - -export async function getStats(teamID: string) { - const team = await getTeam(teamID); - const event = await getEvent(); - const template = await getTemplate(TemplateType.Match); - - if (team === undefined || event === undefined || template === undefined) { - return [] as TeamStats; - } - if (team.scoutingData.length === 0 || template.length === 0) { - return [] as TeamStats; - } - - // Calculate Max Values - const maxs: number[] = []; - for (const teamID of event.teamIDs) { - const team = await getTeam(teamID); - if (team) { - for (const data of team.scoutingData) { - let index = 0; - for (const value of data.values) { - if (typeof value == "number") { - if (index >= maxs.length) - maxs.push(Number.MIN_SAFE_INTEGER); - maxs[index] = Math.max(value, maxs[index]); - index++; - } - else if (typeof value == "boolean") { - if (index >= maxs.length) - maxs.push(Number.MIN_SAFE_INTEGER); - maxs[index] = Math.max(value ? 1 : 0, maxs[index]); - index++; - } - } - } - } - } - - let newStats: TeamStats = []; - template.forEach(element => { - if (typeof element.value === "number" || typeof element.value === "boolean") { - const metric: TeamMetric = { - label: element.label, - average: 0, - max: Number.MIN_SAFE_INTEGER, - min: Number.MAX_SAFE_INTEGER, - percentile: 0 - }; - - newStats.push(metric); - } - }); - - team.scoutingData.forEach(scout => { - scout.values.forEach((value, index) => { - if (typeof value === "number") { - newStats[index].average += value; - newStats[index].max = Math.max(newStats[index].max, value); - newStats[index].min = Math.min(newStats[index].min, value); - } - else if (typeof value === "boolean") { - newStats[index].average += value ? 1 : 0; - newStats[index].max = Math.max(newStats[index].max, value ? 1 : 0); - newStats[index].min = Math.min(newStats[index].min, value ? 1 : 0); - } - }); - }); - - newStats.forEach((stat, index) => { - newStats[index].average /= team.scoutingData.length; - newStats[index].percentile = newStats[index].average / maxs[index]; - }); - - return newStats; -} \ No newline at end of file diff --git a/hooks/useTeam.ts b/hooks/useTeam.ts index 2d2dc7c..ab3f431 100644 --- a/hooks/useTeam.ts +++ b/hooks/useTeam.ts @@ -1,7 +1,7 @@ import { Team } from "../types/DBTypes"; import useStorage, { getStorage, putStorage } from "./useStorage"; -const DEFAULT_TEAM = { +const DEFAULT_TEAM: Team = { id: "", name: "", number: 0, @@ -9,9 +9,8 @@ const DEFAULT_TEAM = { wins: -1, losses: -1, ties: -1, - mediaPaths: [], - scoutingData: [] -} as Team; + mediaPaths: [] +}; /** * Grabs team data as a react hook diff --git a/navigation/RootParamList.tsx b/navigation/RootParamList.tsx index 0b6c80a..8e317c8 100644 --- a/navigation/RootParamList.tsx +++ b/navigation/RootParamList.tsx @@ -15,7 +15,7 @@ export type RootNavParamList = { TeamSelect: { matchID: string }, Scout: { teamID: string, matchID: string, templateType: TemplateType }, DefaultTeam: undefined, - ExportQR: { data: string }, + ExportQR: undefined, ImportQR: undefined, PrintSummaryScreen: undefined, About: undefined, diff --git a/screens/DefaultTeam/TeamSelectScreen.tsx b/screens/DefaultTeam/TeamSelectScreen.tsx index c5008d2..14d5839 100644 --- a/screens/DefaultTeam/TeamSelectScreen.tsx +++ b/screens/DefaultTeam/TeamSelectScreen.tsx @@ -1,8 +1,8 @@ import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { ScrollView, StyleSheet, View } from "react-native"; -import HorizontalBar from "../../components/common/HorizontalBar"; import Subtitle from "../../components/text/Subtitle"; +import Text from '../../components/text/Text'; import Title from "../../components/text/Title"; import useMatch from '../../hooks/useMatch'; import { TemplateType } from '../../types/TemplateTypes'; @@ -20,17 +20,15 @@ export default function TeamSelectScreen({ route }: any) { {match.name} - Select the team to scout - - + Select the team to scout + Red Alliance {match.redTeamIDs.map((teamID) => )} + - - + Blue Alliance {match.blueTeamIDs.map((teamID) => )} - - + ); @@ -41,5 +39,23 @@ const styles = StyleSheet.create({ paddingLeft: 20, paddingRight: 20, paddingTop: 20 - } + }, + allianceHeader: { + paddingBottom: 5, + paddingTop: 5, + marginRight: 8, + marginBottom: 5, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + fontSize: 18, + fontWeight: "bold", + textAlign: "center" + }, + allianceFooter: { + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, + height: 10, + marginRight: 8, + marginBottom: 20 + }, }); diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index c9948cf..91dbb33 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -7,6 +7,7 @@ import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; import StandardButton from "../../components/common/StandardButton"; import Subtitle from "../../components/text/Subtitle"; +import Text from "../../components/text/Text"; import Title from "../../components/text/Title"; import { PaletteContext } from "../../context/PaletteContext"; import useMatch from "../../hooks/useMatch"; @@ -42,25 +43,33 @@ export default function MatchScreen({ route }: any) { {match.name} {match.description} - - {template.length > 0 ? - { navigator.navigate("TeamSelect", { matchID: match.id }); }} /> + + { navigator.navigate("TeamSelect", { matchID: match.id }); }} /> + : undefined} + + Red Alliance + Blue Alliance + {match.redTeamIDs.map(teamID => )} {match.blueTeamIDs.map(teamID => )} + + + + @@ -74,7 +83,23 @@ const styles = StyleSheet.create({ paddingRight: 20 }, allianceHeader: { - marginBottom: 15 + paddingBottom: 5, + paddingTop: 5, + marginRight: 8, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + fontSize: 18, + fontWeight: "bold", + width: 300, + textAlign: "center" + }, + allianceFooter: { + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, + width: 300, + height: 10, + marginRight: 8, + marginBottom: 20 }, headerButtons: { alignSelf: "flex-end", @@ -82,6 +107,6 @@ const styles = StyleSheet.create({ }, previewContainer: { flex: 1, - flexDirection: "row", + flexDirection: "row" } }); diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index 54a4cc3..e2cc1df 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -5,7 +5,6 @@ import { Image, StyleSheet, View } from "react-native"; import Button from '../../components/common/Button'; import Text from '../../components/text/Text'; import { PaletteContext } from '../../context/PaletteContext'; -import useStats from '../../hooks/useStats'; import useTeam from '../../hooks/useTeam'; import StatTable from '../Team/Stats/StatTable'; @@ -14,7 +13,6 @@ export default function TeamPreview(props: { teamID: string }) { const paletteContext = React.useContext(PaletteContext); const navigator = useNavigation(); const [team, setTeam] = useTeam(props.teamID); - const stats = useStats(props.teamID); const decToString = (num: number) => { return (Math.round(num * 10) / 10).toString() @@ -56,22 +54,19 @@ export default function TeamPreview(props: { teamID: string }) { - - - + ); } const styles = StyleSheet.create({ container: { - marginBottom: 10, width: 100, - flexDirection: "column" + flexDirection: "column", }, button: { flexDirection: "column", - padding: 2 + padding: 0 }, subContainer: { height: 100, @@ -91,7 +86,6 @@ const styles = StyleSheet.create({ width: "100%", aspectRatio: 1, backgroundColor: "#444", - borderRadius: 5, padding: 0, margin: 0, justifyContent: "center", @@ -105,8 +99,5 @@ const styles = StyleSheet.create({ subtitle: { fontWeight: "bold", textAlign: "center" - }, - statTable: { - flex: 1 } }); diff --git a/screens/Scout/ScoutingScreen.tsx b/screens/Scout/ScoutingScreen.tsx index 98332ea..0f2c375 100644 --- a/screens/Scout/ScoutingScreen.tsx +++ b/screens/Scout/ScoutingScreen.tsx @@ -1,18 +1,23 @@ import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Alert, StyleSheet, View } from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; import Button from '../../components/common/Button'; -import ScrollContainer from '../../components/containers/ScrollContainer'; +import HorizontalBar from '../../components/common/HorizontalBar'; import ScoutingElement from '../../components/elements/ScoutingElement'; +import Subtitle from '../../components/text/Subtitle'; import Text from '../../components/text/Text'; +import Title from '../../components/text/Title'; +import useScoutingData, { getScoutingData, setScoutingData } from '../../hooks/useScoutingData'; import useTeam from '../../hooks/useTeam'; import useTemplate from '../../hooks/useTemplate'; import { ElementData, ScoutingData } from '../../types/TemplateTypes'; export default function ScoutingScreen({ route }: any) { const navigator = useNavigation(); - const [template, setTemplate] = useTemplate(route.params.templateType); - const [team, setTeam] = useTeam(route.params.teamID); + const [scoutingData, setScoutingDataHook] = useScoutingData(); + const [template] = useTemplate(route.params.templateType); + const [team] = useTeam(route.params.teamID); const onChange = (element: ElementData) => { const index = template.findIndex(e => e.id === element.id); @@ -22,6 +27,7 @@ export default function ScoutingScreen({ route }: any) { const onSubmit = () => { let data: ScoutingData = { + teamID: route.params.teamID, matchID: route.params.matchID, values: [] }; @@ -29,8 +35,9 @@ export default function ScoutingScreen({ route }: any) { if (element.value !== undefined) data.values.push(element.value); } - team.scoutingData.push(data); - setTeam(team); + scoutingData.push(data); + setScoutingDataHook(scoutingData); + navigator.goBack(); navigator.goBack(); Alert.alert("Success", "Data has been saved to storage", [ @@ -40,7 +47,12 @@ export default function ScoutingScreen({ route }: any) { Alert.alert("Are you sure?", "This will delete last round's scouting data", [ { text: "Confirm", - onPress: () => { + onPress: async () => { + const data = await getScoutingData(); + if (data) { + data.splice(scoutingData.length - 1, 1); + setScoutingData(scoutingData); + } Alert.alert("Success!", "Last round's scouting data has been cleared"); } }, @@ -58,15 +70,12 @@ export default function ScoutingScreen({ route }: any) { ], { cancelable: true }); } - React.useLayoutEffect(() => { - navigator.setOptions({ - title: team.number.toString() - }); - }) - return ( - - + + + {team.name} + {team.number} + {template.map((element, index) => Submit - - + + ); } const styles = StyleSheet.create({ - parentView: { - height: "100%", - width: "100%" + container: { + paddingLeft: 20, + paddingRight: 20 }, submitButton: { height: 40, borderRadius: 5, padding: 10, margin: 10, - marginTop: 20, + marginTop: 30, backgroundColor: "#c89f00" }, diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 201ca33..9bfa04f 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -8,10 +8,11 @@ import NavTitle from '../../components/text/NavTitle'; import { DARK_PALETTE, LIGHT_PALETTE, PaletteContext } from '../../context/PaletteContext'; import useEvent from '../../hooks/useEvent'; import { getMatch } from '../../hooks/useMatch'; +import { setScoutingData } from '../../hooks/useScoutingData'; import { clearStorage } from '../../hooks/useStorage'; -import { getTeam, setTeam } from '../../hooks/useTeam'; +import { getTeam } from '../../hooks/useTeam'; import useTemplate from '../../hooks/useTemplate'; -import { TemplateType } from '../../types/TemplateTypes'; +import { ScoutingData, TemplateType } from '../../types/TemplateTypes'; export default function SettingsScreen() { const paletteContext = React.useContext(PaletteContext); @@ -44,13 +45,7 @@ export default function SettingsScreen() { { text: "Confirm", onPress: async () => { - for (const teamID of event.teamIDs) { - const team = await getTeam(teamID); - if (team) { - team.scoutingData = []; - setTeam(team); - } - } + await setScoutingData([]); Alert.alert("Success!", "All scouting data has been cleared"); } }, @@ -63,6 +58,7 @@ export default function SettingsScreen() { } const generateRandomData = async () => { + const scoutingData: ScoutingData[] = []; for (let matchID of event.matchIDs) { const match = await getMatch(matchID); @@ -76,21 +72,20 @@ export default function SettingsScreen() { for (let teamID of teamIDs) { const team = await getTeam(teamID); - if (!team) continue; - if (team.scoutingData.length >= 2) - continue; - const values = template.map(() => Math.round(Math.random() * 10)); - team.scoutingData.push({ + const values = template.filter(elem => elem.value != undefined).map((elem) => typeof elem.value === "number" ? Math.round(Math.random() * (50 - team.rank)) : Math.random() < .5); + + //if (scoutingData.filter(scout => scout.teamID === teamID).length <= 2) + scoutingData.push({ matchID, + teamID, values }); - - await setTeam(team); } } + await setScoutingData(scoutingData); Alert.alert("Success", "Successfully filled with random data!"); } diff --git a/screens/Settings/Template/ElementChooserScreen.tsx b/screens/Settings/Template/ElementChooserScreen.tsx index 0454560..634ec2a 100644 --- a/screens/Settings/Template/ElementChooserScreen.tsx +++ b/screens/Settings/Template/ElementChooserScreen.tsx @@ -43,12 +43,6 @@ export default function ElementChooserScreen({ route }: any) { subtitle={"A simple check or uncheck"} onPress={() => { chooseElement(ElementType.checkbox); }} /> - { chooseElement(ElementType.textbox); }} /> - { + const splitScoutingData = []; + for (let i = 0; i < scoutingData.length; i += CHUNK_SIZE) + splitScoutingData.push(scoutingData.slice(i, i + CHUNK_SIZE)); + + setOutputData(splitScoutingData.map(data => { + const exportData = JSON.stringify({ + eventID: event.id, + exportID: getChecksum(data), + scoutingData: data + }); + + return LZString.compressToEncodedURIComponent(exportData); + })); + }, [scoutingData, event]); + + const cycleForward = () => { + setQRIndex((qrIndex + 1) % outputData.length); + } + const cycleBackward = () => { + setQRIndex((qrIndex - 1 + outputData.length) % outputData.length); + } + React.useLayoutEffect(() => { + navigator.setOptions({ + headerRight: () => ( + + + + + ) + }) + }) const windowSize = Dimensions.get("window"); const qrSize = Math.min(windowSize.width, windowSize.height); return ( - + {outputData.length > 0 ? + + + QRCode {qrIndex + 1} / {outputData.length} + Lines {qrIndex * CHUNK_SIZE} - {(qrIndex + 1) * CHUNK_SIZE} + + : + Loading... + } ); } @@ -27,5 +87,16 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: "black", justifyContent: "center", + }, + text: { + fontSize: 18, + fontWeight: "bold", + width: "100%", + textAlign: "center", + marginTop: 5 + }, + headerButtons: { + alignSelf: "flex-end", + flexDirection: "row" } }); diff --git a/screens/Sharing/PrintSummaryScreen.tsx b/screens/Sharing/PrintSummaryScreen.tsx index de1c21d..8a9eeda 100644 --- a/screens/Sharing/PrintSummaryScreen.tsx +++ b/screens/Sharing/PrintSummaryScreen.tsx @@ -5,7 +5,7 @@ import ScrollContainer from '../../components/containers/ScrollContainer'; import Subtitle from '../../components/text/Subtitle'; import Title from '../../components/text/Title'; import useEvent from '../../hooks/useEvent'; -import { getStats } from '../../hooks/useStats'; +import useScoutingData from '../../hooks/useScoutingData'; import { getTeam } from '../../hooks/useTeam'; import { getTemplate } from '../../hooks/useTemplate'; import { TemplateType } from '../../types/TemplateTypes'; @@ -40,11 +40,14 @@ const PRINT_FOOTER = ``; export default function PrintSummaryScreen() { const navigator = useNavigation(); + const [scoutingData] = useScoutingData(); const [event] = useEvent(); const printSummary = async () => { if (event.id === "bogus") return; + if (scoutingData.length <= 0) + return; const template = await getTemplate(TemplateType.Match); if (template === undefined) return; @@ -57,23 +60,51 @@ export default function PrintSummaryScreen() { printData += `
Team
`; for (const element of template) if (typeof element.value === "number") printData += `` + element.label + `

` + team.number + `

` + team.name + `

` + team.number + `

` + team.name + `

` + (Math.round(stat.average * 100) / 100) + `

` + (Math.round(stat.average * 100) / 100) + `

` printData += ``; printData += ``; for (const teamID of event.teamIDs) { const team = await getTeam(teamID); - const stats = await getStats(teamID); - - if (team === undefined || stats === undefined) + if (team === undefined) continue; + const teamScoutingData = scoutingData.filter((data) => data.teamID === teamID); + + + // Calculate Stats + const avgs: number[] = []; + const maxs: number[] = []; + teamScoutingData.forEach(scout => { + scout.values.forEach((val, index) => { + if (index >= avgs.length) + avgs.push(0); + if (typeof val === "number") + avgs[index] += val; + else + avgs[index] += val ? 1 : 0; + }); + }); + avgs.forEach((val, index) => { + avgs[index] = val / teamScoutingData.length; + }); + scoutingData.forEach(scout => { + scout.values.forEach((val, index) => { + if (index >= maxs.length) + maxs.push(0); + if (typeof val === "number") + maxs[index] = Math.max(maxs[index], val); + else + maxs[index] = 1; + }); + }); + printData += ``; printData += `` - for (const stat of stats) { - printData += `` - } + avgs.forEach((avg, index) => { + printData += `` + }) printData += `
`; } diff --git a/screens/Sharing/SharingScreen.tsx b/screens/Sharing/SharingScreen.tsx index c389b77..40dee4d 100644 --- a/screens/Sharing/SharingScreen.tsx +++ b/screens/Sharing/SharingScreen.tsx @@ -6,16 +6,17 @@ import * as React from 'react'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; -import { useDataImporter, useJsonData } from '../../hooks/useCompressedData'; +import { useDataImporter } from '../../hooks/useCompressedData'; +import useScoutingData from '../../hooks/useScoutingData'; export default function SharingScreen() { const navigator = useNavigation(); - const data = useJsonData(); + const [scoutingData] = useScoutingData(); const importJsonData = useDataImporter(); const exportJson = async () => { const path = FileSystem.documentDirectory + "data.json"; - await FileSystem.writeAsStringAsync(path, data, { encoding: FileSystem.EncodingType.UTF8 }); + await FileSystem.writeAsStringAsync(path, JSON.stringify(scoutingData), { encoding: FileSystem.EncodingType.UTF8 }); Sharing.shareAsync(path); } @@ -45,7 +46,7 @@ export default function SharingScreen() { iconType={"qr-code"} title={"Show QRCode"} subtitle={"Export Scouting Data"} - onPress={() => { navigator.navigate("ExportQR", { data }); }} /> + onPress={() => { navigator.navigate("ExportQR"); }} /> data.teamID === team.id); + + // Calculate Stats + const avgs: number[] = []; + const maxs: number[] = []; + teamScoutingData.forEach(scout => { + scout.values.forEach((val, index) => { + if (index >= avgs.length) + avgs.push(0); + if (typeof val === "number") + avgs[index] += val; + else + avgs[index] += val ? 1 : 0; + }); + }); + avgs.forEach((val, index) => { + avgs[index] = val / teamScoutingData.length; + }); + scoutingData.forEach(scout => { + scout.values.forEach((val, index) => { + if (index >= maxs.length) + maxs.push(0); + if (typeof val === "number") + maxs[index] = Math.max(maxs[index], val); + else + maxs[index] = 1; + }); + }); + // Display Stats const decToString = (num: number) => { return (Math.round(num * 10) / 10).toString() } - - // Stat List const statList: { label: string, value: string, percentile: number }[] = []; statList.push({ label: "Rank", @@ -26,31 +57,38 @@ export default function StatTable(props: { teamID: string, cols: number }) { value: team.wins + "-" + team.ties + "-" + team.losses, percentile: team.wins / (team.wins + team.ties + team.losses) }); - statList.push( - ...stats.map((stat) => { - return { - label: stat.label, - value: decToString(stat.average), - percentile: stat.percentile - } - }) - ); + + if (teamScoutingData.length > 0) { + const labels = template.filter(elem => elem.value !== undefined).map(elem => elem.label); + labels.forEach((label, index) => { + statList.push({ + label, + value: decToString(avgs[index]), + percentile: avgs[index] / maxs[index] + }) + }); + } // Stat Squares let statIndex = 0; const statSquares = [] as React.ReactNodeArray; while (statIndex < statList.length) { + const row = []; for (let col = 0; col < props.cols; col++) { const stat = statList[statIndex]; statIndex++; if (stat) - statSquares.push(); + row.push(); else - statSquares.push(); + row.push(); } - statSquares.push(); + statSquares.push( + + {row} + + ); } return ( @@ -63,14 +101,10 @@ export default function StatTable(props: { teamID: string, cols: number }) { const styles = StyleSheet.create({ tableContainer: { - flex: 1, - flexDirection: "row", - flexWrap: "wrap" + flex: 1 }, - break: { - flexBasis: "100%", - height: 0, - margin: 0 + tableRow: { + flexDirection: "row", }, blank: { flex: 1, diff --git a/screens/Team/TeamScreen.tsx b/screens/Team/TeamScreen.tsx index 722b02c..4738e97 100644 --- a/screens/Team/TeamScreen.tsx +++ b/screens/Team/TeamScreen.tsx @@ -10,7 +10,6 @@ import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; import { PaletteContext } from "../../context/PaletteContext"; import useEvent from "../../hooks/useEvent"; -import useStats from "../../hooks/useStats"; import useTeam from "../../hooks/useTeam"; import StatTable from "./Stats/StatTable"; @@ -18,7 +17,6 @@ export default function TeamScreen({ route }: any) { const paletteContext = React.useContext(PaletteContext); const navigator = useNavigation(); const [team, setTeam] = useTeam(route.params.teamID); - const stats = useStats(team.id); const [event] = useEvent(); // Photos @@ -52,8 +50,7 @@ export default function TeamScreen({ route }: any) { wins: team.wins, losses: team.losses, ties: team.ties, - mediaPaths, - scoutingData: team.scoutingData + mediaPaths }); } const uploadPhoto = async () => { @@ -83,8 +80,7 @@ export default function TeamScreen({ route }: any) { wins: team.wins, losses: team.losses, ties: team.ties, - mediaPaths, - scoutingData: team.scoutingData + mediaPaths }); } const deletePhoto = async (path: string) => { @@ -98,8 +94,7 @@ export default function TeamScreen({ route }: any) { wins: team.wins, losses: team.losses, ties: team.ties, - mediaPaths, - scoutingData: team.scoutingData + mediaPaths }); } @@ -156,11 +151,9 @@ export default function TeamScreen({ route }: any) { {team.name} {team.number} - - + + - - ); diff --git a/types/DBTypes.ts b/types/DBTypes.ts index 31176ea..162c18c 100644 --- a/types/DBTypes.ts +++ b/types/DBTypes.ts @@ -1,4 +1,3 @@ -import { ScoutingData } from "./TemplateTypes"; export interface Event { id: string; @@ -16,7 +15,6 @@ export interface Team { losses: number; ties: number; mediaPaths: string[]; - scoutingData: ScoutingData[]; } export interface Match { diff --git a/types/TemplateTypes.ts b/types/TemplateTypes.ts index 331b998..53ba9b0 100644 --- a/types/TemplateTypes.ts +++ b/types/TemplateTypes.ts @@ -9,8 +9,7 @@ export enum ElementType { text, hr, counter, - checkbox, - textbox + checkbox } export interface ElementData { @@ -18,7 +17,7 @@ export interface ElementData { type: ElementType; label: string; options: any; - value: number | boolean | string | undefined; + value: number | boolean | undefined; } export interface ElementProps { @@ -32,5 +31,6 @@ export type ScoutingTemplate = ElementData[]; export interface ScoutingData { matchID: string; - values: (number | boolean | string)[] + teamID: string; + values: (number | boolean)[] } \ No newline at end of file From c3c1edd04cf3cd37e306946202b363c5a2ae7b6c Mon Sep 17 00:00:00 2001 From: DigiWorm Date: Thu, 10 Feb 2022 20:38:48 -0600 Subject: [PATCH 30/38] Scouting Team Charts --- components/elements/CheckboxElement.tsx | 13 ++- components/text/Subtitle.tsx | 3 +- screens/Match/MatchScreen.tsx | 14 +-- screens/Matches/TeamPreview.tsx | 22 ++-- screens/Settings/AboutScreen.tsx | 17 ++- screens/Settings/ColorPicker.tsx | 13 ++- screens/Settings/Download/DownloadScreen.tsx | 12 +- screens/Settings/PaletteScreen.tsx | 16 ++- screens/Settings/SettingsScreen.tsx | 98 +++++++++-------- .../Settings/Template/EditTemplateScreen.tsx | 17 ++- screens/Sharing/PrintSummaryScreen.tsx | 50 ++++----- screens/Sharing/SharingScreen.tsx | 21 ++++ screens/Team/Stats/StatChart.tsx | 75 +++++++++++++ screens/Team/Stats/StatSquare.tsx | 5 +- screens/Team/Stats/StatTable.tsx | 103 +++++++++--------- screens/Team/TeamBanner.tsx | 4 +- screens/Team/TeamScreen.tsx | 24 ++-- types/TemplateTypes.ts | 4 +- 18 files changed, 318 insertions(+), 193 deletions(-) create mode 100644 screens/Team/Stats/StatChart.tsx diff --git a/components/elements/CheckboxElement.tsx b/components/elements/CheckboxElement.tsx index 9b48127..f6a4d01 100644 --- a/components/elements/CheckboxElement.tsx +++ b/components/elements/CheckboxElement.tsx @@ -7,11 +7,11 @@ import Text from '../text/Text'; export default function CheckboxElement(props: ElementProps) { let elementData = props.data; const defaultValue = elementData.options.defaultValue; - const [isChecked, setChecked] = React.useState(defaultValue === undefined ? defaultValue as boolean : false); + const [value, setValue] = React.useState(defaultValue ? defaultValue as number : 0); // Default Value if (elementData.value === undefined && props.onChange) { - elementData.value = false; + elementData.value = value; props.onChange(elementData); } @@ -19,10 +19,11 @@ export default function CheckboxElement(props: ElementProps) { const changeChecked = (isChecked: boolean) => { // Value - setChecked(isChecked); - elementData.value = isChecked; + const newValue = isChecked ? 1 : 0; + setValue(newValue); + elementData.value = newValue; if (props.isEditable) - elementData.options.defaultValue = isChecked; + elementData.options.defaultValue = newValue; // Vibrate if (isChecked) @@ -45,7 +46,7 @@ export default function CheckboxElement(props: ElementProps) { {props.isEditable ? diff --git a/components/text/Subtitle.tsx b/components/text/Subtitle.tsx index 5015858..b8ab512 100644 --- a/components/text/Subtitle.tsx +++ b/components/text/Subtitle.tsx @@ -11,6 +11,7 @@ export default function Subtitle(props: TextProps) { return ; } \ No newline at end of file diff --git a/screens/Match/MatchScreen.tsx b/screens/Match/MatchScreen.tsx index 91dbb33..6111beb 100644 --- a/screens/Match/MatchScreen.tsx +++ b/screens/Match/MatchScreen.tsx @@ -18,8 +18,8 @@ import TeamPreview from "../Matches/TeamPreview"; export default function MatchScreen({ route }: any) { const paletteContext = React.useContext(PaletteContext); const navigator = useNavigation(); - const [match, setMatch] = useMatch(route.params.matchID); - const [template, setTemplate] = useTemplate(TemplateType.Match); + const [match] = useMatch(route.params.matchID); + const [template] = useTemplate(TemplateType.Match); // Browser Button const onBrowserButton = () => { @@ -86,20 +86,20 @@ const styles = StyleSheet.create({ paddingBottom: 5, paddingTop: 5, marginRight: 8, - borderTopLeftRadius: 10, - borderTopRightRadius: 10, + borderRadius: 6, + marginBottom: 5, fontSize: 18, fontWeight: "bold", width: 300, textAlign: "center" }, allianceFooter: { - borderBottomLeftRadius: 10, - borderBottomRightRadius: 10, + borderRadius: 10, width: 300, height: 10, marginRight: 8, - marginBottom: 20 + marginBottom: 20, + marginTop: 2 }, headerButtons: { alignSelf: "flex-end", diff --git a/screens/Matches/TeamPreview.tsx b/screens/Matches/TeamPreview.tsx index e2cc1df..971192a 100644 --- a/screens/Matches/TeamPreview.tsx +++ b/screens/Matches/TeamPreview.tsx @@ -32,11 +32,14 @@ export default function TeamPreview(props: { teamID: string }) { } else { mediaIcon = ( - - + + + + + ); } @@ -49,7 +52,7 @@ export default function TeamPreview(props: { teamID: string }) { {mediaIcon} - {team.name} + {team.name.substring(0, 16) + (team.name.length > 16 ? "..." : "")} {team.number} @@ -85,11 +88,10 @@ const styles = StyleSheet.create({ thumbnail: { width: "100%", aspectRatio: 1, - backgroundColor: "#444", - padding: 0, - margin: 0, + padding: 1, justifyContent: "center", - alignItems: "center" + alignItems: "center", + borderRadius: 8 }, title: { fontSize: 16, diff --git a/screens/Settings/AboutScreen.tsx b/screens/Settings/AboutScreen.tsx index 67a737c..05301b2 100644 --- a/screens/Settings/AboutScreen.tsx +++ b/screens/Settings/AboutScreen.tsx @@ -2,9 +2,9 @@ import { MaterialIcons } from "@expo/vector-icons"; import * as Application from 'expo-application'; import { Accelerometer } from 'expo-sensors'; import * as React from "react"; -import { Image } from "react-native"; +import { Image, StyleSheet } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; import HorizontalBar from "../../components/common/HorizontalBar"; -import ScrollContainer from "../../components/containers/ScrollContainer"; import Subtitle from "../../components/text/Subtitle"; import Text from "../../components/text/Text"; import Title from "../../components/text/Title"; @@ -35,7 +35,7 @@ export default function AboutScreen() { }, [accelerometer, setEasterEgg, isEasterEgg]); return ( - + Blitz Scouter Version {Application.nativeApplicationVersion} @@ -58,5 +58,12 @@ export default function AboutScreen() { {isEasterEgg ? : null} - ) -} \ No newline at end of file + ) +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20 + } +}) \ No newline at end of file diff --git a/screens/Settings/ColorPicker.tsx b/screens/Settings/ColorPicker.tsx index e7cf06e..601a14d 100644 --- a/screens/Settings/ColorPicker.tsx +++ b/screens/Settings/ColorPicker.tsx @@ -1,9 +1,10 @@ import { useNavigation } from "@react-navigation/native"; import * as React from "react"; import { StyleSheet, TextInput, View } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; import Button from "../../components/common/Button"; import HorizontalBar from "../../components/common/HorizontalBar"; -import ScrollContainer from "../../components/containers/ScrollContainer"; +import Subtitle from "../../components/text/Subtitle"; import Text from "../../components/text/Text"; import Title from "../../components/text/Title"; import { PaletteContext } from "../../context/PaletteContext"; @@ -20,9 +21,9 @@ export function ColorPickerScreen({ route }: any) { } return ( - + Color Picker - + Pick a color, any color - + ); } const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20 + }, textInput: { borderRadius: 10, padding: 10, diff --git a/screens/Settings/Download/DownloadScreen.tsx b/screens/Settings/Download/DownloadScreen.tsx index c1cb26a..f6ce0d4 100644 --- a/screens/Settings/Download/DownloadScreen.tsx +++ b/screens/Settings/Download/DownloadScreen.tsx @@ -1,9 +1,9 @@ import { useNavigation } from "@react-navigation/core"; import React from "react"; import { StyleSheet, View } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; import { DownloadEvent } from "../../../api/TBAAdapter"; import StandardButton from "../../../components/common/StandardButton"; -import ScrollContainer from "../../../components/containers/ScrollContainer"; import Text from "../../../components/text/Text"; import Title from "../../../components/text/Title"; import { PaletteContext } from "../../../context/PaletteContext"; @@ -22,9 +22,7 @@ export default function DownloadScreen({ route }: any) { } return ( - - - + {downloadStatus === "" ? Download Event @@ -45,15 +43,15 @@ export default function DownloadScreen({ route }: any) { {downloadStatus} } - + ); } const styles = StyleSheet.create({ container: { - width: "100%", - height: "100%" + paddingLeft: 20, + paddingRight: 20 }, title: { marginBottom: 15 diff --git a/screens/Settings/PaletteScreen.tsx b/screens/Settings/PaletteScreen.tsx index e35b14a..8038074 100644 --- a/screens/Settings/PaletteScreen.tsx +++ b/screens/Settings/PaletteScreen.tsx @@ -1,8 +1,9 @@ import { useNavigation } from "@react-navigation/native"; import * as React from "react"; +import { StyleSheet } from "react-native"; +import { ScrollView } from "react-native-gesture-handler"; import HorizontalBar from "../../components/common/HorizontalBar"; import StandardButton from "../../components/common/StandardButton"; -import ScrollContainer from "../../components/containers/ScrollContainer"; import Subtitle from "../../components/text/Subtitle"; import Title from "../../components/text/Title"; import { PaletteContext } from "../../context/PaletteContext"; @@ -22,7 +23,7 @@ export function PaletteScreen() { } return ( - + Color Palette Change the color palette to match your team @@ -75,6 +76,13 @@ export function PaletteScreen() { subtitle="Used globally for subtitles" onPress={() => { promptForColor("textSecondary"); }} /> - + ); -} \ No newline at end of file +} + +const styles = StyleSheet.create({ + container: { + paddingLeft: 20, + paddingRight: 20 + } +}) \ No newline at end of file diff --git a/screens/Settings/SettingsScreen.tsx b/screens/Settings/SettingsScreen.tsx index 9bfa04f..2a8b5e9 100644 --- a/screens/Settings/SettingsScreen.tsx +++ b/screens/Settings/SettingsScreen.tsx @@ -5,6 +5,8 @@ import HorizontalBar from '../../components/common/HorizontalBar'; import StandardButton from '../../components/common/StandardButton'; import ScrollContainer from '../../components/containers/ScrollContainer'; import NavTitle from '../../components/text/NavTitle'; +import Subtitle from '../../components/text/Subtitle'; +import Title from '../../components/text/Title'; import { DARK_PALETTE, LIGHT_PALETTE, PaletteContext } from '../../context/PaletteContext'; import useEvent from '../../hooks/useEvent'; import { getMatch } from '../../hooks/useMatch'; @@ -57,37 +59,52 @@ export default function SettingsScreen() { ); } - const generateRandomData = async () => { - const scoutingData: ScoutingData[] = []; - for (let matchID of event.matchIDs) { - const match = await getMatch(matchID); - - if (!match) - continue; + const generateRandomData = async (isConfirmed: boolean) => { + if (!isConfirmed) { + Alert.alert("Are you sure?", "This will replace all scouting data on your device.", + [ + { + text: "Confirm", + onPress: async () => { + await generateRandomData(true); + Alert.alert("Success!", "All scouting data has been randomized"); + } + }, + { + text: "Cancel", + style: "cancel" + } + ], { cancelable: true } + ); + } else { + const scoutingData: ScoutingData[] = []; + for (let matchID of event.matchIDs) { + const match = await getMatch(matchID); + + if (!match) + continue; - console.log(match.id); + console.log(match.id); - const teamIDs = match.blueTeamIDs; - teamIDs.push(...match.redTeamIDs); + const teamIDs = match.blueTeamIDs; + teamIDs.push(...match.redTeamIDs); - for (let teamID of teamIDs) { - const team = await getTeam(teamID); - if (!team) - continue; + for (let teamID of teamIDs) { + const team = await getTeam(teamID); + if (!team) + continue; - const values = template.filter(elem => elem.value != undefined).map((elem) => typeof elem.value === "number" ? Math.round(Math.random() * (50 - team.rank)) : Math.random() < .5); + const values = template.filter(elem => elem.value != undefined).map((elem) => Math.round(15 * Math.random() * (1 - (team.rank / event.teamIDs.length)))); - //if (scoutingData.filter(scout => scout.teamID === teamID).length <= 2) - scoutingData.push({ - matchID, - teamID, - values - }); + scoutingData.push({ + matchID, + teamID, + values + }); + } } + await setScoutingData(scoutingData); } - await setScoutingData(scoutingData); - - Alert.alert("Success", "Successfully filled with random data!"); } return ( @@ -113,8 +130,6 @@ export default function SettingsScreen() { subtitle={"Resets to the default dark palette"} onPress={() => { paletteContext.setPalette(DARK_PALETTE); ToastAndroid.show("Dark Mode!", ToastAndroid.SHORT); }} /> - - {/* Scouting Buttons */} { navigator.navigate("EditTemplate", { templateType: TemplateType.Match }); }} /> + { navigator.navigate("About"); }} /> + + + Danger Zone + Actions here may overwrite your scouting data + { generateRandomData(); }} /> - {/* - { navigator.navigate("EditTemplate", { templateType: TemplateType.Pit }); }} /> - { navigator.navigate("DefaultTeam"); }} />*/} - - + onPress={() => { generateRandomData(false); }} /> { clearData(); }} /> - { navigator.navigate("About"); }} /> - ); } \ No newline at end of file diff --git a/screens/Settings/Template/EditTemplateScreen.tsx b/screens/Settings/Template/EditTemplateScreen.tsx index 09fbf54..10a5d53 100644 --- a/screens/Settings/Template/EditTemplateScreen.tsx +++ b/screens/Settings/Template/EditTemplateScreen.tsx @@ -2,9 +2,8 @@ import { MaterialIcons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Alert, StyleSheet, View } from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; import Button from '../../../components/common/Button'; -import HorizontalBar from '../../../components/common/HorizontalBar'; -import ScrollContainer from '../../../components/containers/ScrollContainer'; import ScoutingElement from '../../../components/elements/ScoutingElement'; import Subtitle from '../../../components/text/Subtitle'; import Text from '../../../components/text/Text'; @@ -58,11 +57,10 @@ export default function EditTemplateScreen({ route }: any) { }); return ( - - + + Edit Template {TEMPLATE_NAMES[templateType]} Scouting - {template.length > 0 ? template.map(element => - +
`; for (const element of template) - if (typeof element.value === "number") + if (element.value !== undefined) printData += `` + element.label + `Notes

` + team.number + `

` + team.name + `

` + (Math.round(stat.average * 100) / 100) + `

` + (Math.round(avg * 100) / 100) + `