mirror of
https://github.com/sensebox/blockly-app
synced 2025-02-18 17:23:57 +01:00
persist & restore last sketch
Leaving the blockly page saves the current state of the blocks via the postMessage protocol. This protocol is now properly encapsulated with a promise based API.
This commit is contained in:
parent
30437d24fe
commit
4412306ef6
6 changed files with 163 additions and 53 deletions
|
@ -320,24 +320,43 @@
|
|||
Ardublockly.init({ blocklyPath: './blockly' });
|
||||
SenseboxExtension.init();
|
||||
|
||||
window.addEventListener('message', function load(event) {
|
||||
switch (event.data) {
|
||||
/**
|
||||
* message handler for the postMessage protocol with the iframe parent (ionic BlocklyPage).
|
||||
*/
|
||||
window.addEventListener('message', function msgHandler (event) {
|
||||
switch (event.data.type) {
|
||||
case 'getSketch':
|
||||
var code = Ardublockly.generateArduino();
|
||||
event.source.postMessage({
|
||||
type: 'sketch',
|
||||
data: code,
|
||||
}, event.origin)
|
||||
event.source.postMessage({ type: 'sketch', data: code }, event.origin);
|
||||
break;
|
||||
|
||||
case 'getXml':
|
||||
var xml = Ardublockly.generateXml();
|
||||
event.source.postMessage({ type: 'xml', data: xml }, event.origin);
|
||||
break;
|
||||
|
||||
case 'setXml':
|
||||
var xml = Ardublockly.replaceBlocksfromXml(event.data.data);
|
||||
break;
|
||||
|
||||
case 'toggleView':
|
||||
document.getElementById('blocklyview').classList.toggle('hidden');
|
||||
document.querySelector('.blocklyToolboxDiv').classList.toggle('hidden');
|
||||
document.getElementById('codeview').classList.toggle('hidden');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`postMessage type ${event.data} not implemented`)
|
||||
event.source.postMessage({
|
||||
type: 'log',
|
||||
data: `postMessage type ${event.data.type} not implemented`,
|
||||
}, event.origin);
|
||||
}
|
||||
});
|
||||
|
||||
// notify parent that we are ready to receive commands.
|
||||
// @FIXME: this is not exactly correct, as the default sketch is still loading after this point!
|
||||
parent.postMessage({ type: 'ready' }, '*')
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
mini
|
||||
color="light"
|
||||
[title]="'BLOCKLY.BTN_CODE' | translate"
|
||||
(click)="toggleView();"
|
||||
(click)="blockly.toggleView()"
|
||||
>
|
||||
<ion-icon name="code"></ion-icon>
|
||||
</button>
|
||||
|
|
|
@ -1,73 +1,70 @@
|
|||
import { Component, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
||||
import { IonicPage, NavController, NavParams, Platform } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { OtaWizardPage } from '../ota-wizard/ota-wizard';
|
||||
import { LoggingProvider } from '../../providers/logging/logging';
|
||||
import { StorageProvider, LASTSKETCH } from '../../providers/storage/storage';
|
||||
import { BlocklyMessageProtocol } from './blockly_protocol';
|
||||
|
||||
@IonicPage()
|
||||
@Component({
|
||||
selector: 'page-blockly',
|
||||
templateUrl: 'blockly.html',
|
||||
})
|
||||
export class BlocklyPage {
|
||||
export class BlocklyPage implements OnInit {
|
||||
@ViewChild('blocklyFrame') blocklyFrame: ElementRef
|
||||
blocklyUrl: SafeResourceUrl
|
||||
blockly: BlocklyMessageProtocol
|
||||
|
||||
private messageHandler: (ev: IframePostMessageEvent) => void
|
||||
private log: LoggingProvider
|
||||
|
||||
constructor(
|
||||
public navCtrl: NavController,
|
||||
public navParams: NavParams,
|
||||
private sanitizer: DomSanitizer,
|
||||
private platform: Platform,
|
||||
private storage: StorageProvider,
|
||||
logger: LoggingProvider,
|
||||
translate: TranslateService,
|
||||
) {
|
||||
this.log = logger.createChild('BlocklyPage')
|
||||
this.blocklyUrl = this.buildBlocklyUrl(translate.currentLang)
|
||||
|
||||
// need to assign it here to keep the function reference for unsubscribing again
|
||||
// and to maintain the this scope properly
|
||||
this.messageHandler = (ev: IframePostMessageEvent) => {
|
||||
const { type, data } = ev.data;
|
||||
switch (type) {
|
||||
case 'sketch':
|
||||
this.log.debug('sketch received, launching ota wizard page', { sketch: data })
|
||||
this.navCtrl.push(OtaWizardPage, { sketch: data })
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', this.messageHandler)
|
||||
async ngOnInit () {
|
||||
// blocklyFrame is available from here on
|
||||
this.blockly = new BlocklyMessageProtocol(this.blocklyFrame, this.log)
|
||||
await this.blockly.ready
|
||||
// load the last sketch
|
||||
const xml = this.storage.get(LASTSKETCH)
|
||||
if (xml) this.blockly.setXml(xml)
|
||||
|
||||
// save state when exiting (on mobile & browsers separately)
|
||||
this.platform.pause.subscribe(() => this.saveBlocklyState())
|
||||
window.addEventListener('beforeunload', () => this.saveBlocklyState())
|
||||
}
|
||||
|
||||
ionViewWillUnload () {
|
||||
window.removeEventListener('message', this.messageHandler)
|
||||
async ionViewCanLeave () {
|
||||
// hold closing the page until we saved the current blockly state
|
||||
await this.saveBlocklyState()
|
||||
return true
|
||||
}
|
||||
|
||||
launchOtaWizard () {
|
||||
this.log.debug('clicked launch ota')
|
||||
this.blocklyFrame.nativeElement.contentWindow.postMessage('getSketch', '*')
|
||||
async saveBlocklyState () {
|
||||
const xml = await this.blockly.getXml()
|
||||
this.storage.set(LASTSKETCH, xml)
|
||||
}
|
||||
|
||||
toggleView () {
|
||||
this.log.debug('clicked toggle view')
|
||||
this.blocklyFrame.nativeElement.contentWindow.postMessage('toggleView', '*')
|
||||
async launchOtaWizard () {
|
||||
const sketch = await this.blockly.getSketch()
|
||||
this.navCtrl.push(OtaWizardPage, { sketch })
|
||||
}
|
||||
|
||||
buildBlocklyUrl (lang: string): SafeResourceUrl {
|
||||
private buildBlocklyUrl (lang: string): SafeResourceUrl {
|
||||
if (!lang) this.log.error('building url with empty language!')
|
||||
const url = `./assets/blockly.html?lang=${lang}`
|
||||
return this.sanitizer.bypassSecurityTrustResourceUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
interface IframePostMessageEvent extends MessageEvent {
|
||||
data: {
|
||||
type: 'sketch',
|
||||
data: any,
|
||||
}
|
||||
}
|
||||
|
|
97
src/pages/blockly/blockly_protocol.ts
Normal file
97
src/pages/blockly/blockly_protocol.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { ElementRef } from '@angular/core';
|
||||
import { LoggingProvider } from '../../providers/logging/logging';
|
||||
|
||||
/**
|
||||
* this file defines & implements the message passing protocol to communicate
|
||||
* with an iframe that has src/assets/blockly.html loaded.
|
||||
* The underlying postMessage protocol is wrapped into a promise API.
|
||||
*/
|
||||
|
||||
export class BlocklyMessageProtocol {
|
||||
ready: Promise<void>
|
||||
|
||||
constructor (private blocklyFrame: ElementRef, private log: LoggingProvider) {
|
||||
window.addEventListener('message', (ev: IframePostMessageEvent) => {
|
||||
if (ev.data.type === 'log')
|
||||
this.log.warn('log entry from blockly:', ev.data)
|
||||
else
|
||||
this.log.debug(`received ${ev.data.type} message from blockly`, { message: ev.data })
|
||||
})
|
||||
|
||||
this.ready = new Promise(resolve => {
|
||||
window.addEventListener('message', (ev: IframePostMessageEvent) => {
|
||||
// @HACK @FIXME: timeout is required, as blockly resolves some async functions
|
||||
// after firing the `ready` event..
|
||||
if (ev.data.type === 'ready') setTimeout(resolve, 300)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private reqResPatterns: BlocklyReqPatterns = {
|
||||
'getSketch': 'sketch',
|
||||
'getXml': 'xml',
|
||||
'setXml': null,
|
||||
'toggleView': null,
|
||||
}
|
||||
|
||||
toggleView() { this.sendRequest({ type: 'toggleView' }) }
|
||||
setXml(data: string) { this.sendRequest({ type: 'setXml', data }) }
|
||||
|
||||
getXml() { return this.sendRequest({ type: 'getXml' }) }
|
||||
getSketch() { return this.sendRequest({ type: 'getSketch' }) }
|
||||
|
||||
private async sendRequest(req: BlocklyRequest): Promise<any> {
|
||||
await this.ready
|
||||
|
||||
if (
|
||||
!this.blocklyFrame ||
|
||||
!this.blocklyFrame.nativeElement &&
|
||||
!this.blocklyFrame.nativeElement.contentWindow
|
||||
) {
|
||||
throw Error('cannot access blockly frame')
|
||||
}
|
||||
|
||||
const expectResponse = this.reqResPatterns[req.type]
|
||||
this.log.debug(`sending ${req.type} message to blockly, expecting response: ${expectResponse}`, { message: req })
|
||||
|
||||
if (!expectResponse)
|
||||
return this.blocklyFrame.nativeElement.contentWindow.postMessage(req, '*')
|
||||
|
||||
// create promise waiting for the response event
|
||||
const resPromise = new Promise<any>((resolve, reject) => {
|
||||
const resHandler = ({ data: res }: IframePostMessageEvent) => {
|
||||
if (expectResponse !== res.type) return
|
||||
window.removeEventListener('message', resHandler)
|
||||
if (expectResponse === 'error') reject(res.data)
|
||||
else resolve(res.data)
|
||||
}
|
||||
// TODO: promise reject after timeout?
|
||||
window.addEventListener('message', resHandler)
|
||||
})
|
||||
|
||||
// send message after registering the subscribe handler!
|
||||
this.blocklyFrame.nativeElement.contentWindow.postMessage(req, '*')
|
||||
return resPromise
|
||||
}
|
||||
}
|
||||
|
||||
interface IframePostMessageEvent extends MessageEvent {
|
||||
data: BlocklyResponse
|
||||
}
|
||||
|
||||
type BlocklyReqPatterns = {
|
||||
[k in BlocklyRequest['type']]: BlocklyResponse['type']
|
||||
}
|
||||
|
||||
type BlocklyRequest =
|
||||
{ type: 'getSketch' } |
|
||||
{ type: 'getXml' } |
|
||||
{ type: 'setXml', data: string } |
|
||||
{ type: 'toggleView' }
|
||||
|
||||
type BlocklyResponse =
|
||||
{ type: 'log', data: any } |
|
||||
{ type: 'error', data: any } |
|
||||
{ type: 'ready', data: undefined } |
|
||||
{ type: 'sketch', data: string } |
|
||||
{ type: 'xml', data: string }
|
|
@ -8,7 +8,7 @@
|
|||
</ion-header>
|
||||
|
||||
<ion-content padding>
|
||||
<ion-list inset>
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-icon name="megaphone" item-start></ion-icon>
|
||||
<ion-label>
|
||||
|
|
|
@ -1,30 +1,27 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
export const SETTINGS = 'appsettings'
|
||||
export const LASTSKETCH = 'lastsketch'
|
||||
|
||||
@Injectable()
|
||||
export class StorageProvider {
|
||||
private cache: Map<string, any> = new Map()
|
||||
|
||||
constructor () {
|
||||
this.registerKey(SETTINGS, {
|
||||
logOptin: false,
|
||||
})
|
||||
this.registerKey(SETTINGS, { logOptin: false })
|
||||
this.registerKey(LASTSKETCH, '')
|
||||
}
|
||||
|
||||
registerKey (key, defaultValue) {
|
||||
const stored = localStorage.getItem(key)
|
||||
if (!stored) {
|
||||
localStorage.setItem(key, JSON.stringify(defaultValue))
|
||||
this.cache[key] = defaultValue
|
||||
if (stored === null) {
|
||||
this.set(key, defaultValue)
|
||||
} else {
|
||||
this.cache[key] = JSON.parse(stored)
|
||||
}
|
||||
}
|
||||
|
||||
get (key) {
|
||||
return this.cache[key]
|
||||
}
|
||||
get (key) { return this.cache[key] }
|
||||
|
||||
set (key, value) {
|
||||
localStorage.setItem(key, JSON.stringify(value))
|
||||
|
|
Loading…
Add table
Reference in a new issue