complete upload and download functionality
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user