diff --git a/examples/farmyard/index.html b/examples/farmyard/index.html index f890c22..c3c48d4 100644 --- a/examples/farmyard/index.html +++ b/examples/farmyard/index.html @@ -8,13 +8,13 @@ - - + + - + diff --git a/src/invent/channels.py b/src/invent/channels.py index c320dc3..e97217e 100644 --- a/src/invent/channels.py +++ b/src/invent/channels.py @@ -18,11 +18,11 @@ limitations under the License. """ -from .task import Task -from .compatability import is_micropython, iscoroutinefunction -if not is_micropython: - import asyncio +import asyncio + +from .task import Task +from .compatability import iscoroutinefunction __all__ = [ diff --git a/src/invent/compatability.py b/src/invent/compatability.py index 5a2a61a..1628c47 100644 --- a/src/invent/compatability.py +++ b/src/invent/compatability.py @@ -2,7 +2,6 @@ import inspect import sys -import types #: A flag to show if MicroPython is the current Python interpreter. @@ -25,6 +24,6 @@ def iscoroutinefunction(obj): if is_micropython: # pragma: no cover # MicroPython seems to treat coroutines as generators :) - return type(obj) is types.GeneratorType + return inspect.isgeneratorfunction(obj) return inspect.iscoroutinefunction(obj) diff --git a/src/invent/tasks/fetch.py b/src/invent/tasks/fetch.py index ab131ba..df0f500 100644 --- a/src/invent/tasks/fetch.py +++ b/src/invent/tasks/fetch.py @@ -1,6 +1,5 @@ import invent import pyscript -import asyncio async def fetch(task, url, json=True): @@ -11,7 +10,7 @@ async def fetch(task, url, json=True): """ if task.indicator: invent.datastore[task.indicator] = True - await asyncio.sleep(4) + response = await pyscript.fetch(url) if task.indicator: invent.datastore[task.indicator] = False diff --git a/src/invent/ui/export.py b/src/invent/ui/export.py index c5d0f4c..715fdc8 100644 --- a/src/invent/ui/export.py +++ b/src/invent/ui/export.py @@ -30,16 +30,16 @@ + href="https://pyscript.net/releases/2024.5.1/core.css"> + src="https://pyscript.net/releases/2024.5.1/core.js"> - + """ @@ -68,51 +68,10 @@ PYSCRIPT_TOML_TEMPLATE = """ -packages = [ "pyodide_http", "requests" ] experimental_create_proxy = "auto" + [files] -# -# invent -# -"{{INVENT}}" = "{invent_src}" -"{{INVENT_TO}}" = "./invent" -"{{INVENT}}/__init__.py"="{{INVENT_TO}}/__init__.py" -"{{INVENT}}/__about__.py"="{{INVENT_TO}}/__about__.py" -"{{INVENT}}/ai.py"="{{INVENT_TO}}/ai.py" -"{{INVENT}}/channels.py"="{{INVENT_TO}}/channels.py" -"{{INVENT}}/compatability.py"="{{INVENT_TO}}/compatability.py" -"{{INVENT}}/datastore.py"="{{INVENT_TO}}/datastore.py" -"{{INVENT}}/i18n.py"="{{INVENT_TO}}/i18n.py" -"{{INVENT}}/media.py"="{{INVENT_TO}}/media.py" -"{{INVENT}}/speech.py"="{{INVENT_TO}}/speech.py" -"{{INVENT}}/utils.py"="{{INVENT_TO}}/utils.py" -# -# invent.ui -# -"{{INVENT}}/ui/__init__.py"="{{INVENT_TO}}/ui/__init__.py" -"{{INVENT}}/ui/app.py"="{{INVENT_TO}}/ui/app.py" -"{{INVENT}}/ui/containers/__init__.py"="{{INVENT_TO}}/ui/containers/__init__.py" -"{{INVENT}}/ui/containers/column.py"="{{INVENT_TO}}/ui/containers/column.py" -"{{INVENT}}/ui/containers/grid.py"="{{INVENT_TO}}/ui/containers/grid.py" -"{{INVENT}}/ui/containers/page.py"="{{INVENT_TO}}/ui/containers/page.py" -"{{INVENT}}/ui/containers/row.py"="{{INVENT_TO}}/ui/containers/row.py" -"{{INVENT}}/ui/core/__init__.py"="{{INVENT_TO}}/ui/core/__init__.py" -"{{INVENT}}/ui/core/component.py"="{{INVENT_TO}}/ui/core/component.py" -"{{INVENT}}/ui/core/property.py"="{{INVENT_TO}}/ui/core/property.py" -"{{INVENT}}/ui/export.py"="{{INVENT_TO}}/ui/export.py" -"{{INVENT}}/ui/utils.py"="{{INVENT_TO}}/ui/utils.py" -"{{INVENT}}/ui/widgets/__init__.py"="{{INVENT_TO}}/ui/widgets/__init__.py" -"{{INVENT}}/ui/widgets/audio.py"="{{INVENT_TO}}/ui/widgets/audio.py" -"{{INVENT}}/ui/widgets/button.py"="{{INVENT_TO}}/ui/widgets/button.py" -"{{INVENT}}/ui/widgets/checkbox.py"="{{INVENT_TO}}/ui/widgets/checkbox.py" -"{{INVENT}}/ui/widgets/code.py"="{{INVENT_TO}}/ui/widgets/code.py" -"{{INVENT}}/ui/widgets/html.py"="{{INVENT_TO}}/ui/widgets/html.py" -"{{INVENT}}/ui/widgets/image.py"="{{INVENT_TO}}/ui/widgets/image.py" -"{{INVENT}}/ui/widgets/slider.py"="{{INVENT_TO}}/ui/widgets/slider.py" -"{{INVENT}}/ui/widgets/switch.py"="{{INVENT_TO}}/ui/widgets/switch.py" -"{{INVENT}}/ui/widgets/textbox.py"="{{INVENT_TO}}/ui/widgets/textbox.py" -"{{INVENT}}/ui/widgets/textinput.py"="{{INVENT_TO}}/ui/widgets/textinput.py" -"{{INVENT}}/ui/widgets/fileupload.py"="{{INVENT_TO}}/ui/widgets/fileupload.py" +"https://invent.pyscriptapps-dev.com/invent/latest/invent.zip" = "./*" """ diff --git a/src/invent/ui/utils.py b/src/invent/ui/utils.py index 4f5bd8d..02c8a3a 100644 --- a/src/invent/ui/utils.py +++ b/src/invent/ui/utils.py @@ -21,7 +21,7 @@ import random from pyscript import document -from invent.compatability import is_micropython +from pyscript.ffi import create_proxy __all__ = [ @@ -61,8 +61,5 @@ def sanitize(raw): def proxy(function): if not function: return None - import pyodide - return ( - pyodide.ffi.create_proxy(function) if not is_micropython else function - ) + return create_proxy(function) diff --git a/src/pyscript.toml b/src/pyscript.toml deleted file mode 100644 index 0a1e5cb..0000000 --- a/src/pyscript.toml +++ /dev/null @@ -1,21 +0,0 @@ -[files] -"{INVENT}" = "https://mchilvers.pyscriptapps.com/invent/latest/invent/" -"{INVENT_TO}" = "./invent" -"{INVENT}/__init__.py"="{INVENT_TO}/__init__.py" -"{INVENT}/__about__.py"="{INVENT_TO}/__about__.py" -"{INVENT}/channels.py"="{INVENT_TO}/channels.py" -"{INVENT}/datastore.py"="{INVENT_TO}/datastore.py" -"{INVENT}/i18n.py"="{INVENT_TO}/i18n.py" -"{INVENT}/media.py"="{INVENT_TO}/media.py" -"{INVENT}/utils.py"="{INVENT_TO}/utils.py" -"{INVENT}/ui/__init__.py"="{INVENT_TO}/ui/__init__.py" -"{INVENT}/ui/app.py"="{INVENT_TO}/ui/app.py" -"{INVENT}/ui/page.py"="{INVENT_TO}/ui/page.py" -"{INVENT}/ui/core.py"="{INVENT_TO}/ui/core.py" -"{INVENT}/ui/utils.py"="{INVENT_TO}/ui/utils.py" -"{INVENT}/ui/widgets/__init__.py"="{INVENT_TO}/ui/widgets/__init__.py" -"{INVENT}/ui/widgets/button.py"="{INVENT_TO}/ui/widgets/button.py" -"{INVENT}/ui/widgets/image.py"="{INVENT_TO}/ui/widgets/image.py" -"{INVENT}/ui/widgets/textbox.py"="{INVENT_TO}/ui/widgets/textbox.py" -"{INVENT}/ui/widgets/textinput.py"="{INVENT_TO}/ui/widgets/textinput.py" -"{INVENT}/ui/widgets/fileupload.py"="{INVENT_TO}/ui/widgets/fileupload.py" diff --git a/src/tools/builder/index.html b/src/tools/builder/index.html index 43553ec..e7327a5 100644 --- a/src/tools/builder/index.html +++ b/src/tools/builder/index.html @@ -6,11 +6,11 @@ Invent - +
- + diff --git a/src/tools/builder/src/blocks/media/definitions.ts b/src/tools/builder/src/blocks/media/definitions.ts index 7882655..546492e 100644 --- a/src/tools/builder/src/blocks/media/definitions.ts +++ b/src/tools/builder/src/blocks/media/definitions.ts @@ -6,10 +6,9 @@ function getSoundFiles(): any { const audioFiles: Array = Object.values(builder.state.media).filter((file: MediaFileModel) => { return file.type.startsWith("audio") }); - if (audioFiles.length > 0){ return audioFiles.map((file: MediaFileModel) => { - return [file.name, file.path]; + return [file.name, file.name]; }); } else { diff --git a/src/tools/builder/src/blocks/media/generators.ts b/src/tools/builder/src/blocks/media/generators.ts index 9df116f..1610d78 100644 --- a/src/tools/builder/src/blocks/media/generators.ts +++ b/src/tools/builder/src/blocks/media/generators.ts @@ -9,7 +9,7 @@ pythonGenerator.forBlock['sound_files'] = function(block: Blockly.Block) { pythonGenerator.forBlock['play_sound'] = function(block: Blockly.Block, generator: Blockly.Generator) { const file: string = generator.valueToCode(block, 'file', 0); - const code = `invent.play_sound("${file}")\n`; + const code = `invent.play_sound(invent.media.sounds.${file})\n`; return code; }; diff --git a/src/tools/builder/src/data/models/media-file-model.ts b/src/tools/builder/src/data/models/media-file-model.ts index 20f3048..ebbf5e5 100644 --- a/src/tools/builder/src/data/models/media-file-model.ts +++ b/src/tools/builder/src/data/models/media-file-model.ts @@ -1,6 +1,5 @@ export interface MediaFileModel { name: string; - file: Blob; type: string; path: string; } \ No newline at end of file diff --git a/src/tools/builder/src/main.ts b/src/tools/builder/src/main.ts index abcc09a..881e67d 100644 --- a/src/tools/builder/src/main.ts +++ b/src/tools/builder/src/main.ts @@ -9,9 +9,9 @@ import InventWidgets from "@/views/builder/components/page-editor/widgets"; import "@/data/providers/icon-provider"; // @ts-ignore -import { whenDefined } from "https://pyscript.net/releases/2024.4.1/core.js"; +import { whenDefined } from "https://pyscript.net/releases/2024.5.1/core.js"; -whenDefined("py").then(() => { +whenDefined("mpy").then(() => { LocalizationUtilities.loadPreferredLanguageAsync().then(() => { createApp(App) .use(Components) diff --git a/src/tools/builder/src/views/builder/builder-model.ts b/src/tools/builder/src/views/builder/builder-model.ts index cd915c4..a20c714 100644 --- a/src/tools/builder/src/views/builder/builder-model.ts +++ b/src/tools/builder/src/views/builder/builder-model.ts @@ -7,11 +7,9 @@ import type { WidgetModel } from "@/data/models/widget-model"; import type { PageModel } from "@/data/models/page-model"; import * as Blockly from 'blockly/core'; import { pythonGenerator } from 'blockly/python'; -import confetti from "canvas-confetti"; import type { DatastoreValueModel } from "@/data/models/datastore-value-model"; import type { MediaFileModel } from "@/data/models/media-file-model"; import type { IbSelectOption } from "@/components/ib-select/ib-select-types"; -import { CommonUtilities } from "@/utilities/common-utilities"; /** @@ -25,35 +23,35 @@ export class BuilderModel extends ViewModelBase { return "builder"; } - private apiKey = "psdc_gAAAAABl5zj-GCPoqFRv62GrpO8ud1mZhz_bbMTXSwwd1WR0ayuaiFLl15WnafvFiKMQEULC1YdSLOu4P8PEr5Cj8WPTJq2w0bgZsusOIur9UKf17tIsSlRHrDDEWLpHD1GSooHYvLNyLDFfoGDPEd50pfdoKDy8F7K3plvTjQfEC5lGnNjKt53uKlrwrEFJmLiGiV9-U4TD_uNUOAwnnIHOxMtZ0UI-MQ=="; - private username = "joshualowe1002"; - private projectSlug = CommonUtilities.getRandomId("invent", "").toLowerCase(); - /** * Reactive instance of the view state. */ public state: BuilderState = reactive(new BuilderState()); public async init(): Promise { - await this.setupProject(); + /** + * Wait for the "builder" object to be available in the global scope. + */ + // @ts-ignore + while (!window['builder']){ + await new Promise(r => setTimeout(r, 10)); + } + this.getPages(); this.setDefaultPage(); this.getAvailableComponents(); /* - * BuilderUtilities is really just a bridge between this class (the JS-side of - * the view model) and the "Builder" class in "builder.py" (the Python-side of - * the view model). - */ + * BuilderUtilities is really just a bridge between this class (the JS-side of + * the view model) and the "Builder" class in "builder.py" (the Python-side of + * the view model). + */ BuilderUtilities.init(this); this.listenForIframeMessages(); - } - private async setupProject(): Promise { - this.state.project = await this.getProject(); - if (this.state.project === null) { - this.state.project = await this.createProject("Invent Demo", "app"); - } + window.parent.postMessage({ + type: "invent-ready" + }, location.origin); } // Pages /////////////////////////////////////////////////////////////////////////// @@ -73,20 +71,35 @@ export class BuilderModel extends ViewModelBase { } public listenForIframeMessages(): void { - window.addEventListener("message", (event: MessageEvent) => { - console.log(event.data); + window.addEventListener("message", async (event: MessageEvent) => { + // Only allow same origin messages. + if (event.origin !== location.origin) return; + + const { type, data } = event.data; + + console.log(`Invent - received message type ${type}:`, data); - switch (event.data.type){ - case "save": { - window.postMessage({ - type: "save", + switch (type){ + case "save-request": { + event.source?.postMessage({ + type: "save-response", data: this.save(), - }, "*"); - + }); + break; } - case "load": { - this.load(event.data.data); + + case "load-request": { + await this.load(data); + break; + } + + case "media-upload-complete": { + this.state.media[data.name] = { + name: data.name, + type: data.type, + path: data.path + } break; } } @@ -154,11 +167,11 @@ export class BuilderModel extends ViewModelBase { ); } - public getSidebarTabColor(key: string): string { + public getSidebarTabColor(key: string): string { return this.state.activeSidebarTab === key ? 'gray' : 'transparent'; } - public getPageButtonColor(page: PageModel): string { + public getPageButtonColor(page: PageModel): string { return this.state.activePage && this.state.activeBuilderTab === 'app' && this.state.activePage.properties.id === page.properties.id ? 'gray' : 'transparent'; } @@ -204,64 +217,41 @@ export class BuilderModel extends ViewModelBase { return datastoreCode.join("\n"); } - public async getPythonCode(): Promise { - this.state.isPublishing = true; - - const datastore: string = this.getDatastoreValues(); - const generatedCode: string = pythonGenerator.workspaceToCode(Blockly.getMainWorkspace()); - const code: string = `${this.state.functions}\n${generatedCode}`; - - const result: any = BuilderUtilities.exportAsPyScriptApp(datastore, code); - const indexHtml: string = result["index.html"]; - const mainPy: string = result["main.py"]; - const pyscriptToml: string = result["pyscript.toml"]; - - console.log(indexHtml); - console.log(mainPy); - console.log(pyscriptToml); - - await Promise.all([ - this.uploadFile(this.createFormDataBlob('index.html', indexHtml, 'text/html')), - this.uploadFile(this.createFormDataBlob('pyscript.toml', pyscriptToml, 'application/toml')), - this.uploadFile(this.createFormDataBlob('main.py', mainPy, 'application/x-python-code')), - ]).then(() => { - confetti({ - particleCount: 100, - spread: 200 - }); - - ModalUtilities.showModal({ - modal: "AppPublished", - options: { - url: this.state.project.latest.url - } - }); - - this.state.isPublishing = false; - }); - } - public async load(data: any): Promise { // Load App BuilderUtilities.getAppFromDict(data.app); this.state.pages = []; nextTick(() => { - this.init(); + // this.init(); + this.getPages(); + this.setDefaultPage(); + this.getAvailableComponents(); }); + // Load media (this MUST be done before loading the blocks so that it can + // reference the media files in blocks such as playing sounds etc.). + this.state.media = data.media; + // Load Blocks Blockly.serialization.workspaces.load(data.blocks, Blockly.getMainWorkspace()); // Load Datastore this.state.datastore = data.datastore; + } public save(): any { + const datastore: string = this.getDatastoreValues(); + const generatedCode: string = pythonGenerator.workspaceToCode(Blockly.getMainWorkspace()); + const code: string = `${this.state.functions}\n${generatedCode}`; + const psdc: any = BuilderUtilities.exportAsPyScriptApp(datastore, code); + return { app: JSON.stringify(BuilderUtilities.getAppAsDict()), - blocks: Blockly.serialization.workspaces.save(Blockly.getMainWorkspace()), + blocks: JSON.stringify(Blockly.serialization.workspaces.save(Blockly.getMainWorkspace())), datastore: JSON.stringify(this.state.datastore), - } + psdc + }; } /** @@ -281,88 +271,6 @@ export class BuilderModel extends ViewModelBase { return formData; } - public async getProject() { - const response = await fetch( - `/api/projects/${this.username}/${this.projectSlug}`, - { - method: "GET", - headers: { - "Authorization": `Bearer ${this.apiKey}`, - } - } - ); - if (response.status === 404) { - return null; - } - - const result = await response.json(); - if (response.status != 200) { - throw result; - } - - return result; - } - - public async createProject(description: string, type: string) { - const response = await fetch( - `/api/projects/`, - { - method: "POST", - headers: { - "Authorization": `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: this.projectSlug, description, type - }) - } - ); - - const result = await response.json(); - if (response.status != 200) { - throw result; - } - - return result; - } - - public async uploadFile(formData: FormData): Promise { - const endpoint = `/api/projects/${this.state.project.id}/files?overwrite=True`; - const response = await fetch(endpoint, { - method: 'POST', - body: formData, - headers: { - "Authorization": `Bearer ${this.apiKey}`, - }, - }); - - const result = await response.json(); - if (response.status != 200) { - throw result; - } - - return result; - } - - public async uploadMediaFile(file: File): Promise { - let mediaFolder: string = ""; - - if (file.type.startsWith('image')){ - mediaFolder = "images"; - } - else if (file.type.startsWith('audio')){ - mediaFolder = "sounds"; - } - else if (file.type.startsWith('video')){ - mediaFolder = "videos"; - } - - const path: string = `media/${mediaFolder}/${file.name}`; - const formData: FormData = this.createFormDataFromBlob(path, file); - this.uploadFile(formData); - return `https://${this.username}.pyscriptapps.com/${this.projectSlug}/latest/${path}`; - } - public onBuilderTabClicked(tab: string) { this.state.activeBuilderTab = tab; @@ -376,7 +284,7 @@ export class BuilderModel extends ViewModelBase { } } - public getBuilderTabColor(key: string): string { + public getBuilderTabColor(key: string): string { return this.state.activeBuilderTab === key ? 'gray' : 'transparent'; } @@ -395,39 +303,9 @@ export class BuilderModel extends ViewModelBase { } public onAddMediaFile(): void { - // Create file input in order to open file chooser. - const fileInput: HTMLInputElement = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = ".png, .jpg, .gif, .jpeg, .mp3, .wav, .mp4, .mov"; - - // Runs when the user chooses a file. - fileInput.addEventListener("change", (event: Event) => { - const target: HTMLInputElement = event.target as HTMLInputElement; - const reader: FileReader = new FileReader(); - - reader.addEventListener("load", async (event: ProgressEvent) => { - // Parse JSON file and then open the editor with its contents. - if (event.target && target.files) { - const file: File = target.files[0]; - const path: string = await this.uploadMediaFile(file); - const fileName: string = file.name; - this.state.media[fileName] = { - name: fileName, - type: file.type, - file, - path - } - } - }); - - // Trigger reading the uploaded files. - if (target.files) { - reader.readAsDataURL(target.files[0]); - } + window.parent.postMessage({ + type: "add-media-request", }); - - // Click on the file input to open a file chooser. - fileInput.click(); } public getImageFiles(): Array {