complete upload and download functionality

pull/79/head
Jon Eugster 3 years ago
parent a52f10ab11
commit ab0cb5ba3d

@ -3,14 +3,31 @@ import { Button } from './Button'
import { GameIdContext } from '../App'; import { GameIdContext } from '../App';
import { useStore } from 'react-redux'; import { useStore } from 'react-redux';
import { useAppDispatch, useAppSelector } from '../hooks'; import { useAppDispatch, useAppSelector } from '../hooks';
import { useSelector } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDownload, faUpload, faEraser } from '@fortawesome/free-solid-svg-icons' import { faDownload, faUpload, faEraser } from '@fortawesome/free-solid-svg-icons'
import { deleteProgress } from '../state/progress'; import { deleteProgress, selectProgress, loadProgress, GameProgressState } from '../state/progress';
const downloadFile = ({ data, fileName, fileType }) => {
const blob = new Blob([data], { type: fileType })
const a = document.createElement('a')
a.download = fileName
a.href = window.URL.createObjectURL(blob)
const clickEvt = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
})
a.dispatchEvent(clickEvt)
a.remove()
}
function GameMenu() { function GameMenu() {
const [file, setFile] = React.useState<File>();
const gameId = React.useContext(GameIdContext) const gameId = React.useContext(GameIdContext)
const store = useStore() const store = useStore()
@ -18,24 +35,60 @@ function GameMenu() {
const openEraseMenu = () => setEraseMenu(true); const openEraseMenu = () => setEraseMenu(true);
const closeEraseMenu = () => setEraseMenu(false); const closeEraseMenu = () => setEraseMenu(false);
const [uploadMenu, setUploadMenu] = React.useState(false);
const openUploadMenu = () => setUploadMenu(true);
const closeUploadMenu = () => setUploadMenu(false);
const gameProgress = useSelector(selectProgress(gameId))
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const downloadProgress = () => {}; const downloadProgress = (e) => {
// const uploadProgress = () => {}; e.preventDefault()
downloadFile({
data: JSON.stringify(gameProgress),
fileName: `lean4game-${gameId}-${new Date().toLocaleDateString()}.json`,
fileType: 'text/json',
})
};
const handleFileChange = (e) => {
if (e.target.files) {
setFile(e.target.files[0]);
}
};
const uploadProgress = (e) => {
if (!file) {
return;
}
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e) => {
const data = JSON.parse(e.target.result.toString()) as GameProgressState;
console.debug("Json Data", data);
dispatch(loadProgress({game: gameId, data: data}))
}
closeUploadMenu()
}
const eraseProgress = () => { const eraseProgress = () => {
dispatch(deleteProgress({game: gameId})) dispatch(deleteProgress({game: gameId}))
closeEraseMenu() closeEraseMenu()
} }
const downloadAndErase = () => { const downloadAndErase = (e) => {
downloadProgress () downloadProgress(e)
eraseProgress() eraseProgress()
} }
return <nav className="game-menu"> return <nav className="game-menu">
<Button disabled={true} onClick={downloadProgress} title="Download game progress" to=""><FontAwesomeIcon icon={faDownload} /></Button> <Button onClick={downloadProgress} title="Download game progress" to=""><FontAwesomeIcon icon={faDownload} /></Button>
<Button disabled={true} title="Load game progress from JSON" to=""><FontAwesomeIcon icon={faUpload} /></Button> <Button title="Load game progress from JSON" onClick={openUploadMenu} to=""><FontAwesomeIcon icon={faUpload} /></Button>
<Button title="Clear game progress" to="" onClick={openEraseMenu}><FontAwesomeIcon icon={faEraser} /></Button> <Button title="Clear game progress" to="" onClick={openEraseMenu}><FontAwesomeIcon icon={faEraser} /></Button>
{eraseMenu? {eraseMenu?
@ -45,15 +98,33 @@ function GameMenu() {
<div className="codicon codicon-close modal-close" onClick={closeEraseMenu}></div> <div className="codicon codicon-close modal-close" onClick={closeEraseMenu}></div>
<h2>Delete Progress?</h2> <h2>Delete Progress?</h2>
<p>Do you want to delete your saved state irreversibly?</p> <p>Do you want to delete your saved progress irreversibly?</p>
<p>(This only affects your saved proofs, no levels are ever locked. <p>(This only affects your saved proofs, no levels are ever locked.
Saves from other games are not deleted.)</p> Saves from other games are not deleted.)</p>
<Button onClick={eraseProgress} to="">Delete</Button> <Button onClick={eraseProgress} to="">Delete</Button>
<Button disabled={true} onClick={downloadAndErase} to="">Download & Delete</Button> <Button onClick={downloadAndErase} to="">Download & Delete</Button>
<Button onClick={closeEraseMenu} to="">Cancel</Button> <Button onClick={closeEraseMenu} to="">Cancel</Button>
</div> </div>
</div> : null} </div> : null}
{uploadMenu ?
<div className="modal-wrapper">
<div className="modal-backdrop" onClick={closeUploadMenu} />
<div className="modal">
<div className="codicon codicon-close modal-close" onClick={closeUploadMenu}></div>
<h2>Upload Saved Progress</h2>
<p>Select a JSON file with the saved game progress to load your progress.</p>
<p><b>Warning:</b> This will delete your current game progress!
Consider <a className="download-link" onClick={downloadProgress} >downloading your current progress</a> first!</p>
<input type="file" onChange={handleFileChange}/>
<Button to="" onClick={uploadProgress}>Load selected file</Button>
</div>
</div> : null}
</nav> </nav>
} }

@ -23,6 +23,10 @@
padding: 20px; padding: 20px;
} }
i {
font-style: italic;
}
h1 { h1 {
font-size: 2em; font-size: 2em;
margin: .67em 0; margin: .67em 0;
@ -203,3 +207,10 @@ svg .world-title {
margin-right: .4em; margin-right: .4em;
margin-bottom: .2em; margin-bottom: .2em;
} }
.modal a.download-link {
cursor: pointer;
font-style: italic;
text-decoration: underline dotted;
}

@ -50,7 +50,7 @@ const customBaseQuery = async (
let leanClient = await connection.startLeanClient(args.game) let leanClient = await connection.startLeanClient(args.game)
console.log(`Sending request ${args.method}`) console.log(`Sending request ${args.method}`)
let res = await leanClient.sendRequest(args.method, args.params) let res = await leanClient.sendRequest(args.method, args.params)
console.log('Received response', res) console.log('Received response') //, res)
return {'data': res} return {'data': res}
} catch (e) { } catch (e) {
return {'error': e} return {'error': e}

@ -2,8 +2,13 @@ import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit'
import { loadState } from "./localStorage"; import { loadState } from "./localStorage";
export interface GameProgressState {
[world: string] : {[level: number]: LevelProgressState}
}
interface ProgressState { interface ProgressState {
level: {[game: string]: {[world: string]: {[level: number]: LevelProgressState}}} level: {[game: string]: GameProgressState}
} }
interface Selection { interface Selection {
selectionStartLineNumber: number, selectionStartLineNumber: number,
@ -52,6 +57,10 @@ export const progressSlice = createSlice({
deleteProgress(state, action: PayloadAction<{game: string}>) { deleteProgress(state, action: PayloadAction<{game: string}>) {
state.level[action.payload.game] = {} state.level[action.payload.game] = {}
}, },
loadProgress(state, action: PayloadAction<{game: string, data:GameProgressState}>) {
console.debug(`setting data to:\n ${action.payload.data}`)
state.level[action.payload.game] = action.payload.data
},
} }
}) })
@ -82,10 +91,10 @@ export function selectCompleted(game: string, world: string, level: number) {
} }
} }
export function selectProgress() { export function selectProgress(game: string) {
return (state) => { return (state) => {
return state.progress return state.progress.level[game] ?? null
} }
} }
export const { changedSelection, codeEdited, levelCompleted, deleteProgress } = progressSlice.actions export const { changedSelection, codeEdited, levelCompleted, deleteProgress, loadProgress } = progressSlice.actions

Loading…
Cancel
Save