diff --git a/.jshintrc b/.jshintrc index ce7eed7d7..897804370 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,11 +1,9 @@ { "browser": true, - "jquery": true, - "mootools": true, - "node": true, "undef": true, "sub": true, "newcap": false, + "esversion": 9, "indent": 4, "globals": { "alert": true, diff --git a/tnoodle-ui/src/main/api/tnoodle.api.js b/tnoodle-ui/src/main/api/tnoodle.api.js index 87cad7c4a..f4ef4fb26 100644 --- a/tnoodle-ui/src/main/api/tnoodle.api.js +++ b/tnoodle-ui/src/main/api/tnoodle.api.js @@ -9,7 +9,7 @@ let bestMbldAttemptEndpoint = "/frontend/mbld/best"; let wcaEventsEndpoint = "/frontend/data/events"; let formatsEndpoint = "/frontend/data/formats"; -export const fetchZip = (wcif, mbld, password, translations) => { +export const fetchZip = (scrambleClient, wcif, mbld, password, translations) => { let payload = { wcif, multiCubes: { requestedScrambles: mbld }, @@ -20,11 +20,20 @@ export const fetchZip = (wcif, mbld, password, translations) => { payload.zipPassword = password; } - return postToTnoodle(zipEndpoint, payload) - .then((response) => response.blob()) + let targetMarker = wcif.id; + + return scrambleClient.loadScrambles(zipEndpoint, payload, targetMarker) + .then((result) => convertToBlob(result)) .catch((error) => console.error(error)); }; +const convertToBlob = async (result) => { + let {contentType, payload} = result; + let res = await fetch(`data:${contentType};base64,${payload}`); + + return await res.blob(); +}; + export const fetchWcaEvents = () => { return fetch(baseUrl + wcaEventsEndpoint) .then((response) => response.json()) diff --git a/tnoodle-ui/src/main/api/tnoodle.socket.js b/tnoodle-ui/src/main/api/tnoodle.socket.js new file mode 100644 index 000000000..29a777e20 --- /dev/null +++ b/tnoodle-ui/src/main/api/tnoodle.socket.js @@ -0,0 +1,77 @@ +export class ScrambleClient { + constructor(onHandshake, onProgress) { + this.onHandshake = onHandshake; + this.onProgress = onProgress; + + this.state = SCRAMBLING_STATES.IDLE; + + this.contentType = null; + this.resultPayload = null; + } + + loadScrambles(endpoint, payload, targetMarker) { + let that = this; + + return new Promise(function (resolve, reject) { + let ws = new WebSocket(BASE_URL + endpoint); + + ws.onopen = () => { + that.state = SCRAMBLING_STATES.INITIATE; + ws.send(JSON.stringify(payload)); + }; + + ws.onerror = (err) => { + reject(err); + }; + + ws.onclose = (cls) => { + if (that.state === SCRAMBLING_STATES.DONE && cls.wasClean) { + let resultObject = { + contentType: that.contentType, + payload: that.resultPayload + }; + + resolve(resultObject); + } else { + reject(cls); + } + }; + + ws.onmessage = (msg) => { + if (that.state === SCRAMBLING_STATES.INITIATE) { + that.state = SCRAMBLING_STATES.SCRAMBLING; + + let rawPayload = msg.data.toString(); + let targetPayload = JSON.parse(rawPayload); + + that.onHandshake(targetPayload); + } else if (that.state === SCRAMBLING_STATES.SCRAMBLING) { + if (msg.data === targetMarker) { + that.state = SCRAMBLING_STATES.COMPUTED_TYPE; + } else { + that.onProgress(msg.data); + } + } else if (that.state === SCRAMBLING_STATES.COMPUTED_TYPE) { + that.state = SCRAMBLING_STATES.COMPUTED_DATA; + + that.contentType = msg.data; + } else if (that.state === SCRAMBLING_STATES.COMPUTED_DATA) { + that.state = SCRAMBLING_STATES.DONE; + + that.resultPayload = msg.data; + } + }; + }); + } +} + +const BASE_URL = window.location.origin.replace(/^https?:\/\//,'ws://'); + +const SCRAMBLING_STATES = { + IDLE: "IDLE", + INITIATE: "INITIATE", + SCRAMBLING: "SCRAMBLING", + COMPUTED_TYPE: "COMPUTED_TYPE", + COMPUTED_DATA: "COMPUTED_DATA", + DONE: "DONE" +}; diff --git a/tnoodle-ui/src/main/components/EventPicker.jsx b/tnoodle-ui/src/main/components/EventPicker.jsx index ca53a08a3..025b6ae5d 100644 --- a/tnoodle-ui/src/main/components/EventPicker.jsx +++ b/tnoodle-ui/src/main/components/EventPicker.jsx @@ -9,11 +9,16 @@ import { import MbldDetail from "./MbldDetail"; import FmcTranslationsDetail from "./FmcTranslationsDetail"; import "./EventPicker.css"; +import { ProgressBar } from "react-bootstrap"; const mapStateToProps = (store) => ({ editingDisabled: store.editingDisabled, wcif: store.wcif, wcaFormats: store.wcaFormats, + generatingScrambles: store.generatingScrambles, + scramblingProgressTarget: store.scramblingProgressTarget, + scramblingProgressCurrent: store.scramblingProgressCurrent, + fileZipBlob: store.fileZipBlob }); const mapDispatchToProps = { @@ -185,10 +190,38 @@ const EventPicker = connect( ); }; - render() { - let wcaEvent = this.props.wcif.events.find( - (event) => event.id === this.props.event.id + maybeShowProgressBar = (rounds) => { + let eventId = this.props.event.id; + + let current = this.props.scramblingProgressCurrent[eventId] || 0; + let target = this.props.scramblingProgressTarget[eventId]; + + if (rounds.length === 0 || !this.props.generatingScrambles || target === undefined) { + return; + } + + let progress = (current / target) * 100 + let miniThreshold = 2; + + if (progress === 0) { + progress = miniThreshold; + } + + return ( + ); + }; + + render() { + let wcaEvent = this.props.wcifEvent; let rounds = wcaEvent != null ? wcaEvent.rounds : []; return ( @@ -219,6 +252,7 @@ const EventPicker = connect(
{this.props.event.name}
+ {this.maybeShowProgressBar(rounds)} diff --git a/tnoodle-ui/src/main/components/Main.jsx b/tnoodle-ui/src/main/components/Main.jsx index 257162750..c01b0e772 100644 --- a/tnoodle-ui/src/main/components/Main.jsx +++ b/tnoodle-ui/src/main/components/Main.jsx @@ -4,7 +4,14 @@ import EventPickerTable from "./EventPickerTable"; import Interceptor from "./Interceptor"; import VersionInfo from "./VersionInfo"; import { fetchZip } from "../api/tnoodle.api"; -import { updateFileZipBlob } from "../redux/ActionCreators"; +import { ScrambleClient } from "../api/tnoodle.socket"; +import { + updateFileZipBlob, + updateGeneratingScrambles, + updateScramblingProgressTarget, + updateScramblingProgressCurrentEvent, + resetScramblingProgressCurrent +} from "../redux/ActionCreators"; import { connect } from "react-redux"; import { isUsingStaging } from "../api/wca.api"; import "./Main.css"; @@ -17,10 +24,15 @@ const mapStateToProps = (store) => ({ officialZip: store.officialZip, fileZipBlob: store.fileZipBlob, translations: store.translations, + generatingScrambles: store.generatingScrambles }); const mapDispatchToProps = { updateFileZipBlob, + updateGeneratingScrambles, + updateScramblingProgressTarget, + updateScramblingProgressCurrentEvent, + resetScramblingProgressCurrent }; const Main = connect( @@ -32,14 +44,13 @@ const Main = connect( super(props); this.state = { - generatingScrambles: false, competitionNameFileZip: "", }; } onSubmit = (evt) => { evt.preventDefault(); - if (this.state.generatingScrambles) { + if (this.props.generatingScrambles) { return; } @@ -50,26 +61,25 @@ const Main = connect( } }; - setGeneratingScrambles = (flag) => { - this.setState({ ...this.state, generatingScrambles: flag }); - }; - generateZip = () => { // If user navigates during generation proccess, we still get the correct name this.setState({ ...this.state, competitionNameFileZip: this.props.wcif.name, - generatingScrambles: true, }); + let scrambleClient = new ScrambleClient(this.props.updateScramblingProgressTarget, this.props.updateScramblingProgressCurrentEvent); fetchZip( + scrambleClient, this.props.wcif, this.props.mbld, this.props.password, this.props.translations ).then((blob) => { this.props.updateFileZipBlob(blob); - this.setGeneratingScrambles(false); + this.props.updateGeneratingScrambles(false); + this.props.resetScramblingProgressCurrent(); }); + this.props.updateGeneratingScrambles(true); }; downloadZip = () => { @@ -100,7 +110,7 @@ const Main = connect( }; scrambleButton = () => { - if (this.state.generatingScrambles) { + if (this.props.generatingScrambles) { return (