diff --git a/kiosk/src/App.tsx b/kiosk/src/App.tsx index bc55dc96fb1..ec1cb4c7d70 100644 --- a/kiosk/src/App.tsx +++ b/kiosk/src/App.tsx @@ -14,8 +14,14 @@ const url = window.location.href; const clean = !!/clean(?:[:=])1/.test(url); const locked = !!/lock(?:[:=])1/i.test(url); const time = (/time=((?:[0-9]{1,3}))/i.exec(url))?.[1]; +const shareSrc = /shared[:=]([^#?]+)/i.exec(url)?.[1]; -const kioskSingleton: Kiosk = new Kiosk(clean, locked, time); +const kioskSingleton: Kiosk = new Kiosk({ + clean, + locked, + time, + shareSrc, +}); kioskSingleton.initialize().catch(error => alert(error)); function App() { diff --git a/kiosk/src/Components/MainMenu.tsx b/kiosk/src/Components/MainMenu.tsx index 6604a1a9201..00771577354 100644 --- a/kiosk/src/Components/MainMenu.tsx +++ b/kiosk/src/Components/MainMenu.tsx @@ -7,6 +7,7 @@ import HighScoresList from "./HighScoresList"; import { DeleteButton } from "./DeleteButton"; import { tickEvent } from "../browserUtils"; import DeletionModal from "./DeletionModal"; +import { createKioskShareLink } from "../share"; interface IProps { kiosk: Kiosk @@ -49,7 +50,7 @@ const MainMenu: React.FC = ({ kiosk }) => { updateLoop(); } }, configData.GamepadPollLoopMilli); - + return () => { if (intervalId) { clearInterval(intervalId); @@ -60,6 +61,17 @@ const MainMenu: React.FC = ({ kiosk }) => { } }); + const onUploadClick = async () => { + const sharePointer = await createKioskShareLink({ + games: kiosk.games, + }); + + const outputLink = `https://arcade.makecode.com/kiosk?shared=${sharePointer}`; + + alert(`send em to ${outputLink}`); + console.log(outputLink); + } + return(
} + { + !kiosk.locked && + } @@ -81,5 +96,5 @@ const MainMenu: React.FC = ({ kiosk }) => { ) } - + export default MainMenu; \ No newline at end of file diff --git a/kiosk/src/Models/Kiosk.ts b/kiosk/src/Models/Kiosk.ts index 410cef937c2..2696cfc0d16 100644 --- a/kiosk/src/Models/Kiosk.ts +++ b/kiosk/src/Models/Kiosk.ts @@ -6,6 +6,15 @@ import { KioskState } from "./KioskState"; import configData from "../config.json"; import { getGameDetailsAsync } from "../BackendRequests" import { tickEvent } from "../browserUtils"; +import { getSharedKioskData } from "../share"; + +export interface KioskOpts { + clean: boolean; + locked: boolean; + time?: string; + shareSrc?: string; +} + export class Kiosk { games: GameData[] = []; gamepadManager: GamepadManager = new GamepadManager(); @@ -19,6 +28,7 @@ export class Kiosk { clean: boolean; locked: boolean; time?: string; + shareSrc?: string; // `{shareId}/{filename}` private readonly highScoresLocalStorageKey: string = "HighScores"; private readonly addedGamesLocalStorageKey: string = "UserAddedGames"; @@ -30,27 +40,36 @@ export class Kiosk { private builtGamesCache: { [gameId: string]: BuiltSimJSInfo } = { }; private defaultGameDescription = "Made with love in MakeCode Arcade"; - constructor(clean: boolean, locked: boolean, time?: string) { + constructor(opts: KioskOpts) { + const { clean, locked, time, shareSrc } = opts; this.clean = clean; - this.locked = locked; + this.shareSrc = shareSrc; + // TODO figure out this interaction; right now treating loaded shareSrc as readonly to keep easy + this.locked = locked || !!shareSrc; this.time = time; } async downloadGameListAsync(): Promise { + if (this.shareSrc) { + const [id, ...filename] = this.shareSrc.split("/"); + + const kioskData = await getSharedKioskData(id, filename.join("/")); + this.games = kioskData.games; + return; + } if (!this.clean) { let url = configData.GameDataUrl; if (configData.Debug) { url = `/static/kiosk/${url}`; } - + const response = await fetch(url); if (!response.ok) { throw new Error(`Unable to download game list from "${url}"`); } - + try { this.games = (await response.json()).games; - this.games.push() } catch (error) { throw new Error(`Unable to process game list downloaded from "${url}": ${error}`); @@ -68,7 +87,6 @@ export class Kiosk { if (name.toLowerCase() === "untitled") { return "Kiosk Game"; } - return name; } @@ -76,7 +94,7 @@ export class Kiosk { if (desc.length === 0) { return this.defaultGameDescription } - + return desc; } @@ -88,7 +106,7 @@ export class Kiosk { if (!allAddedGames[gameId]) { let gameName; let gameDescription; - + try { const gameDetails = await getGameDetailsAsync(gameId); gameName = this.getGameName(gameDetails.name); @@ -97,10 +115,10 @@ export class Kiosk { gameName = "Kiosk Game"; gameDescription = this.defaultGameDescription; } - + const gameUploadDate = (new Date()).toLocaleString() const newGame = new GameData(gameId, gameName, gameDescription, "None", games[gameId].id, gameUploadDate, true); - + this.games.push(newGame); gamesToAdd.push(gameName); allAddedGames[gameId] = newGame; @@ -256,8 +274,8 @@ export class Kiosk { const launchedGameHighs = this.getHighScores(this.launchedGame); const currentHighScore = this.mostRecentScores[0]; const lastScore = launchedGameHighs[launchedGameHighs.length - 1]?.score; - if (launchedGameHighs.length === configData.HighScoresToKeep - && lastScore + if (launchedGameHighs.length === configData.HighScoresToKeep + && lastScore && currentHighScore < lastScore) { this.exitGame(KioskState.GameOver); diff --git a/kiosk/src/browserUtils.ts b/kiosk/src/browserUtils.ts index 31732d78c51..ff76739ad86 100644 --- a/kiosk/src/browserUtils.ts +++ b/kiosk/src/browserUtils.ts @@ -21,4 +21,5 @@ export function devicePixelRatio(): number { export function isLocal() { return window.location.hostname === "localhost"; -} \ No newline at end of file +} + diff --git a/kiosk/src/share.ts b/kiosk/src/share.ts new file mode 100644 index 00000000000..2fff541bb65 --- /dev/null +++ b/kiosk/src/share.ts @@ -0,0 +1,93 @@ +import { GameData } from "./Models/GameData"; + +const apiRoot = "https://www.makecode.com"; +const description = "A kiosk for MakeCode Arcade"; + +export interface SharedKioskData { + games: GameData[]; +} + +export async function createKioskShareLink(kioskData: SharedKioskData) { + const payload = createShareRequest( + "kiosk", + createProjectFiles("kiosk", kioskData) + ); + const url = apiRoot + "/api/scripts"; + + const result = await fetch( + url, + { + method: "POST", + body: new Blob([JSON.stringify(payload)], { type: "application/json" }) + } + ); + + if (result.status === 200) { + const resJSON = await result.json(); + // return "https://arcade.makecode.com/" + resJSON.shortid; + return `${resJSON.shortid}/kiosk.json`; + } + + return "ERROR" +} + + +function createShareRequest(projectName: string, files: {[index: string]: string}) { + const header = { + "name": projectName, + "meta": { + }, + "editor": "tsprj", + "pubId": undefined, + "pubCurrent": false, + "target": "arcade", + "id": crypto.randomUUID(), + "recentUse": Date.now(), + "modificationTime": Date.now(), + "path": projectName, + "saveId": {}, + "githubCurrent": false, + "pubVersions": [] + } + + return { + id: header.id, + name: projectName, + target: header.target, + description: description, + editor: "tsprj", + header: JSON.stringify(header), + text: JSON.stringify(files), + meta: { + } + } +} + +function createProjectFiles(projectName: string, kioskData: SharedKioskData) { + const files: {[index: string]: string} = {}; + + const config = { + "name": projectName, + "description": description, + "dependencies": { + "device": "*" + }, + "files": [ + "main.ts", + "kiosk.json" + ], + "preferredEditor": "tsprj" + }; + files["pxt.json"] = JSON.stringify(config, null, 4); + files["main.ts"] = " "; + files["kiosk.json"] = JSON.stringify(kioskData, undefined, 4); + + return files; +} + +export async function getSharedKioskData(shareId: string, filename: string): Promise { + const resp = await fetch(`${apiRoot}/api/${shareId}/text`); + const proj: any = await resp.json(); + const kioskData = JSON.parse(proj[filename]); + return kioskData; +} \ No newline at end of file