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..ea3d7b3ff --- /dev/null +++ b/apps/nostt/README.md @@ -0,0 +1,8 @@ +# 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. \ No newline at end of file diff --git a/apps/nostt/metadata.json b/apps/nostt/metadata.json new file mode 100644 index 000000000..8d59abd7a --- /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.", + "src":"nostt.app.js", + "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..ee8f0b5f6 --- /dev/null +++ b/apps/nostt/nostt.app.js @@ -0,0 +1,506 @@ +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() { + // @ts-ignore + 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');
+
+ // @ts-ignore
+ 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