-
Notifications
You must be signed in to change notification settings - Fork 211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ugly glue for sharable kiosk code #6014
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is such a good change. I need to take advantage of interfaces and objects more often. Especially since we might be wanting to expand these options in the future. Thanks for doing this. |
||
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<void> { | ||
if (this.shareSrc) { | ||
const [id, ...filename] = this.shareSrc.split("/"); | ||
|
||
const kioskData = await getSharedKioskData(id, filename.join("/")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this just for leaving us the option to send the shared kiosk entries to a different file/nested file? If I'm understanding everything correctly, is the filename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this one it's emitting as kiosk.json every time yes; I wrote it like this just to keep it simple, but we could choose to omit the filename when sharing it, and default to kiosk.json when filename is empty. This mirrors our tutorial loading logic from shared projects / github projects -- defaulting to readme.md, but allowing a user to e.g. store multiple kiosks in one project if they wish to, just with a bit more specification involved on which file to read from. |
||
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,15 +87,14 @@ export class Kiosk { | |
if (name.toLowerCase() === "untitled") { | ||
return "Kiosk Game"; | ||
} | ||
|
||
return name; | ||
} | ||
|
||
getGameDescription(desc: string) { | ||
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); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is this how we create all share links across targets? We send a POST request to this endpoint and the payload and header of the request determine what target is used and more? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, here's the entry point for where those get spun up in webapp https://github.com/microsoft/pxt/blob/master/webapp/src/app.tsx#L4106 & the actual requests are made in these two functions depending on if it's anonymous or persistent: https://github.com/microsoft/pxt/blob/master/webapp/src/app.tsx#L4106 |
||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Asking for more understanding. Why are each of these files needed? Are these the files a share link is expected to serve so to speak? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
return files; | ||
} | ||
|
||
export async function getSharedKioskData(shareId: string, filename: string): Promise<SharedKioskData> { | ||
const resp = await fetch(`${apiRoot}/api/${shareId}/text`); | ||
const proj: any = await resp.json(); | ||
const kioskData = JSON.parse(proj[filename]); | ||
return kioskData; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. I'm guessing when kiosk starts using react-common we can just take advantage of the share dialog or a simplified version of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like that, I just wanted to implement the smallest amount of ui in this that i could while still having an mvp to keep it simple / only spent about 20 minutes implementing / testing it (same reason this just comes from a button you have to click right now which isn't shippable, instead of hooking it into the gamepad movement flow); for now could just pop to a page with a qr code of it and link saying it's shareable as well if we wanted to add another state.