From fee692613b81c061c42c6b9cd5fc32dda6562e2b Mon Sep 17 00:00:00 2001 From: Justin Wyne <1986068+wyne@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:15:58 -0800 Subject: [PATCH 1/8] Export game to image --- package-lock.json | 57 +++++++++++++++++ package.json | 3 +- src/Navigation.tsx | 11 ++++ src/components/GameListItem.tsx | 107 +++++++++++++++++++++++++------- src/screens/ExportScreen.tsx | 102 ++++++++++++++++++++++++++++++ 5 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 src/screens/ExportScreen.tsx diff --git a/package-lock.json b/package-lock.json index 203c5195..2f6243bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.0", "react-native-size-matters": "^0.4.0", + "react-native-view-shot": "3.7.0", "react-native-web": "~0.19.6", "react-navigation": "^4.4.4", "react-navigation-stack": "^2.10.4", @@ -9129,6 +9130,14 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "funding": [ @@ -9953,6 +9962,14 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -12803,6 +12820,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "license": "MIT", @@ -19723,6 +19752,18 @@ "generate-icon": "bin/generate-icon.js" } }, + "node_modules/react-native-view-shot": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.7.0.tgz", + "integrity": "sha512-tQruLNjs7Ee/p6xUgJqF6glnatHaq/UqaIQ6KdYIFG0+XpUZdhqmEM4WMLsYfayfFEhdlF86G1S3eXMOfDNzFg==", + "dependencies": { + "html2canvas": "^1.4.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-web": { "version": "0.19.7", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.7.tgz", @@ -21353,6 +21394,14 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -21791,6 +21840,14 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "8.3.2", "license": "MIT", diff --git a/package.json b/package.json index 4046d5e7..6c2a4494 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "react-native-safe-area-context": "4.6.3", "react-native-screens": "~3.22.0", "react-native-size-matters": "^0.4.0", + "react-native-view-shot": "3.7.0", "react-native-web": "~0.19.6", "react-navigation": "^4.4.4", "react-navigation-stack": "^2.10.4", @@ -95,4 +96,4 @@ "private": true, "name": "scorepad", "version": "1.0.0" -} +} \ No newline at end of file diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 63acb90d..24101499 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -8,6 +8,7 @@ import GameScreen from "../src/screens/GameScreen"; import SettingsScreen from "../src/screens/SettingsScreen"; import AppInfoScreen from "../src/screens/AppInfoScreen"; import OnboardingScreen from '../src/screens/OnboardingScreen'; +import ExportScreen from '../src/screens/ExportScreen'; import HomeHeader from '../src/components/Headers/HomeHeader'; import GameHeader from '../src/components/Headers/GameHeader'; import SettingsHeader from '../src/components/Headers/SettingsHeader'; @@ -24,6 +25,7 @@ export type RootStackParamList = { Game: undefined; Settings: undefined; AppInfo: undefined; + Export: undefined; Onboarding: OnboardingScreenParamList; Tutorial: OnboardingScreenParamList; }; @@ -102,6 +104,15 @@ export const Navigation = () => { }, }} /> + { + return ; + }, + }} + /> = ({ navigation, game, index resolve(); }); - // Tap + /** + * Choose Game and navigate to GameScreen + */ const chooseGameHandler = async () => { asyncSetCurrentGame(dispatch).then(() => { navigation.navigate("Game"); @@ -50,7 +53,48 @@ const GameListItem: React.FunctionComponent = ({ navigation, game, index }); }; - // Long Press + const actions: MenuAction[] = [ + { + id: 'rename', + title: `Rename`, + image: Platform.select({ + ios: 'pencil', + android: 'create', + }), + }, + { + id: 'export', + title: `Export`, + image: Platform.select({ + ios: 'square.and.arrow.up', + android: 'ic_menu_share', + }), + }, + { + id: 'delete', + title: `Delete`, + attributes: { + destructive: true, + }, + image: Platform.select({ + ios: 'trash', + android: 'ic_menu_delete', + }), + }, + ]; + + /** + * Export Game + */ + const exportGameHandler = async () => { + asyncSetCurrentGame(dispatch).then(() => { + navigation.navigate("Export"); + }); + }; + + /** + * Delete Game + */ const deleteGameHandler = async () => { Alert.alert( 'Delete Game', @@ -78,29 +122,46 @@ const GameListItem: React.FunctionComponent = ({ navigation, game, index }); }; + type MenuActionHandler = (eativeEvent: NativeActionEvent) => void; + + const menuActionHandler: MenuActionHandler = async ({ nativeEvent }) => { + switch (nativeEvent.event) { + case 'export': + exportGameHandler(); + break; + case 'delete': + deleteGameHandler(); + break; + } + }; + return ( - - - {game.title} - - {game.dateCreated} - - - {playerNames.join(', ')} - - - - {playerNames.length} - - - {rounds + 1} - - - + + + + {game.title} + + {game.dateCreated} + + + {playerNames.join(', ')} + + + + {playerNames.length} + + + {rounds + 1} + + + + ); }; diff --git a/src/screens/ExportScreen.tsx b/src/screens/ExportScreen.tsx new file mode 100644 index 00000000..c1a0f662 --- /dev/null +++ b/src/screens/ExportScreen.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useRef } from 'react'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { View, StyleSheet, ScrollView, Text } from 'react-native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { ParamListBase } from '@react-navigation/native'; +import { captureRef } from "react-native-view-shot"; + +import { selectGameById } from '../../redux/GamesSlice'; +import RoundScoreColumn from '../components/ScoreLog/RoundScoreColumn'; +import TotalScoreColumn from '../components/ScoreLog/TotalScoreColumn'; +import PlayerNameColumn from '../components/ScoreLog/PlayerNameColumn'; +import { useAppSelector } from '../../redux/hooks'; +import { Button } from 'react-native-elements'; + +interface Props { + navigation: NativeStackNavigationProp; +} + +const ExportScreen: React.FunctionComponent = ({ navigation }) => { + const currentGameId = useAppSelector(state => state.settings.currentGameId); + + if (typeof currentGameId == 'undefined') return null; + + const roundCurrent = useAppSelector(state => selectGameById(state, currentGameId)?.roundCurrent || 0); + const roundTotal = useAppSelector(state => selectGameById(state, currentGameId)?.roundTotal || 0); + + const currentGame = useAppSelector(state => selectGameById(state, state.settings.currentGameId)); + if (typeof currentGame == 'undefined') return null; + + + const roundsScrollViewEl = useRef(null); + + const roundsIterator = [...Array(roundTotal + 1).keys()]; + + const exportImage = () => { + if (roundsScrollViewEl.current == null) return; + + captureRef(roundsScrollViewEl, { + result: 'tmpfile', + quality: 1, + format: 'png', + snapshotContentContainer: true, + }).then(uri => { + console.log("do something with ", uri); + }); + }; + + return ( + + + + + + {currentGame?.title} + + + Created: {new Date(currentGame.dateCreated).toLocaleDateString()} +   {new Date(currentGame.dateCreated).toLocaleTimeString()} + + + + + + {roundsIterator.map((item, round) => ( + + + + ))} + + + +