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.
ios
Norwin 6 years ago
parent 30437d24fe
commit 4412306ef6

@ -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,
}
}

@ -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…
Cancel
Save