diff --git a/apps/nostt/ChangeLog b/apps/nostt/ChangeLog new file mode 100644 index 000000000..5314d96b9 --- /dev/null +++ b/apps/nostt/ChangeLog @@ -0,0 +1 @@ +1.00: NOS Teletekst finished! \ No newline at end of file diff --git a/apps/nostt/README.md b/apps/nostt/README.md new file mode 100644 index 000000000..f9b57cc66 --- /dev/null +++ b/apps/nostt/README.md @@ -0,0 +1,12 @@ +# NOS Teletekst + + +Dutch Teletekst using the NOS Teletekst api. Requires http access via BangleJS GadgetBridge. See https://www.espruino.com/Gadgetbridge. Make sure `Allow Internet Access` is enabled. + +## Usage + +Tap once to bring up a numpad to enter the desired page. You can also swipe left/right to change the page, or swipe up/down to walk through the subpages. + +## Creator + +[Albert van der Meer](https://github.com/avandermeer) \ No newline at end of file diff --git a/apps/nostt/metadata.json b/apps/nostt/metadata.json new file mode 100644 index 000000000..b4f2cc068 --- /dev/null +++ b/apps/nostt/metadata.json @@ -0,0 +1,17 @@ +{ + "id":"nostt", + "name":"NOS Teletekst", + "shortName": "Teletekst", + "version": "1.00", + "description": "Dutch Teletekst using the NOS Teletekst api. Requires http access via BangleJS GadgetBridge.", + "type": "app", + "storage": [ + {"name":"nostt.app.js","url":"nostt.app.js"}, + {"name":"nostt.img","url":"nostt.icon.js","evaluate":true} + ], + "readme": "README.md", + "icon":"nostt_logo.png", + "supports": ["BANGLEJS2"], + "screenshots": [{"url": "nostt_screenshot_1.png"}, {"url": "nostt_screenshot_2.png"}], + "tags": "nos,teletext,teletekst,news,weather" +} \ No newline at end of file diff --git a/apps/nostt/nostt.app.js b/apps/nostt/nostt.app.js new file mode 100644 index 000000000..5eefa928f --- /dev/null +++ b/apps/nostt/nostt.app.js @@ -0,0 +1,505 @@ +class View { + + constructor() { + + this.navigationState = { + prevPage: { + p: undefined, + s: undefined, + }, + prevSubPage: { + p: undefined, + s: undefined, + }, + nextPage: { + p: undefined, + s: undefined, + }, + nextSubPage: { + p: undefined, + s: undefined, + }, + currentPage: { + p: undefined, + s: undefined, + }, + }; + + this.colorArray = { + 0: [0, 0, 0], + 1: [1, 0, 0], + 2: [0, 1, 0], + 3: [1, 1, 0], + 4: [0, 0, 1], + 5: [1, 0, 1], + 6: [0, 1, 1], + 7: [1, 1, 1], + 16: [0, 0, 0], + 17: [1, 0, 0], + 18: [0, 1, 0], + 19: [1, 1, 0], + 20: [0, 0, 1], + 21: [1, 0, 1], + 22: [0, 1, 1], + 23: [1, 1, 1], + }; + + + } + + start() { + g.clear(); + if (this.nextStartPage) { + this.show(this.nextStartPage); + this.nextStartPage = undefined; + } + else { + if (this.navigationState.currentPage.p) { + this.show(this.navigationState.currentPage.p); + } + else { + this.show(101); //load default + } + } + + } + + split_at_fourty(res, value) { + res.push(value.substring(0, 40)); + if (value.length > 40) { // at least two rows + return this.split_at_fourty(res, value.substring(40)); + } + else { + return res; + } + + } + +// strToUtf8Bytes(str) { +// const utf8 = []; +// for (let ii = 0; ii < str.length; ii++) { +// let charCode = str.charCodeAt(ii); +// if (charCode < 0x80) utf8.push(charCode); +// else if (charCode < 0x800) { +// utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f)); +// } else if (charCode < 0xd800 || charCode >= 0xe000) { +// utf8.push(0xe0 | (charCode >> 12), 0x80 | ((charCode >> 6) & 0x3f), 0x80 | (charCode & 0x3f)); +// } else { +// ii++; +// // Surrogate pair: +// // UTF-16 encodes 0x10000-0x10FFFF by subtracting 0x10000 and +// // splitting the 20 bits of 0x0-0xFFFFF into two halves +// charCode = 0x10000 + (((charCode & 0x3ff) << 10) | (str.charCodeAt(ii) & 0x3ff)); +// utf8.push( +// 0xf0 | (charCode >> 18), +// 0x80 | ((charCode >> 12) & 0x3f), +// 0x80 | ((charCode >> 6) & 0x3f), +// 0x80 | (charCode & 0x3f), +// ); +// } +// } +// return utf8; +// } + + loadPrevPage() { + if (this.navigationState.prevPage.p) { + this.show(this.navigationState.prevPage.p, this.navigationState.prevPage.s); + } + } + + loadNextPage() { + if (this.navigationState.nextPage.p) { + this.show(this.navigationState.nextPage.p, this.navigationState.nextPage.s); + } + } + + loadPrevSubPage() { + if (this.navigationState.prevSubPage.p) { + this.show(this.navigationState.prevSubPage.p, this.navigationState.prevSubPage.s); + } + } + + loadNextSubPage() { + if (this.navigationState.nextSubPage.p) { + this.show(this.navigationState.nextSubPage.p, this.navigationState.nextSubPage.s); + } + } + + handleSwipe(lr, ud){ + if (lr == -1 && ud == 0) { + this.loadNextPage(); + } + if (lr == 1 && ud == 0) { + this.loadPrevPage(); + } + if (lr == 0 && ud == 1) { + this.loadPrevSubPage(); + } + if (lr == 0 && ud == -1) { + this.loadNextSubPage(); + } + } + + show(pageId, subPageId) { + if(!subPageId){ + subPageId = 1; + } + + if (Bangle.http) { + Bangle.http('https://teletekst-data.nos.nl/page/' + pageId + '-' + subPageId).then((data) => { + + const res = data.resp; + g.clear(); + + + this.navigationState = { + prevPage: { + p: undefined, + s: undefined, + }, + prevSubPage: { + p: undefined, + s: undefined, + }, + nextPage: { + p: undefined, + s: undefined, + }, + nextSubPage: { + p: undefined, + s: undefined, + }, + currentPage: { + p: pageId, + s: subPageId, + }, + }; + + // set next -, previous -, next sub - and previous sub page + let navNIndex = res.indexOf('pn=n_'); + if (navNIndex > -1) { + this.navigationState.nextPage.p = parseInt(res.substring(navNIndex + 5, navNIndex + 8)); + this.navigationState.nextPage.s = parseInt(res.substring(navNIndex + 9, navNIndex + 10)); + } + let navPIndex = res.indexOf('pn=p_'); + if (navPIndex > -1) { + this.navigationState.prevPage.p = parseInt(res.substring(navPIndex + 5, navPIndex + 8)); + this.navigationState.prevPage.s = parseInt(res.substring(navPIndex + 9, navPIndex + 10)); + } + let navPSIndex = res.indexOf('pn=ps'); + if (navPSIndex > -1) { + this.navigationState.prevSubPage.p = parseInt(res.substring(navPSIndex + 5, navPSIndex + 8)); + this.navigationState.prevSubPage.s = parseInt(res.substring(navPSIndex + 9, navPSIndex + 10)); + } + let navNSIndex = res.indexOf('pn=ns'); + if (navNSIndex > -1) { + this.navigationState.nextSubPage.p = parseInt(res.substring(navNSIndex + 5, navNSIndex + 8)); + this.navigationState.nextSubPage.s = parseInt(res.substring(navNSIndex + 9, navNSIndex + 10)); + } + + let split = E.toString(res.split('
')[1].split('
')[0]); + + this.render(split); + }); + } + } + + + + + render(source) { + + g.setFontAlign(-1, -1); + g.setFont('4x6'); + + + const bytes = E.toUint8Array(E.decodeUTF8(source)); + let rowIndex = 0; + let totalIndex = 0; + let charIndex = 0; + + for (let charByte of bytes) { + { + if ((charByte >= 0 && charByte <= 7) || (charByte >= 16 && charByte <= 23)) { + const color = this.colorArray[charByte]; + g.setColor(color[0], color[1], color[2]); + } + } + g.drawString(source[totalIndex], (charIndex * 4) + 6, rowIndex * 7); + charIndex++; + totalIndex++; + if (charIndex == 40) { + rowIndex++; + charIndex = 0; + g.flip(); + } + } + } + + +} + +const BUTTON_BORDER_WITH = 2; + +class Button { +// position; +// value; +// highlightTimeoutId; + + + constructor(position, value) { + this.position = position; + this.value = value; + } + + draw(highlight) { + g.setColor(g.theme.fg); + g.fillRect( + this.position.x1, + this.position.y1, + this.position.x2, + this.position.y2 + ); + + if (highlight) { + g.setColor(g.theme.bgH); + } else { + g.setColor(g.theme.bg); + } + g.fillRect( + this.position.x1 + BUTTON_BORDER_WITH, + this.position.y1 + BUTTON_BORDER_WITH, + this.position.x2 - BUTTON_BORDER_WITH, + this.position.y2 - BUTTON_BORDER_WITH + ); + + g.setColor(g.theme.fg); + g.setFontAlign(0, 0); + g.setFont("Vector", 35); + g.drawString( + this.value, + this.position.x1 + (this.position.x2 - this.position.x1) / 2 + 2, + this.position.y1 + (this.position.y2 - this.position.y1) / 2 + 2 + ); + } + + handleTouchInput(n, e) { + if ( + e.x >= this.position.x1 && + e.x <= this.position.x2 && + e.y >= this.position.y1 && + e.y <= this.position.y2 + ) { + this.draw(true); // draw to highlight + this.highlightTimeoutId = setTimeout(() => { + this.draw(); + this.highlightTimeoutId = undefined; + }, 100); + return this.value; + } + else { + return undefined; + } + } + + disable() { + // disable button + if (this.highlightTimeoutId) { + clearTimeout(this.highlightTimeoutId); + this.highlightTimeoutId = undefined; + } + } + +} + +class Input { + + constructor(callback) { + this.inputCallback = callback; + this.inputVal = ""; + + let button1 = new Button({ x1: 1, y1: 35, x2: 58, y2: 70 }, '1'); + let button2 = new Button({ x1: 60, y1: 35, x2: 116, y2: 70 }, '2'); + let button3 = new Button({ x1: 118, y1: 35, x2: 174, y2: 70 }, '3'); + + let button4 = new Button({ x1: 1, y1: 72, x2: 58, y2: 105 }, '4'); + let button5 = new Button({ x1: 60, y1: 72, x2: 116, y2: 105 }, '5'); + let button6 = new Button({ x1: 118, y1: 72, x2: 174, y2: 105 }, '6'); + + let button7 = new Button({ x1: 1, y1: 107, x2: 58, y2: 140 }, '7'); + let button8 = new Button({ x1: 60, y1: 107, x2: 116, y2: 140 }, '8'); + let button9 = new Button({ x1: 118, y1: 107, x2: 174, y2: 140 }, '9'); + + let buttonOK = new Button({ x1: 1, y1: 142, x2: 58, y2: 174 }, "OK"); + let button0 = new Button({ x1: 60, y1: 142, x2: 116, y2: 174 }, "0"); + let buttonDelete = new Button({ x1: 118, y1: 142, x2: 174, y2: 174 }, "<-"); + + this.inputButtons = [ + button1, + button2, + button3, + button4, + button5, + button6, + button7, + button8, + button9, + buttonOK, + button0, + buttonDelete, + ]; + } + + handleTouchInput(n, e) { + let res = 'none'; + for (let button of this.inputButtons) { + const touchResult = button.handleTouchInput(n, e); + if (touchResult) { + res = touchResult; + } + } + + switch (res) { + case 'OK': + if(this.inputVal.length == 3){ + this.inputCallback(parseInt(this.inputVal)); + } + break; + case '<-': + this.removeNumber(); + this.drawField(); + break; + case 'none': + break; + default: + this.appendNumber(parseInt(res)); + this.drawField(); + } + + } + + + hide() { + for (let button of this.inputButtons) { + button.disable(); + } + } + + start(preset) { + if (preset) { + this.inputVal = preset.toString(); + } + else { + this.inputVal = ''; + } + + this.draw(); + } + + appendNumber(number) { + if (number === 0 && this.inputVal.length === 0) { + return; + } + + if (this.inputVal.length <= 2) { + this.inputVal = this.inputVal + number; + } + } + + removeNumber() { + if (this.inputVal.length > 0) { + this.inputVal = this.inputVal.slice(0, -1); + } + } + + reset() { + this.inputVal = ""; + } + + draw() { + g.clear(); + this.drawButtons(); + this.drawField(); + } + + drawButtons() { + for (let button of this.inputButtons) { + button.draw(); + } + } + + drawField() { + g.clearRect(0, 0, 176, 34); + g.setColor(g.theme.fg); + g.setFontAlign(-1, -1); + g.setFont("Vector:26x40"); + g.drawString(this.inputVal, 2, 0); + } +} + +// require('./Input'); + +class NOSTeletekstApp { + + constructor() { + console.log("this is the teletekst app!"); + this.isLeaving = false; + this.viewMode= 'VIEW'; + this.view = new View(); + this.input = new Input((newVal)=>this.inputHandler(newVal)); + this.view.start(); + + Bangle.setUI({ + mode: "custom", + remove: () => { + this.isLeaving = true; + console.log("teletext app: i am packing my stuff, goodbye"); + require("widget_utils").show(); // re-show widgets + }, + touch: (n, e) => { + if (this.viewMode == 'VIEW') { + // we need to go to input mode + this.setViewMode('INPUT'); + return; + } + if (this.viewMode == 'INPUT') { + this.input.handleTouchInput(n, e); + return; + + } + }, + swipe: (lr, ud) => { + if (this.viewMode == 'VIEW') { + this.view.handleSwipe(lr,ud); + } + if (this.viewMode == 'INPUT') { + if(lr == 1 && ud == 0){ + this.setViewMode('VIEW'); + } + } + } + + }); + + } + + inputHandler(input){ + // set viewMode back to view + this.view.nextStartPage = input; + this.setViewMode('VIEW'); + } + + setViewMode(newViewMode){ + this.viewMode = newViewMode; + if(newViewMode=='INPUT'){ + this.input.start(); + } + if(newViewMode=='VIEW'){ + this.input.hide(); + this.view.start(); + } + } + + +} +new NOSTeletekstApp(); \ No newline at end of file diff --git a/apps/nostt/nostt.icon.js b/apps/nostt/nostt.icon.js new file mode 100644 index 000000000..b9ef929be --- /dev/null +++ b/apps/nostt/nostt.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/AH4A/AH4AWgmSAwn/wACB/0Agf/4EAhMkyVIB4MB/4AB/ARGpIRBpMggEPCIQAC4ARCgQRDkkAv4jEAQIRLAQIRC/4RCiVJIgJKBwARCgPACIxWCCIn8NwQAB8YRFgIRDNARcFPQgRBNYZHB+IRDEYyPDAAPwn4RJIggRBg4RDNYrGCJQQRGkCSCWYP4CIgpCUI7FFCIMfa5DFFCIL7GJQQRLgARBoBXCO4KhBAH4A/AH4A/AD4A=")) \ No newline at end of file diff --git a/apps/nostt/nostt_logo.png b/apps/nostt/nostt_logo.png new file mode 100644 index 000000000..bf8f0d47d Binary files /dev/null and b/apps/nostt/nostt_logo.png differ diff --git a/apps/nostt/nostt_screenshot_1.png b/apps/nostt/nostt_screenshot_1.png new file mode 100644 index 000000000..ad65ecba7 Binary files /dev/null and b/apps/nostt/nostt_screenshot_1.png differ diff --git a/apps/nostt/nostt_screenshot_2.png b/apps/nostt/nostt_screenshot_2.png new file mode 100644 index 000000000..0faa9b1f6 Binary files /dev/null and b/apps/nostt/nostt_screenshot_2.png differ