From d5c445f47254ec8865b785854bcf8c170985f2f1 Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Tue, 15 Aug 2023 14:44:21 +0200 Subject: [PATCH 0001/2187] Added `cards` app This app will be able to render cards as synchronized by Catima --- apps/cards/Barcode.js | 9 + apps/cards/README.md | 44 ++ apps/cards/app-icon.js | 1 + apps/cards/app.js | 183 ++++++ apps/cards/app.png | Bin 0 -> 218 bytes apps/cards/codabar.js | 63 +++ apps/cards/code39.js | 105 ++++ apps/cards/metadata.json | 21 + apps/cards/qrcode.js | 675 +++++++++++++++++++++++ apps/cards/screenshot_cards_barcode.png | Bin 0 -> 2222 bytes apps/cards/screenshot_cards_card1.png | Bin 0 -> 2643 bytes apps/cards/screenshot_cards_overview.png | Bin 0 -> 2287 bytes apps/cards/screenshot_cards_qrcode.png | Bin 0 -> 2935 bytes apps/cards/settings.js | 24 + 14 files changed, 1125 insertions(+) create mode 100644 apps/cards/Barcode.js create mode 100644 apps/cards/README.md create mode 100644 apps/cards/app-icon.js create mode 100644 apps/cards/app.js create mode 100644 apps/cards/app.png create mode 100644 apps/cards/codabar.js create mode 100644 apps/cards/code39.js create mode 100644 apps/cards/metadata.json create mode 100644 apps/cards/qrcode.js create mode 100644 apps/cards/screenshot_cards_barcode.png create mode 100644 apps/cards/screenshot_cards_card1.png create mode 100644 apps/cards/screenshot_cards_overview.png create mode 100644 apps/cards/screenshot_cards_qrcode.png create mode 100644 apps/cards/settings.js diff --git a/apps/cards/Barcode.js b/apps/cards/Barcode.js new file mode 100644 index 000000000..ad27da7e6 --- /dev/null +++ b/apps/cards/Barcode.js @@ -0,0 +1,9 @@ +class Barcode{ + constructor(data, options){ + this.data = data; + this.text = options.text || data; + this.options = options; + } +} + +module.exports = Barcode; diff --git a/apps/cards/README.md b/apps/cards/README.md new file mode 100644 index 000000000..724ef9534 --- /dev/null +++ b/apps/cards/README.md @@ -0,0 +1,44 @@ +# Cards + +Basic viewer for loyalty cards synced from Catima through GadgetBridge. +The app can display the cards' info (balance, expiration, note, etc.) and tapping on the appropriate field will display the code, if the type is supported. + +Double tapping on the code will come back to the visualization of the card's details. + +Beware that the small screen of the Banglejs 2 cannot render properly complex barcodes (in fact the resolution is very limited to render most barcodes). + +### Supported codes types + +* `CODE_39` +* `CODABAR` +* `QR_CODE` + +### How to sync + +_WIP: we currently cannot synchronize cards_ + +You can test it by sending on your bangle a file like this: + +_android.cards.json_ + +```json +[ + { + "id": 1, + "name": "First card", + "value": "01234", + "note": "Some stuff", + "type": "CODE_39", + "balance": "15 EUR", + "expiration": "1691102081" + }, + { + "id": 2, + "name": "Second card", + "value": "Hello world", + "note": "This is a qr generated on the bangle!", + "type": "QR_CODE", + "balance": "2 P" + } +] +``` diff --git a/apps/cards/app-icon.js b/apps/cards/app-icon.js new file mode 100644 index 000000000..3ec6948c4 --- /dev/null +++ b/apps/cards/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AYoIAjF/4v/F/4v/F/4v/FAdNAAsoADgv/F/4v/F/4vqu4AjF/4v/F/4v6poAjF/4AfFAYAGF/4v/F/4v/F/4v/F94A/AH4A/AH4A/ABo")) diff --git a/apps/cards/app.js b/apps/cards/app.js new file mode 100644 index 000000000..52eaa392c --- /dev/null +++ b/apps/cards/app.js @@ -0,0 +1,183 @@ +/* CARDS is a list of: + {id:int, + name, + value, + type, + expiration, + color, + balance, + note, + ... + } +*/ + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var FILE = "android.cards.json"; + +var Locale = require("locale"); + +var fontSmall = "6x8"; +var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; +var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; +var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; + +var CARDS = require("Storage").readJSON("android.cards.json",true)||[]; +var settings = require("Storage").readJSON("cards.settings.json",true)||{}; + +function getDate(timestamp) { + return new Date(timestamp*1000); +} +function formatDay(date) { + let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); + if (!settings.useToday) { + return formattedDate; + } + const today = new Date(Date.now()); + if (date.getDay() == today.getDay() && date.getMonth() == today.getMonth()) + return /*LANG*/"Today "; + else { + const tomorrow = new Date(Date.now() + 86400 * 1000); + if (date.getDay() == tomorrow.getDay() && date.getMonth() == tomorrow.getMonth()) { + return /*LANG*/"Tomorrow "; + } + return formattedDate; + } +} + +function printSquareCode(binary, size) { + var ratio = g.getWidth()/size; + for (var y = 0; y < size; y++) { + for (var x = 0; x < size; x++) { + if (binary[x + y * size]) { + g.setColor(g.theme.bg).fillRect({x:x*ratio, y:y*ratio, w:ratio, h:ratio}); + } else { + g.setColor(g.theme.fg).fillRect({x:x*ratio, y:y*ratio, w:ratio, h:ratio}); + } + } + } +} +function printLinearCode(binary) { + var yFrom = 0; + var width = g.getWidth()/binary.length; + for(var b = 0; b < binary.length; b++){ + var x = b * width; + if(binary[b] === "1"){ + g.setColor(g.theme.fg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight()}); + } + else if(binary[b]){ + g.setColor(g.theme.bg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight()}); + } + } +} + +function showCode(card) { + var code; + //FIXME doesn't work.. + var listener = (data) => { + if(data.double) showCard(card); + Bangle.removeListener("tap", listener); + }; + Bangle.on("tap", listener); + switch (card.type) { + case "QR_CODE": + const getBinaryQR = require("cards.qrcode.js"); + code = getBinaryQR(card.value); + printSquareCode(code.data, code.size); + break; + case "CODE_39": + const CODE39 = require("cards.code39.js"); + code = new CODE39(card.value, {}); + printLinearCode(code.encode().data); + break; + case "CODABAR": + const codabar = require("cards.codabar.js"); + code = new codabar(card.value, {}); + printLinearCode(code.encode().data); + break; + default: + g.clear(true); + g.setFont("Vector:15"); + g.setFontAlign(0,0); + g.drawString(card.value, g.getWidth()/2, g.getHeight()/2); + } +} + +function showCard(card) { + var lines = []; + var bodyFont = fontBig; + if(!card) return; + g.setFont(bodyFont); + //var lines = []; + if (card.name) lines = g.wrapString(card.name, g.getWidth()-10); + var titleCnt = lines.length; + var start = getDate(card.expiration); + var includeDay = true; + if (titleCnt) lines.push(""); // add blank line after name + lines = lines.concat("", /*LANG*/"Tap here to see the value"); + var valueLine = lines.length - 1; + if (card.expiration) + lines = lines.concat("",/*LANG*/"Expires"+": ", g.wrapString(formatDay(getDate(card.expiration)), g.getWidth()-10)); + if(card.balance) + lines = lines.concat("",/*LANG*/"Balance"+": ", g.wrapString(card.balance, g.getWidth()-10)); + if(card.note && card.note.trim()) + lines = lines.concat("",g.wrapString(card.note, g.getWidth()-10)); + lines = lines.concat("",/*LANG*/"< Back"); + E.showScroller({ + h : g.getFontHeight(), // height of each menu item in pixels + c : lines.length, // number of menu items + // a function to draw a menu item + draw : function(idx, r) { + // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 + g.setBgColor(idx=lines.length-2) + showList(); + if (idx>=valueLine) + showCode(card); + }, + back : () => showList() + }); +} + +// https://github.com/metafloor/bwip-js +// https://github.com/lindell/JsBarcode + +function showList() { + if(CARDS.length == 0) { + E.showMessage(/*LANG*/"No cards"); + return; + } + E.showScroller({ + h : 52, + c : Math.max(CARDS.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) + draw : function(idx, r) {"ram" + var card = CARDS[idx]; + g.setColor(g.theme.fg); + g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); + if (!card) return; + var isPast = false; + var x = r.x+2, name = card.name; + var body = card.expiration ? formatDay(getDate(card.expiration)) : ""; + if (card.balance) body += "\n" + card.balance; + if (name) g.setFontAlign(-1,-1).setFont(fontBig) + .setColor(isPast ? "#888" : g.theme.fg).drawString(name, x+4,r.y+2); + if (body) { + g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg); + g.drawString(body, x+10,r.y+20); + } + g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items + if(card.color) { + g.setColor("#"+(0x1000000+Number(card.color)).toString(16).padStart(6,"0")); + g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4); + } + }, + select : idx => showCard(CARDS[idx]), + back : () => load() + }); +} +showList(); diff --git a/apps/cards/app.png b/apps/cards/app.png new file mode 100644 index 0000000000000000000000000000000000000000..b2bfa59f442e61c0b74d96f9779f1345f4bd6856 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?3oVGw3ym^DWNDA?oa z;uumf=j}8{-c|=5m+ZyI^_Q?+2#Ss8R9nR;*v!N+VS?U=IHT`#&uxh`W?DdpSP*vaD3`byh- zj`jSVdaP`F%o(knzb6Yb-?RU?#Oa>>*Obm$8Oii3@>75=xxpHC@T5Rn#A2ZpKpumq LtDnm{r-UW|sxn7| literal 0 HcmV?d00001 diff --git a/apps/cards/codabar.js b/apps/cards/codabar.js new file mode 100644 index 000000000..072a8508a --- /dev/null +++ b/apps/cards/codabar.js @@ -0,0 +1,63 @@ +// Encoding specification: +// http://www.barcodeisland.com/codabar.phtml + +const Barcode = require("cards.Barcode.js"); + +class codabar extends Barcode{ + constructor(data, options){ + if (/^[0-9\-\$\:\.\+\/]+$/.test(data)) { + data = "A" + data + "A"; + } + + super(data.toUpperCase(), options); + + this.text = this.options.text || this.text.replace(/[A-D]/g, ''); + } + + valid(){ + return /^[A-D][0-9\-\$\:\.\+\/]+[A-D]$/.test(this.data) + } + + encode(){ + var result = []; + var encodings = this.getEncodings(); + for(var i = 0; i < this.data.length; i++){ + result.push(encodings[this.data.charAt(i)]); + // for all characters except the last, append a narrow-space ("0") + if (i !== this.data.length - 1) { + result.push("0"); + } + } + return { + text: this.text, + data: result.join('') + }; + } + + getEncodings(){ + return { + "0": "101010011", + "1": "101011001", + "2": "101001011", + "3": "110010101", + "4": "101101001", + "5": "110101001", + "6": "100101011", + "7": "100101101", + "8": "100110101", + "9": "110100101", + "-": "101001101", + "$": "101100101", + ":": "1101011011", + "/": "1101101011", + ".": "1101101101", + "+": "1011011011", + "A": "1011001001", + "B": "1001001011", + "C": "1010010011", + "D": "1010011001" + }; + } +} + +module.exports = codabar diff --git a/apps/cards/code39.js b/apps/cards/code39.js new file mode 100644 index 000000000..5eced539b --- /dev/null +++ b/apps/cards/code39.js @@ -0,0 +1,105 @@ +// Encoding documentation: +// https://en.wikipedia.org/wiki/Code_39#Encoding + +const Barcode = require("cards.Barcode.js"); + +class CODE39 extends Barcode { + constructor(data, options){ + data = data.toUpperCase(); + + // Calculate mod43 checksum if enabled + if(options.mod43){ + data += getCharacter(mod43checksum(data)); + } + + super(data, options); + } + + encode(){ + // First character is always a * + var result = getEncoding("*"); + + // Take every character and add the binary representation to the result + for(let i = 0; i < this.data.length; i++){ + result += getEncoding(this.data[i]) + "0"; + } + + // Last character is always a * + result += getEncoding("*"); + + return { + data: result, + text: this.text + }; + } + + valid(){ + return /^[0-9A-Z\-\.\ \$\/\+\%]+$/.test(this.data); + } +} + + + + + + +// All characters. The position in the array is the (checksum) value +var characters = [ + "0", "1", "2", "3", + "4", "5", "6", "7", + "8", "9", "A", "B", + "C", "D", "E", "F", + "G", "H", "I", "J", + "K", "L", "M", "N", + "O", "P", "Q", "R", + "S", "T", "U", "V", + "W", "X", "Y", "Z", + "-", ".", " ", "$", + "/", "+", "%", "*" +]; + +// The decimal representation of the characters, is converted to the +// corresponding binary with the getEncoding function +var encodings = [ + 20957, 29783, 23639, 30485, + 20951, 29813, 23669, 20855, + 29789, 23645, 29975, 23831, + 30533, 22295, 30149, 24005, + 21623, 29981, 23837, 22301, + 30023, 23879, 30545, 22343, + 30161, 24017, 21959, 30065, + 23921, 22385, 29015, 18263, + 29141, 17879, 29045, 18293, + 17783, 29021, 18269, 17477, + 17489, 17681, 20753, 35770 +]; + +// Get the binary representation of a character by converting the encodings +// from decimal to binary +function getEncoding(character){ + return getBinary(characterValue(character)); +} + +function getBinary(characterValue){ + return encodings[characterValue].toString(2); +} + +function getCharacter(characterValue){ + return characters[characterValue]; +} + +function characterValue(character){ + return characters.indexOf(character); +} + +function mod43checksum(data){ + var checksum = 0; + for(let i = 0; i < data.length; i++){ + checksum += characterValue(data[i]); + } + + checksum = checksum % 43; + return checksum; +} + +module.exports = CODE39; diff --git a/apps/cards/metadata.json b/apps/cards/metadata.json new file mode 100644 index 000000000..538a8b56e --- /dev/null +++ b/apps/cards/metadata.json @@ -0,0 +1,21 @@ +{ + "id": "cards", + "name": "Cards", + "version": "0.1", + "description": "Display loyalty cards", + "icon": "app.png", + "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_event1.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], + "tags": "cards", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"cards.app.js","url":"app.js"}, + {"name":"cards.settings.js","url":"settings.js"}, + {"name":"cards.Barcode.js","url":"Barcode.js"}, + {"name":"cards.qrcode.js","url":"qrcode.js"}, + {"name":"cards.codabar.js","url":"codabar.js"}, + {"name":"cards.code39.js","url":"code39.js"}, + {"name":"cards.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"cards.settings.json"}] +} diff --git a/apps/cards/qrcode.js b/apps/cards/qrcode.js new file mode 100644 index 000000000..d27c55d7b --- /dev/null +++ b/apps/cards/qrcode.js @@ -0,0 +1,675 @@ +var c = E.compiledC(` +// int get_qr(int, int) + +typedef signed char __int8_t; +typedef unsigned char __uint8_t; +typedef signed short int __int16_t; +typedef unsigned short int __uint16_t; +typedef signed int __int32_t; +typedef unsigned int __uint32_t; + +typedef __int8_t int8_t; +typedef __int16_t int16_t; +typedef __int32_t int32_t; +typedef __uint8_t uint8_t; +typedef __uint16_t uint16_t; +typedef __uint32_t uint32_t; + +typedef struct QRCode { + uint8_t version; + uint8_t size; + uint8_t ecc; + uint8_t mode; + uint8_t mask; + uint8_t *modules; +} QRCode; +uint16_t qrcode_getBufferSize(uint8_t version); + +int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data); +int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length); + +bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y); + +static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = { + { 10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372}, + { 7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750}, + { 17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430}, + { 13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040}, +}; + +static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = { + { 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, + { 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, + { 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, + { 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, +}; + +static const uint16_t NUM_RAW_DATA_MODULES[40] = { + 208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523, + 7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587, + 19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648 +}; +static int max(int a, int b) { + if (a > b) { return a; } + return b; +} + +static int abs(int value) { + if (value < 0) { return -value; } + return value; +} + +static void *memset(void *s, int c, int n) { + char *arr = (char *)s; + for (int i = 0; i= '0' && c <= '9') { return (c - '0'); } + if (c >= 'A' && c <= 'Z') { return (c - 'A' + 10); } + switch (c) { + case ' ': return 36; + case '$': return 37; + case '%': return 38; + case '*': return 39; + case '+': return 40; + case '-': return 41; + case '.': return 42; + case '/': return 43; + case ':': return 44; + } + + return -1; +} + +static bool isAlphanumeric(const char *text, uint16_t length) { + while (length != 0) { + if (getAlphanumeric(text[--length]) == -1) { return false; } + } + return true; +} +static bool isNumeric(const char *text, uint16_t length) { + while (length != 0) { + char c = text[--length]; + if (c < '0' || c > '9') { return false; } + } + return true; +} +static char getModeBits(uint8_t version, uint8_t mode) { + unsigned int modeInfo = 0x7bbb80a; + if (version > 9) { modeInfo >>= 9; } + + if (version > 26) { modeInfo >>= 9; } + char result = 8 + ((modeInfo >> (3 * mode)) & 0x07); + if (result == 15) { result = 16; } + + return result; +} + +typedef struct BitBucket { + uint32_t bitOffsetOrWidth; + uint16_t capacityBytes; + uint8_t *data; +} BitBucket; +static uint16_t bb_getGridSizeBytes(uint8_t size) { + return (((size * size) + 7) / 8); +} + +static uint16_t bb_getBufferSizeBytes(uint32_t bits) { + return ((bits + 7) / 8); +} + +static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) { + bitBuffer->bitOffsetOrWidth = 0; + bitBuffer->capacityBytes = capacityBytes; + bitBuffer->data = data; + + memset(data, 0, bitBuffer->capacityBytes); +} + +static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) { + bitGrid->bitOffsetOrWidth = size; + bitGrid->capacityBytes = bb_getGridSizeBytes(size); + bitGrid->data = data; + + memset(data, 0, bitGrid->capacityBytes); +} + +static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) { + uint32_t offset = bitBuffer->bitOffsetOrWidth; + for (int8_t i = length - 1; i >= 0; i--, offset++) { + bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7)); + } + bitBuffer->bitOffsetOrWidth = offset; +} + +static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + uint8_t mask = 1 << (7 - (offset & 0x07)); + if (on) { + bitGrid->data[offset >> 3] |= mask; + } else { + bitGrid->data[offset >> 3] &= ~mask; + } +} + +static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + uint8_t mask = 1 << (7 - (offset & 0x07)); + bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0); + if (on ^ invert) { + bitGrid->data[offset >> 3] |= mask; + } else { + bitGrid->data[offset >> 3] &= ~mask; + } +} + +static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) { + uint32_t offset = y * bitGrid->bitOffsetOrWidth + x; + return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; +} + +static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) { + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t y = 0; y < size; y++) { + for (uint8_t x = 0; x < size; x++) { + if (bb_getBit(isFunction, x, y)) { continue; } + + bool invert = 0; + switch (mask) { + case 0: invert = (x + y) % 2 == 0; break; + case 1: invert = y % 2 == 0; break; + case 2: invert = x % 3 == 0; break; + case 3: invert = (x + y) % 3 == 0; break; + case 4: invert = (x / 3 + y / 2) % 2 == 0; break; + case 5: invert = x * y % 2 + x * y % 3 == 0; break; + case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; + case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; + } + bb_invertBit(modules, x, y, invert); + } + } +} + +static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) { + bb_setBit(modules, x, y, on); + bb_setBit(isFunction, x, y, true); +} +static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) { + uint8_t size = modules->bitOffsetOrWidth; + + for (int8_t i = -4; i <= 4; i++) { + for (int8_t j = -4; j <= 4; j++) { + uint8_t dist = max(abs(i), abs(j)); + int16_t xx = x + j, yy = y + i; + if (0 <= xx && xx < size && 0 <= yy && yy < size) { + setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4); + } + } + } +} +static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) { + for (int8_t i = -2; i <= 2; i++) { + for (int8_t j = -2; j <= 2; j++) { + setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1); + } + } +} + +static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) { + + uint8_t size = modules->bitOffsetOrWidth; + uint32_t data = ecc << 3 | mask; + uint32_t rem = data; + for (int i = 0; i < 10; i++) { + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + } + + data = data << 10 | rem; + data ^= 0x5412; + for (uint8_t i = 0; i <= 5; i++) { + setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0); + } + + setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0); + setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0); + setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0); + + for (int8_t i = 9; i < 15; i++) { + setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0); + } + for (int8_t i = 0; i <= 7; i++) { + setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0); + } + + for (int8_t i = 8; i < 15; i++) { + setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0); + } + + setFunctionModule(modules, isFunction, 8, size - 8, true); +} +static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) { + + int8_t size = modules->bitOffsetOrWidth; + if (version < 7) { return; } + uint32_t rem = version; + for (uint8_t i = 0; i < 12; i++) { + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + } + + uint32_t data = version << 12 | rem; + for (uint8_t i = 0; i < 18; i++) { + bool bit = ((data >> i) & 1) != 0; + uint8_t a = size - 11 + i % 3, b = i / 3; + setFunctionModule(modules, isFunction, a, b, bit); + setFunctionModule(modules, isFunction, b, a, bit); + } +} + +static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) { + + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t i = 0; i < size; i++) { + setFunctionModule(modules, isFunction, 6, i, i % 2 == 0); + setFunctionModule(modules, isFunction, i, 6, i % 2 == 0); + } + drawFinderPattern(modules, isFunction, 3, 3); + drawFinderPattern(modules, isFunction, size - 4, 3); + drawFinderPattern(modules, isFunction, 3, size - 4); + + if (version > 1) { + + uint8_t alignCount = version / 7 + 2; + uint8_t step; + if (version != 32) { + step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2; + } else { + step = 26; + } + + uint8_t alignPositionIndex = alignCount - 1; + uint8_t alignPosition[alignCount]; + + alignPosition[0] = 6; + + uint8_t l_size = version * 4 + 17; + for (uint8_t i = 0, pos = l_size - 7; i < alignCount - 1; i++, pos -= step) { + alignPosition[alignPositionIndex--] = pos; + } + + for (uint8_t i = 0; i < alignCount; i++) { + for (uint8_t j = 0; j < alignCount; j++) { + if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) { + continue; + } else { + drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]); + } + } + } + } + drawFormatBits(modules, isFunction, ecc, 0); + drawVersion(modules, isFunction, version); +} +static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) { + + uint32_t bitLength = codewords->bitOffsetOrWidth; + uint8_t *data = codewords->data; + + uint8_t size = modules->bitOffsetOrWidth; + uint32_t i = 0; + for (int16_t right = size - 1; right >= 1; right -= 2) { + if (right == 6) { right = 5; } + + for (uint8_t vert = 0; vert < size; vert++) { + for (int j = 0; j < 2; j++) { + uint8_t x = right - j; + bool upwards = ((right & 2) == 0) ^ (x < 6); + uint8_t y = upwards ? size - 1 - vert : vert; + if (!bb_getBit(isFunction, x, y) && i < bitLength) { + bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0); + i++; + } + } + } + } +} +static uint32_t getPenaltyScore(BitBucket *modules) { + uint32_t result = 0; + + uint8_t size = modules->bitOffsetOrWidth; + for (uint8_t y = 0; y < size; y++) { + + bool colorX = bb_getBit(modules, 0, y); + for (uint8_t x = 1, runX = 1; x < size; x++) { + bool cx = bb_getBit(modules, x, y); + if (cx != colorX) { + colorX = cx; + runX = 1; + + } else { + runX++; + if (runX == 5) { + result += 3; + } else if (runX > 5) { + result++; + } + } + } + } + for (uint8_t x = 0; x < size; x++) { + bool colorY = bb_getBit(modules, x, 0); + for (uint8_t y = 1, runY = 1; y < size; y++) { + bool cy = bb_getBit(modules, x, y); + if (cy != colorY) { + colorY = cy; + runY = 1; + } else { + runY++; + if (runY == 5) { + result += 3; + } else if (runY > 5) { + result++; + } + } + } + } + + uint16_t black = 0; + for (uint8_t y = 0; y < size; y++) { + uint16_t bitsRow = 0, bitsCol = 0; + for (uint8_t x = 0; x < size; x++) { + bool color = bb_getBit(modules, x, y); + if (x > 0 && y > 0) { + bool colorUL = bb_getBit(modules, x - 1, y - 1); + bool colorUR = bb_getBit(modules, x, y - 1); + bool colorL = bb_getBit(modules, x - 1, y); + if (color == colorUL && color == colorUR && color == colorL) { + result += 3; + } + } + bitsRow = ((bitsRow << 1) & 0x7FF) | color; + bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x); + if (x >= 10) { + if (bitsRow == 0x05D || bitsRow == 0x5D0) { + result += 40; + } + if (bitsCol == 0x05D || bitsCol == 0x5D0) { + result += 40; + } + } + if (color) { black++; } + } + } + uint16_t total = size * size; + for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) { + result += 10; + } + + return result; +} + +static uint8_t rs_multiply(uint8_t x, uint8_t y) { + uint16_t z = 0; + for (int8_t i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + return z; +} + +static void rs_init(uint8_t degree, uint8_t *coeff) { + memset(coeff, 0, degree); + coeff[degree - 1] = 1; + uint16_t root = 1; + for (uint8_t i = 0; i < degree; i++) { + + for (uint8_t j = 0; j < degree; j++) { + coeff[j] = rs_multiply(coeff[j], root); + if (j + 1 < degree) { + coeff[j] ^= coeff[j + 1]; + } + } + root = (root << 1) ^ ((root >> 7) * 0x11D); + } +} + +static void rs_getRemainder(uint8_t degree, uint8_t *coeff, const uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) { + for (uint8_t i = 0; i < length; i++) { + uint8_t factor = data[i] ^ result[0]; + for (uint8_t j = 1; j < degree; j++) { + result[(j - 1) * stride] = result[j * stride]; + } + result[(degree - 1) * stride] = 0; + + for (uint8_t j = 0; j < degree; j++) { + result[j * stride] ^= rs_multiply(coeff[j], factor); + } + } +} +static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) { + int8_t mode = 2; + + if (isNumeric((char*)text, length)) { + mode = 0; + bb_appendBits(dataCodewords, 1 << 0, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 0)); + + uint16_t accumData = 0; + uint8_t accumCount = 0; + for (uint16_t i = 0; i < length; i++) { + accumData = accumData * 10 + ((char)(text[i]) - '0'); + accumCount++; + if (accumCount == 3) { + bb_appendBits(dataCodewords, accumData, 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) { + bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1); + } + + } else if (isAlphanumeric((char*)text, length)) { + mode = 1; + bb_appendBits(dataCodewords, 1 << 1, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 1)); + + uint16_t accumData = 0; + uint8_t accumCount = 0; + for (uint16_t i = 0; i < length; i++) { + accumData = accumData * 45 + getAlphanumeric((char)(text[i])); + accumCount++; + if (accumCount == 2) { + bb_appendBits(dataCodewords, accumData, 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) { + bb_appendBits(dataCodewords, accumData, 6); + } + + } else { + bb_appendBits(dataCodewords, 1 << 2, 4); + bb_appendBits(dataCodewords, length, getModeBits(version, 2)); + for (uint16_t i = 0; i < length; i++) { + bb_appendBits(dataCodewords, (char)(text[i]), 8); + } + } + + return mode; +} + +static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) { + uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1]; + uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1]; + uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; + uint8_t blockEccLen = totalEcc / numBlocks; + uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks; + uint8_t shortBlockLen = moduleCount / 8 / numBlocks; + + uint8_t shortDataBlockLen = shortBlockLen - blockEccLen; + + uint8_t result[data->capacityBytes]; + memset(result, 0, sizeof(result)); + + uint8_t coeff[blockEccLen]; + rs_init(blockEccLen, coeff); + + uint16_t offset = 0; + uint8_t *dataBytes = data->data; + + for (uint8_t i = 0; i < shortDataBlockLen; i++) { + uint16_t index = i; + uint8_t stride = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) { + result[offset++] = dataBytes[index]; + if (blockNum == numShortBlocks) { stride++; } + + index += stride; + } + } + { + uint16_t index = shortDataBlockLen * (numShortBlocks + 1); + uint8_t stride = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) { + result[offset++] = dataBytes[index]; + + if (blockNum == 0) { stride++; } + index += stride; + } + } + + uint8_t blockSize = shortDataBlockLen; + for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) { + if (blockNum == numShortBlocks) { blockSize++; } + + rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks); + dataBytes += blockSize; + } + + memcpy(data->data, result, data->capacityBytes); + data->bitOffsetOrWidth = moduleCount; +} + +static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0); + +uint16_t qrcode_getBufferSize(uint8_t version) { + return bb_getGridSizeBytes(4 * version + 17); +} +int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) { + uint8_t size = version * 4 + 17; + qrcode->version = version; + qrcode->size = size; + qrcode->ecc = ecc; + qrcode->modules = modules; + + uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03; + uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1]; + uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1]; + struct BitBucket codewords; + uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)]; + bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes)); + int8_t mode = encodeDataCodewords(&codewords, data, length, version); + + if (mode < 0) { return -1; } + qrcode->mode = mode; + uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth; + if (padding > 4) { padding = 4; } + bb_appendBits(&codewords, 0, padding); + bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8); + for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) { + bb_appendBits(&codewords, padByte, 8); + } + + BitBucket modulesGrid; + bb_initGrid(&modulesGrid, modules, size); + + BitBucket isFunctionGrid; + uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)]; + bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size); + drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits); + performErrorCorrection(version, eccFormatBits, &codewords); + drawCodewords(&modulesGrid, &isFunctionGrid, &codewords); + uint8_t mask = 0; + int32_t minPenalty = (2147483647); + for (uint8_t i = 0; i < 8; i++) { + drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i); + applyMask(&modulesGrid, &isFunctionGrid, i); + int penalty = getPenaltyScore(&modulesGrid); + if (penalty < minPenalty) { + mask = i; + minPenalty = penalty; + } + applyMask(&modulesGrid, &isFunctionGrid, i); + } + + qrcode->mask = mask; + drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask); + applyMask(&modulesGrid, &isFunctionGrid, mask); + + return 0; +} + +int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) { + return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data)); +} + +bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) { + if (x >= qrcode->size || y >= qrcode->size) { + return false; + } + + uint32_t offset = y * qrcode->size + x; + return (qrcode->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0; +} + +int get_qr (char *string, uint8_t *qrcodeBitmap) { + // The structure to manage the QR code + QRCode qrcode; + + // Allocate a chunk of memory to store the QR code + uint8_t qrcodeBytes[qrcode_getBufferSize(3)]; + + qrcode_initText(&qrcode, qrcodeBytes, 3, 0, string); + for (uint8_t y = 0; y < qrcode.size; y++) { + for (uint8_t x = 0; x < qrcode.size; x++) { + qrcodeBitmap[x + y * qrcode.size] = qrcode_getModule(&qrcode, x, y); + } + } + return qrcode.size; +} +`); + +function getBinaryQR (value) { + var qrcodeBitmap = new Uint8Array(850); + var flatValue = Uint8Array(E.toArrayBuffer(E.toFlatString(value ,0))); + var valueAddr = E.getAddressOf(flatValue, true); + var qrAddr = E.getAddressOf(qrcodeBitmap, true); + if (valueAddr == 0 || qrAddr == 0) { + console.log ("Failed to get flat arrays.."); + //return; + } + var qrsize = c.get_qr(valueAddr, qrAddr); + return { data: qrcodeBitmap, size: qrsize }; +} + +module.exports = getBinaryQR; diff --git a/apps/cards/screenshot_cards_barcode.png b/apps/cards/screenshot_cards_barcode.png new file mode 100644 index 0000000000000000000000000000000000000000..1910c173e1e3f1f69c774f2aeeac9904346078f8 GIT binary patch literal 2222 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 literal 0 HcmV?d00001 diff --git a/apps/cards/screenshot_cards_qrcode.png b/apps/cards/screenshot_cards_qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..5bace3e6efc390a744bca7c6ff6d35ccc96ef565 GIT binary patch literal 2935 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 ai0#X-nV2|BX3xu&G>G8h?Ofv+e))gT)1V9h literal 0 HcmV?d00001 diff --git a/apps/cards/settings.js b/apps/cards/settings.js new file mode 100644 index 000000000..b11ba785a --- /dev/null +++ b/apps/cards/settings.js @@ -0,0 +1,24 @@ +(function(back) { + function gbSend(message) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } + var settings = require("Storage").readJSON("cards.settings.json",1)||{}; + function updateSettings() { + require("Storage").writeJSON("cards.settings.json", settings); + } + var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; + var mainmenu = { + "" : { "title" : "Cards" }, + "< Back" : back, + /*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" }, + /*LANG*/"Use 'Today',..." : { + value : !!settings.useToday, + onchange: v => { + settings.useToday = v; + updateSettings(); + } + }, + }; + E.showMenu(mainmenu); +}) From 56f9788034db6c61224bab32610cfd32f1317112 Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Tue, 15 Aug 2023 15:31:18 +0200 Subject: [PATCH 0002/2187] cards: added changelog and used libraries --- apps/cards/ChangeLog | 1 + apps/cards/README.md | 8 +++++++- apps/cards/metadata.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 apps/cards/ChangeLog diff --git a/apps/cards/ChangeLog b/apps/cards/ChangeLog new file mode 100644 index 000000000..00945cd13 --- /dev/null +++ b/apps/cards/ChangeLog @@ -0,0 +1 @@ +0.01: Simple app to display loyalty cards diff --git a/apps/cards/README.md b/apps/cards/README.md index 724ef9534..4bd70a0b1 100644 --- a/apps/cards/README.md +++ b/apps/cards/README.md @@ -1,6 +1,6 @@ # Cards -Basic viewer for loyalty cards synced from Catima through GadgetBridge. +Simple app to display loyalty cards synced from Catima through GadgetBridge. The app can display the cards' info (balance, expiration, note, etc.) and tapping on the appropriate field will display the code, if the type is supported. Double tapping on the code will come back to the visualization of the card's details. @@ -42,3 +42,9 @@ _android.cards.json_ } ] ``` + +### Credits + +Barcode generation adapted from [lindell/JsBarcode](https://github.com/lindell/JsBarcode) + +QR code generation adapted from [ricmoo/QRCode](https://github.com/ricmoo/QRCode) diff --git a/apps/cards/metadata.json b/apps/cards/metadata.json index 538a8b56e..5f72a1c5b 100644 --- a/apps/cards/metadata.json +++ b/apps/cards/metadata.json @@ -4,7 +4,7 @@ "version": "0.1", "description": "Display loyalty cards", "icon": "app.png", - "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_event1.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], + "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], "tags": "cards", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", From 2bc2d9ff3585c44a12b60548c6dae32e1c9bc602 Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Wed, 16 Aug 2023 08:58:32 +0200 Subject: [PATCH 0003/2187] Added padding to codes and updated screenshots --- apps/cards/app.js | 29 ++++++++++++++++-------- apps/cards/metadata.json | 2 +- apps/cards/screenshot_cards_barcode.png | Bin 2222 -> 2283 bytes apps/cards/screenshot_cards_card1.png | Bin 2643 -> 2693 bytes apps/cards/screenshot_cards_card2.png | Bin 0 -> 2506 bytes 5 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 apps/cards/screenshot_cards_card2.png diff --git a/apps/cards/app.js b/apps/cards/app.js index 52eaa392c..a6d802352 100644 --- a/apps/cards/app.js +++ b/apps/cards/app.js @@ -47,27 +47,30 @@ function formatDay(date) { } function printSquareCode(binary, size) { - var ratio = g.getWidth()/size; + var padding = 5; + var ratio = (g.getWidth()-(2*padding))/size; + g.setColor(g.theme.fg).fillRect(0, 0, g.getWidth(), g.getHeight()); for (var y = 0; y < size; y++) { for (var x = 0; x < size; x++) { if (binary[x + y * size]) { - g.setColor(g.theme.bg).fillRect({x:x*ratio, y:y*ratio, w:ratio, h:ratio}); + g.setColor(g.theme.bg).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); } else { - g.setColor(g.theme.fg).fillRect({x:x*ratio, y:y*ratio, w:ratio, h:ratio}); + g.setColor(g.theme.fg).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); } } } } function printLinearCode(binary) { - var yFrom = 0; + var yFrom = 15; + var yTo = 28; var width = g.getWidth()/binary.length; for(var b = 0; b < binary.length; b++){ var x = b * width; if(binary[b] === "1"){ - g.setColor(g.theme.fg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight()}); + g.setColor(g.theme.fg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); } else if(binary[b]){ - g.setColor(g.theme.bg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight()}); + g.setColor(g.theme.bg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); } } } @@ -80,6 +83,8 @@ function showCode(card) { Bangle.removeListener("tap", listener); }; Bangle.on("tap", listener); + E.showScroller(); + g.clear(true); switch (card.type) { case "QR_CODE": const getBinaryQR = require("cards.qrcode.js"); @@ -87,18 +92,23 @@ function showCode(card) { printSquareCode(code.data, code.size); break; case "CODE_39": + g.setFont("Vector:20"); + g.setFontAlign(0,1); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); const CODE39 = require("cards.code39.js"); code = new CODE39(card.value, {}); printLinearCode(code.encode().data); break; case "CODABAR": + g.setFont("Vector:20"); + g.setFontAlign(0,1); + g.drawString(card.value, g.getWidth()/2, g.getHeight()); const codabar = require("cards.codabar.js"); code = new codabar(card.value, {}); printLinearCode(code.encode().data); break; default: - g.clear(true); - g.setFont("Vector:15"); + g.setFont("Vector:30"); g.setFontAlign(0,0); g.drawString(card.value, g.getWidth()/2, g.getHeight()/2); } @@ -114,8 +124,7 @@ function showCard(card) { var titleCnt = lines.length; var start = getDate(card.expiration); var includeDay = true; - if (titleCnt) lines.push(""); // add blank line after name - lines = lines.concat("", /*LANG*/"Tap here to see the value"); + lines = lines.concat("", /*LANG*/"View code"); var valueLine = lines.length - 1; if (card.expiration) lines = lines.concat("",/*LANG*/"Expires"+": ", g.wrapString(formatDay(getDate(card.expiration)), g.getWidth()-10)); diff --git a/apps/cards/metadata.json b/apps/cards/metadata.json index 5f72a1c5b..0aa3249fb 100644 --- a/apps/cards/metadata.json +++ b/apps/cards/metadata.json @@ -4,7 +4,7 @@ "version": "0.1", "description": "Display loyalty cards", "icon": "app.png", - "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], + "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], "tags": "cards", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", diff --git a/apps/cards/screenshot_cards_barcode.png b/apps/cards/screenshot_cards_barcode.png index 1910c173e1e3f1f69c774f2aeeac9904346078f8..e57e9765a76c717f1fcd8d8b85c85def3bff566f 100644 GIT binary patch literal 2283 zcmZ{mc{r3^AIFawF*J`co{2FA4Ot?p5l!}NV<}5Yj5T9>ZINtIOxf2`#u9m=NM%iU z@{}pNv7}y3Z69fSKVPe zZMID;g?xLq_2gUS@|X3q^cdI8OOd@g^8*spPq@Mb7H;Y@#S>rrBWnFe_xRMx;it1J z?h{R}krUH8-3!dM>{WcSq4-;$G|zuzwYA$}jQ+&BWk$v^r$2bX7A`wHYf5Pe902_e z6uw*ZqnJs7c;cAGp=f zj)02#k!-amsst)w2)cDMM)*c!sLgWV`*P1#pUC+o_C3ela97!vN*{eKF8Fm>w+>Ek zC54dVC4d+@t#)3d2YPUAm!v3dV(@McVWOSNV~PfuV4dXUYz>pY9vQ^(*4q@_>5Onr z>ZnzLTWw8$=u+I%i#yy-3+CADpckHdfi#S=JlH}DcEdMv(jz zx#(bEGI216`=dlyv6Z@?TK;nHBqmsn?@8N3jnTD)GvfAqbeinnRubGiM{crXGp_}@ zENM0cRgXDEag~iezP*|=)--|jlG+y8qT;UGMNYvB1vu&zzA1}qe#uO=e(>xVVF_{^ z`KNij4f=5I1SKKUC!Fsv4ie=v!X5*Q*C&V;rdA|!ZwvQm-<(S(RwMVpnG8LweF_Gh z7)rF7TIN%KTvaC8&G@w>q%pEGSx{k2{e+*BR6Avhc2s%Qd98D2aQe%8_LmJvoDCW+ zT>Iv@&$thC##V%2ady%T?Q#d3-R8Qj>Gkx>e%i!GUyd;MMiTHUkMG|pp%(-!uXcFt z5mMw8q#0nC4qPlq+>~2krF%$WF5lKEj5N`dyFoI?%Vmg=lS^7{CQa=6>^?9nJ+^HQ z@elEPl@%Ni-cjtS)HNSu-MY;&g=AbHV+j$nNJ8Ch0FX;-#80z7yNKXKV~t_9MscV5 z(9CxaZc&)rST;Ve0_K*mGRU;A_mT=c1gS0qHI#wWBF=T*J8w&I3Y|5_1kXOa+}&fF z8}>dn;`F(|a(<2-D6sP{LNm!=Jj>r%_QpE{`5xO(Sdaswa!b z@3DYCln{#Tl-^gR(Li1z@-!t1u%A=&Jw3zYb?ju)wZT7osD0R0zxVyE2( zwPZ6J#V3>oZw|GD1D0!|qi*hi`MbCM}mgG zW`D-3LIUG1Xxd->bVlJqoBjyTR!weTfC7EV*(FiL+hU+(IC-Pb(*{Cs9>yfK&25?BJ6$>#()QM{PE$Y{(R|GEN96v6 zNTT>o%O;Chm8xQEgh^t>oojvZ5!^)9mduy_2twtxAdUP-1EyV2@`!u$ZX7e0m%E!7FEJ{-?Av*{({hkB(c@8SWL@m+}-IOtb4(}O*&1Oo+v*HMz{FcAGF z?_lLg-e)3=ikzG}nfk5h-V2oP6I!fhXXTL0^;;!Wi`$b%(i=*E_2${Mlcx}Sp62&; zqogQi_2?0_>TAOt9OHX*jz4W@?CaqT1?epuSf!7SE_4?yXY@4pzG-5)_Jt*gdZ0Ep z6%;qxZbnPS%I3cu-N5iP9!?b3&j1n3nZ;D>=En*iQs*ttnnNet0#xcuYS%xuEZH3T z_M(kvtp)VK_E`wnX^Dtl8dpKF_PYhL9AS(6Jld};nSj(X7;fAlpvuRPhAko@m@+}_ z#~&zXt*;AIH!y{HBD_H+tU)8yrj*;e)i8hnY&0V(wqY!xX1VGNK zX3?3VmsF~y@gYq@3$q|Pl`SeHzDy(Qv*2tO*2pe7{%6B1(0EEw<%7JeTu#V{mR_aC zg6wKiZ6h-Jlzwx7t+!npZn~hPsOWj2q?NKjhSwZi;q`IDO-*n%B}-4!A1WEDZK-Z~ q2oNN1t{XB8RT*c>+8IRj8mbFnU^;R!_dQg^05fAtqe=s}%l`#-lng%r literal 2222 zcmYLLdpwhEAIFBh6V2nez1HK9MV^FA%dzG#!b;>=TCoX}!-H}*C8NnR$;(1?uoZ?J zE3}@LLy6?{B**dOkdWBq_}*LZ;q(36_x-uqt+;cqOiE1Lj!S6THci^x-fnrZB*<4*<&Uj}ZvsS2mO!xL+t4u3EVxnW(}LS%)LfGj~b|7A;&E!g@G#*!2WQ(iHbAf32Nw*_#ls<8|Mxm1SAbe6BTa zmUCXRX4ASOi3M|`I)x;2!~-1-oBOQdG9|&5Qx$>$?Vtcj)wrx~LFIyt4j39v?+c3D zt640e|0fC<(lszp3O4ecjB^2M;kkrXeCX|s{`!^1Tj{O~Hd_l92C2`C8OGipkB(Iw z2&_#|%2dpR%P|_EjE4FlIF96g^SpLV_KDTk$4S~*Tr1pq3Va-^agzF2-tdWDm#S(0 zGYaU02?HoZ!74JAM}9}R)6;0t=G3Hw_g}h$osncs8J9}&{a__tBpw7#wA-!nSsp;R z$q<_6>KK`M?eo~!VNJ&g4rb|`Y#0v{j%Sj=`LPoSHC5@Be#IWjEGLp+x|Etoq^vNt zz1@xU@^2SJbuM$-Nd0CcKAxHiJ(gOGbz}Wu&@Ct1m5Q0%4^A{GO$JSJFa~nNFG|wo z;>&j{-3x#1QQvF5xTFKX~HWscLRNN0;CAq&RvVS7NbU9eoXl|S*8lV zRJJp~c|(kZsY+NAAe#x$CxxtkH52^j?QBBhEFirna_E$V@i42g@3dqb=wl-M{r23x`pPc(@tMx(_ z4722`07n_QpYiouHG6QB0GYqi55?U$PQB`^M*bz3H$cm;j`dq*V&6!hET&38zwYC< zp>)mIwF7Rp-%A28dB;aR9L1jpMNi?g=!RuQ;{CB4*(k)EabF{@H;glaJ>v3Q$8YJb&E@Gs{} z{Ezmo@^f8Wf3W0_sw)pwy_X`6LzdPzaxlmLU_cTv$FDRHruINmc@1=ZGq~ zc9doZMB^!Zf@i!k*X_xpDc#iKX!kH0lJ5pqTN{PkGG8iUc^}Z1+;ozw=d0t?4f?#& z#rKbECEuFuZBFr)t=1t3xMxjgy<^$7UPfDoEN7NE%baSIlmEP)5h7>y-AiKb;)wK- zxRL%s`+`#naRu1e#)%iAw=kmte5sZE#6OH zLWrB%y(it?aM1v43jySRuWg5@F*JCGSf4=Z2eesLy*{t|$bu*$26N4mWaqjIpxk_Q z*b6oA?eV9Gbk__3DCwpZMytKxjNwW0WB~@RwojiW;ljEbV;`HlS5#ik^e5=S|7jdLPPwI?e72_Xt>0AIsfmN7r(F6-0>~TRJzwgWJU;OkngDXkV zFc30~;p z>c^B*obHwX?(J`xC>ad^@`yCL6GS9vQ7nj*PH$6i_Rni?{44MO|0vj%Y+qguFDGf$ zMap5Ji&dTN)v=H+Q~e=ATKVEZ%AhHnC2cooi&R zQBPiMH`O6Xnm@pE)09(>YUp21g4BlE8rH}O^)i0b5@-oP>v2nKZhP4LS>iN%Ve{j9 zaHdCP$ALR_-J$8&d1QG_PsFsrV|8d_qT!_3J^yk`bI;Q3hPD7A(m0%f0b-M#Ffsaf zGR?zvkjFeUe6W`oPVj4t6!YY>x@^Wz#7LK%irM8Hl495lJiB4FA(r_c@mYCT$mXh?5KQE<)Rhk#-piN zZ>U7pqkjUH+b3jTk{b@36fZsV=vky;tf)EJD7iAj{1zI>W7jYEGiUGJ1NBnU=xM;= kur|Occax^m5%3v2p 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 B&yWBB literal 0 HcmV?d00001 From 26da9b2bb36b6842ec1583f815f27d2a4a25ce66 Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Thu, 17 Aug 2023 14:19:40 +0200 Subject: [PATCH 0004/2187] Made code colours independent on the theme --- apps/cards/app.js | 41 +++++++++++++++++++++++----------------- apps/cards/metadata.json | 2 +- apps/cards/settings.js | 5 ----- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/apps/cards/app.js b/apps/cards/app.js index a6d802352..3a2635e69 100644 --- a/apps/cards/app.js +++ b/apps/cards/app.js @@ -14,6 +14,10 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); +//may make it configurable in the future +const WHITE=-1 +const BLACK=0 + var FILE = "android.cards.json"; var Locale = require("locale"); @@ -49,13 +53,12 @@ function formatDay(date) { function printSquareCode(binary, size) { var padding = 5; var ratio = (g.getWidth()-(2*padding))/size; - g.setColor(g.theme.fg).fillRect(0, 0, g.getWidth(), g.getHeight()); for (var y = 0; y < size; y++) { for (var x = 0; x < size; x++) { if (binary[x + y * size]) { - g.setColor(g.theme.bg).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); + g.setColor(BLACK).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); } else { - g.setColor(g.theme.fg).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); + g.setColor(WHITE).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio}); } } } @@ -67,47 +70,51 @@ function printLinearCode(binary) { for(var b = 0; b < binary.length; b++){ var x = b * width; if(binary[b] === "1"){ - g.setColor(g.theme.fg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); + g.setColor(BLACK).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); } else if(binary[b]){ - g.setColor(g.theme.bg).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); + g.setColor(WHITE).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)}); } } } function showCode(card) { - var code; - //FIXME doesn't work.. + //FIXME tap doesn't work.. var listener = (data) => { if(data.double) showCard(card); Bangle.removeListener("tap", listener); }; Bangle.on("tap", listener); E.showScroller(); - g.clear(true); + // theme independent + g.setColor(WHITE).fillRect(0, 0, g.getWidth(), g.getHeight()); switch (card.type) { - case "QR_CODE": + case "QR_CODE": { const getBinaryQR = require("cards.qrcode.js"); - code = getBinaryQR(card.value); + let code = getBinaryQR(card.value); printSquareCode(code.data, code.size); break; - case "CODE_39": + } + case "CODE_39": { g.setFont("Vector:20"); - g.setFontAlign(0,1); + g.setFontAlign(0,1).setColor(BLACK); g.drawString(card.value, g.getWidth()/2, g.getHeight()); const CODE39 = require("cards.code39.js"); - code = new CODE39(card.value, {}); + let code = new CODE39(card.value, {}); printLinearCode(code.encode().data); break; - case "CODABAR": + } + case "CODABAR": { g.setFont("Vector:20"); - g.setFontAlign(0,1); + g.setFontAlign(0,1).setColor(BLACK); g.drawString(card.value, g.getWidth()/2, g.getHeight()); const codabar = require("cards.codabar.js"); - code = new codabar(card.value, {}); + let code = new codabar(card.value, {}); printLinearCode(code.encode().data); break; + } default: + g.clear(true); g.setFont("Vector:30"); g.setFontAlign(0,0); g.drawString(card.value, g.getWidth()/2, g.getHeight()/2); @@ -146,7 +153,7 @@ function showCard(card) { }, select : function(idx) { if (idx>=lines.length-2) showList(); - if (idx>=valueLine) + else if (idx==valueLine) showCode(card); }, back : () => showList() diff --git a/apps/cards/metadata.json b/apps/cards/metadata.json index 0aa3249fb..63b7da847 100644 --- a/apps/cards/metadata.json +++ b/apps/cards/metadata.json @@ -1,7 +1,7 @@ { "id": "cards", "name": "Cards", - "version": "0.1", + "version": "0.01", "description": "Display loyalty cards", "icon": "app.png", "screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}], diff --git a/apps/cards/settings.js b/apps/cards/settings.js index b11ba785a..db0ab56de 100644 --- a/apps/cards/settings.js +++ b/apps/cards/settings.js @@ -1,13 +1,8 @@ (function(back) { - function gbSend(message) { - Bluetooth.println(""); - Bluetooth.println(JSON.stringify(message)); - } var settings = require("Storage").readJSON("cards.settings.json",1)||{}; function updateSettings() { require("Storage").writeJSON("cards.settings.json", settings); } - var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[]; var mainmenu = { "" : { "title" : "Cards" }, "< Back" : back, From b8a123cfe8fbfc93e27f319942977247615ab408 Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Sat, 19 Aug 2023 11:56:29 +0200 Subject: [PATCH 0005/2187] Added support for loyalty cards from gadgetbridge --- apps/android/ChangeLog | 1 + apps/android/boot.js | 5 +++++ apps/android/metadata.json | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index f2a0c5b3f..d531e43a9 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -31,3 +31,4 @@ 0.30: Send firmware and hardware versions on connection Allow alarm enable/disable 0.31: Implement API for activity fetching +0.32: Added support for loyalty cards from gadgetbridge diff --git a/apps/android/boot.js b/apps/android/boot.js index a8027a67c..846fc40a8 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -236,6 +236,11 @@ event.t="remove"; } require("messages").pushMessage(event); + }, + "cards" : function() { + // we receive all, just override what we have + if (Array.isArray(event.d)) + require("Storage").writeJSON("android.cards.json", event.d); } }; var h = HANDLERS[event.t]; diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 8d65d32e3..68bd946c5 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.31", + "version": "0.32", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", @@ -15,6 +15,6 @@ {"name":"android.img","url":"app-icon.js","evaluate":true}, {"name":"android.boot.js","url":"boot.js"} ], - "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}], + "data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}], "sortorder": -8 } From 7b8cfeb6edb16384f66ef5148fca6cbb006f7c6b Mon Sep 17 00:00:00 2001 From: Hugh Barney Date: Sat, 19 Aug 2023 18:48:03 +0100 Subject: [PATCH 0006/2187] Asteroids - increased ship, asteroids and font size --- apps/astroid/ChangeLog | 1 + apps/astroid/asteroids.js | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/astroid/ChangeLog b/apps/astroid/ChangeLog index faa0ca5f8..d6a9951bc 100644 --- a/apps/astroid/ChangeLog +++ b/apps/astroid/ChangeLog @@ -1,2 +1,3 @@ 0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast 0.03: Bangle 2 support +0.04: Increase size if ship, asteroids and fonts for better readability diff --git a/apps/astroid/asteroids.js b/apps/astroid/asteroids.js index 6cfa70b47..44593fb95 100644 --- a/apps/astroid/asteroids.js +++ b/apps/astroid/asteroids.js @@ -18,6 +18,10 @@ if (process.env.HWVERSION==2) { } var W = g.getWidth(); var H = g.getHeight(); +var SS = W/11; // ship back length +var SL = W/15; // ship side length +var AS = W/18; // asteroid radius + g.clear().setFontAlign(0,-1); function newAst(x,y) { @@ -25,7 +29,7 @@ function newAst(x,y) { x:x,y:y, vx:Math.random()-0.5, vy:Math.random()-0.5, - rad:3+Math.random()*5 + rad:3+Math.random()*AS }; return a; } @@ -42,7 +46,9 @@ var lastFrame; function gameStop() { running = false; g.clear(); - g.drawString("Game Over!",120,(H-6)/2); + g.setFont('Vector', W/7); + g.setFontAlign(0,0); + g.drawString("Game Over!", W/2, H/2); g.flip(); } @@ -104,12 +110,13 @@ function onFrame() { } g.clear(); - g.drawString(score,W-20,0); + g.setFont('Vector', 16); + g.drawString(score,W-20,16); var rs = Math.PI*0.8; g.drawPoly([ - ship.x+Math.cos(ship.r)*4, ship.y+Math.sin(ship.r)*4, - ship.x+Math.cos(ship.r+rs)*3, ship.y+Math.sin(ship.r+rs)*3, - ship.x+Math.cos(ship.r-rs)*3, ship.y+Math.sin(ship.r-rs)*3, + ship.x+Math.cos(ship.r)*SS, ship.y+Math.sin(ship.r)*SS, + ship.x+Math.cos(ship.r+rs)*SL, ship.y+Math.sin(ship.r+rs)*SL, + ship.x+Math.cos(ship.r-rs)*SL, ship.y+Math.sin(ship.r-rs)*SL, ],true); var na = []; ammo.forEach(function(a) { From bc92e0c8c6e301fbe22b061fff3ca901547a67e3 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sat, 19 Aug 2023 20:10:13 +0200 Subject: [PATCH 0007/2187] widbat: Use flash, not fork to indicate charging Green fork is not easily visible, which can be confusing. --- apps/widbat/ChangeLog | 1 + apps/widbat/metadata.json | 2 +- apps/widbat/widget.js | 11 +++++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/widbat/ChangeLog b/apps/widbat/ChangeLog index cb11b1be9..9577d952b 100644 --- a/apps/widbat/ChangeLog +++ b/apps/widbat/ChangeLog @@ -8,3 +8,4 @@ 0.09: Misc speed/memory tweaks 0.10: Color changes due to the battery level 0.11: Change level for medium charge (50% -> 40%), and darken color on light themes as yellow was almost invisible +0.12: Use black flash instead of green fork to indicate charging diff --git a/apps/widbat/metadata.json b/apps/widbat/metadata.json index 0151fcbd7..7f0d99f09 100644 --- a/apps/widbat/metadata.json +++ b/apps/widbat/metadata.json @@ -1,7 +1,7 @@ { "id": "widbat", "name": "Battery Level Widget", - "version": "0.11", + "version": "0.12", "description": "Show the current battery level and charging status in the top right of the clock", "icon": "widget.png", "type": "widget", diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index 98eb09227..f87452893 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -26,16 +26,19 @@ var s = 39; var x = this.x, y = this.y; g.reset(); - if (Bangle.isCharging()) { - g.setColor("#0f0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); - x+=16; - } g.setColor(g.theme.fg).fillRect(x,y+2,x+s-4,y+21).clearRect(x+2,y+4,x+s-6,y+19).fillRect(x+s-3,y+10,x+s,y+14); var battery = E.getBattery(); if(battery < 20) {g.setColor("#f00");} else if (battery < 40) {g.setColor(g.theme.dark ? "#ff0" : "#f80");} else {g.setColor("#0f0");} g.fillRect(x+4,y+6,x+4+battery*(s-12)/100,y+17); + if (Bangle.isCharging()) { + let hy = y+11; + let hx = x+s/2; + let flash = [x+5,hy, hx-4,hy-1, hx,y+6, x+s-7,y+hy, + hx+4,y+hy+1, hx,y+17]; + g.setColor(g.theme.fg).fillPoly(flash); + } }}; setWidth(); })() From 2e3ee8cbc360a36eb47691b975b0178cb11b42c0 Mon Sep 17 00:00:00 2001 From: Hugh Barney Date: Sun, 20 Aug 2023 15:10:06 +0100 Subject: [PATCH 0008/2187] Asteroids - improved collision detected for larger ship v astroid --- apps/astroid/ChangeLog | 1 + apps/astroid/asteroids.js | 7 +++++-- apps/astroid/metadata.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/astroid/ChangeLog b/apps/astroid/ChangeLog index d6a9951bc..8f1d5e355 100644 --- a/apps/astroid/ChangeLog +++ b/apps/astroid/ChangeLog @@ -1,3 +1,4 @@ 0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast 0.03: Bangle 2 support 0.04: Increase size if ship, asteroids and fonts for better readability +0.05: improve collision detect for larger ship v astroid diff --git a/apps/astroid/asteroids.js b/apps/astroid/asteroids.js index 44593fb95..862b6f368 100644 --- a/apps/astroid/asteroids.js +++ b/apps/astroid/asteroids.js @@ -21,6 +21,9 @@ var H = g.getHeight(); var SS = W/11; // ship back length var SL = W/15; // ship side length var AS = W/18; // asteroid radius +// radius of ship, assumed a circle inside equilateral traingle of side SS +// r = a / root 3 where a is length of equilateral triangle +var SR = SS / Math.sqrt(3); g.clear().setFontAlign(0,-1); @@ -45,7 +48,7 @@ var lastFrame; function gameStop() { running = false; - g.clear(); + //g.clear(); g.setFont('Vector', W/7); g.setFontAlign(0,0); g.drawString("Game Over!", W/2, H/2); @@ -172,7 +175,7 @@ function onFrame() { var dx = a.x-ship.x; var dy = a.y-ship.y; var d = Math.sqrt(dx*dx+dy*dy); - if (d < a.rad) crashed = true; + if (d < a.rad + SR) crashed = true; }); ast=na; if (!ast.length) { diff --git a/apps/astroid/metadata.json b/apps/astroid/metadata.json index abb3681ff..e0f4425d8 100644 --- a/apps/astroid/metadata.json +++ b/apps/astroid/metadata.json @@ -1,7 +1,7 @@ { "id": "astroid", "name": "Asteroids!", - "version": "0.03", + "version": "0.05", "description": "Retro asteroids game", "icon": "asteroids.png", "screenshots": [{"url":"screenshot_asteroids.png"}], From 648cc11fdc8ba8ef678295ed8d72e0956c97e079 Mon Sep 17 00:00:00 2001 From: Hugh Barney Date: Sun, 20 Aug 2023 16:15:34 +0100 Subject: [PATCH 0009/2187] Asteroids, switched to 7 point astroid polygon --- apps/astroid/ChangeLog | 3 ++- apps/astroid/asteroids.js | 18 ++++++++++++++---- apps/astroid/metadata.json | 4 ++-- apps/astroid/screenshot.png | Bin 0 -> 2140 bytes apps/astroid/screenshot_asteroids.png | Bin 1498 -> 0 bytes 5 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 apps/astroid/screenshot.png delete mode 100644 apps/astroid/screenshot_asteroids.png diff --git a/apps/astroid/ChangeLog b/apps/astroid/ChangeLog index 8f1d5e355..0e2f13745 100644 --- a/apps/astroid/ChangeLog +++ b/apps/astroid/ChangeLog @@ -1,4 +1,5 @@ 0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast 0.03: Bangle 2 support -0.04: Increase size if ship, asteroids and fonts for better readability +0.04: Increase size of ship, asteroids and fonts for better readability 0.05: improve collision detect for larger ship v astroid +0.06: added, 7 point asteroid ploygon, made ship solid, rather than outline diff --git a/apps/astroid/asteroids.js b/apps/astroid/asteroids.js index 862b6f368..076e9a4c3 100644 --- a/apps/astroid/asteroids.js +++ b/apps/astroid/asteroids.js @@ -48,10 +48,9 @@ var lastFrame; function gameStop() { running = false; - //g.clear(); g.setFont('Vector', W/7); g.setFontAlign(0,0); - g.drawString("Game Over!", W/2, H/2); + g.drawString("Game Over", W/2, H/2); g.flip(); } @@ -116,7 +115,7 @@ function onFrame() { g.setFont('Vector', 16); g.drawString(score,W-20,16); var rs = Math.PI*0.8; - g.drawPoly([ + g.fillPoly([ ship.x+Math.cos(ship.r)*SS, ship.y+Math.sin(ship.r)*SS, ship.x+Math.cos(ship.r+rs)*SL, ship.y+Math.sin(ship.r+rs)*SL, ship.x+Math.cos(ship.r-rs)*SL, ship.y+Math.sin(ship.r-rs)*SL, @@ -147,7 +146,18 @@ function onFrame() { ast.forEach(function(a) { a.x += a.vx*d; a.y += a.vy*d; - g.drawCircle(a.x, a.y, a.rad); + //g.drawCircle(a.x, a.y, a.rad); + // a 7 point asteroid with rough circle radius of scale 2 + g.drawPoly([ + a.x , a.y - 1.5 * a.rad, + a.x + a.rad , a.y , + a.x + a.rad/2 , a.y , + a.x + a.rad/2 , a.y + a.rad/2 , + a.x , a.y + a.rad , + a.x - a.rad , a.y , + a.x - a.rad , a.y - a.rad + ],true); + if (a.x<0) a.x+=W; if (a.y<0) a.y+=H; if (a.x>=W) a.x-=W; diff --git a/apps/astroid/metadata.json b/apps/astroid/metadata.json index e0f4425d8..f73feec43 100644 --- a/apps/astroid/metadata.json +++ b/apps/astroid/metadata.json @@ -1,10 +1,10 @@ { "id": "astroid", "name": "Asteroids!", - "version": "0.05", + "version": "0.06", "description": "Retro asteroids game", "icon": "asteroids.png", - "screenshots": [{"url":"screenshot_asteroids.png"}], + "screenshots": [{"url":"screenshot.png"}], "tags": "game", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, diff --git a/apps/astroid/screenshot.png b/apps/astroid/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..81120267dba73cd07b48925bc5c5b04129c75ca4 GIT binary patch literal 2140 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 MUHx3vIVCg!06kBM&j0`b From 4079123c93bed52b653a8d53f4520aafc33c16b9 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 19 Aug 2023 18:21:21 -0500 Subject: [PATCH 0010/2187] =?UTF-8?q?WIP:=20New=20app=20=E2=80=9Cstamplog?= =?UTF-8?q?=E2=80=9D,=20with=20partial=20implementation=20of=20main=20scre?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/stamplog/app.js | 340 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 apps/stamplog/app.js diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js new file mode 100644 index 000000000..df11c5f15 --- /dev/null +++ b/apps/stamplog/app.js @@ -0,0 +1,340 @@ +Layout = require('Layout'); +locale = require('locale'); +storage = require('Storage'); + +// Storage filename to store user's timestamp log +const LOG_FILENAME = 'stamplog.json'; + +// Min number of pixels of movement to recognize a touchscreen drag/swipe +const DRAG_THRESHOLD = 10; + +var settings = { + logItemFont: '12x20' +}; + + +// Fetch a stringified image +function getIcon(id) { + if (id == 'add') { +// Graphics.createImage(` +// XX X X X X +// XX X X X X +// XXXXXX X X X X +// XXXXXX X X X X +// XX X X X X +// XX X X X X +// X XX X X +// X X X X +// X XX X +// X X X +// X X +// X XX +// XXX XX +// XXXXX +// XXXX +// XX +// `); + return "\0\x17\x10\x81\x000\t\x12`$K\xF0\x91'\xE2D\x83\t\x12\x06$H\x00\xB1 \x01$\x80\x042\x00\b(\x00 \x00A\x80\x01\xCC\x00\x03\xE0\x00\x0F\x00\x00\x18\x00\x00"; + } else if (id == 'menu') { +// Graphics.createImage(` +// +// +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// XXXXXXXXXXXXXXXX +// XXXXXXXXXXXXXXXX +// +// +// `); + return "\0\x10\x10\x81\0\0\0\0\0\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0\xFF\xFF\xFF\xFF\0\0\0\0"; + } +} + + +//// Data models ////////////////////////////////// + +// High-level timestamp log object that provides an interface to the +// UI for managing log entries and automatically loading/saving +// changes to flash storage. +class StampLog { + constructor(filename) { + // Name of file to save log to + this.filename = filename; + + // `true` when we have changes that need to be saved + this.isDirty = false; + // Wait at most this many msec upon first data change before + // saving (this is to avoid excessive writes to flash if several + // changes happen quickly; we wait a little bit so they can be + // rolled into a single write) + this.saveTimeout = 30000; + // setTimeout ID for scheduled save job + this.saveId = null; + // Underlying raw log data object. Outside this class it's + // recommended to use only the class methods to change it rather + // than modifying the object directly to ensure that changes are + // recognized and saved to storage. + this.log = this.load(); + } + + // Return the version of the log data that is currently in storage + load() { + let log = storage.readJSON(this.filename, true); + if (!log) log = []; + // Convert stringified datetimes back into Date objects + for (let logEntry of log) { + logEntry.stamp = new Date(logEntry.stamp); + } + return log; + } + + // Write current log data to storage if anything needs to be saved + save() { + // Cancel any pending scheduled calls to save() + if (this.saveId) { + clearTimeout(this.saveId); + this.saveId = null; + } + + if (this.isDirty) { + if (storage.writeJSON(this.filename, this.log)) { + console.log('stamplog: save to storage completed'); + this.isDirty = false; + } else { + console.log('stamplog: save to storage FAILED'); + } + } else { + console.log('stamplog: skipping save to storage because no changes made'); + } + } + + // Mark log as needing to be (re)written to storage + setDirty() { + this.isDirty = true; + if (!this.saveId) { + this.saveId = setTimeout(this.save.bind(this), this.saveTimeout); + } + } + + // Add a timestamp for the current time to the end of the log + addEntry() { + this.log.push({ + stamp: new Date() + }); + this.setDirty(); + } + + // Delete the log objects given in the array `entries` from the log + deleteEntries(entries) { + this.log = this.log.filter(entry => !entries.includes(entry)); + this.setDirty(); + } +} + + +//// UI /////////////////////////////////////////// + +// UI layout render callback for log entries +function renderLogItem(elem) { + if (elem.item) { + g.setColor(g.theme.bg) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1) + .setFont('12x20') + .setFontAlign(-1, -1) + .setColor(g.theme.fg) + .drawLine(elem.x, elem.y, elem.x + elem.w - 1, elem.y) + .drawString(locale.date(elem.item.stamp, 1) + + '\n' + + locale.time(elem.item.stamp).trim(), + elem.x, elem.y); + } else { + g.setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.25)) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1); + } +} + +// Main app screen interface, launched by calling start() +class MainScreen { + constructor(stampLog) { + this.stampLog = stampLog; + + // Values set up by start() + this.logItemsPerPage = null; + this.logScrollPos = null; + this.layout = null; + + // Handlers/listeners + this.buttonTimeoutId = null; + this.listeners = {}; + } + + // Launch this UI and make it live + start() { + this.layout = this.getLayout(); + mainScreen.scrollLog('b'); + mainScreen.render(); + + Object.assign(this.listeners, this.getTouchListeners()); + Bangle.on('drag', this.listeners.drag); + } + + // Stop this UI, shut down all timers/listeners, and otherwise clean up + stop() { + if (this.buttonTimeoutId) { + clearTimeout(this.buttonTimeoutId); + this.buttonTimeoutId = null; + } + + // Kill layout handlers + Bangle.removeListener('drag', this.listeners.drag); + Bangle.setUI(); + + // Probably not necessary, but provides feedback for debugging :-) + g.clear(); + } + + // Generate the layout structure for the main UI + getLayout() { + let layout = new Layout( + {type: 'v', + c: [ + // Placeholder to force bottom alignment when there is unused + // vertical screen space + {type: '', id: 'placeholder', fillx: 1, filly: 1}, + + {type: 'v', + id: 'logItems', + + // To be filled in with log item elements once we determine + // how many will fit on screen + c: [], + }, + + {type: 'h', + id: 'buttons', + c: [ + {type: 'btn', font: '6x8:2', fillx: 1, label: '+ XX:XX', id: 'addBtn', + cb: this.addTimestamp.bind(this)}, + {type: 'btn', font: '6x8:2', label: getIcon('menu'), id: 'menuBtn', + cb: L => console.log(L)}, + ], + }, + ], + } + ); + + // Calculate how many log items per page we have space to display + layout.update(); + let availableHeight = layout.placeholder.h; + g.setFont(settings.logItemFont); + let logItemHeight = g.getFontHeight() * 2; + this.logItemsPerPage = Math.floor(availableHeight / logItemHeight); + + // Populate log items in layout + for (i = 0; i < this.logItemsPerPage; i++) { + layout.logItems.c.push( + {type: 'custom', render: renderLogItem, item: undefined, fillx: 1, height: logItemHeight} + ); + } + layout.update(); + + return layout; + } + + // Redraw a particular display `item`, or everything if `item` is falsey + render(item) { + if (!item || item == 'log') { + let layLogItems = this.layout.logItems; + let logIdx = this.logScrollPos - this.logItemsPerPage; + for (let elem of layLogItems.c) { + logIdx++; + elem.item = this.stampLog.log[logIdx]; + } + this.layout.render(layLogItems); + } + + if (!item || item == 'buttons') { + this.layout.addBtn.label = getIcon('add') + ' ' + locale.time(new Date(), 1).trim(); + this.layout.render(this.layout.buttons); + + // Auto-update time of day indication on log-add button upon next minute + if (!this.buttonTimeoutId) { + this.buttonTimeoutId = setTimeout( + () => { + this.buttonTimeoutId = null; + this.render('buttons'); + }, + 60000 - (Date.now() % 60000) + ); + } + } + + } + + getTouchListeners() { + let distanceY = null; + + function dragHandler(ev) { + // Handle up/down swipes for scrolling + if (ev.b) { + if (distanceY === null) { + // Drag started + distanceY = ev.dy; + } else { + // Drag in progress + distanceY += ev.dy; + } + } else { + // Drag ended + if (Math.abs(distanceY) > DRAG_THRESHOLD) { + this.scrollLog(distanceY > 0 ? 'u' : 'd'); + this.render('log'); + } + distanceY = null; + } + } + + return { + 'drag': dragHandler.bind(this), + }; + } + + // Add current timestamp to log and update UI display + addTimestamp() { + this.stampLog.addEntry(); + this.scrollLog('b'); + this.render('log'); + } + + // Scroll display in given direction or to given position: + // 'u': up, 'd': down, 't': to top, 'b': to bottom + scrollLog(how) { + top = (this.stampLog.log.length - 1) % this.logItemsPerPage; + bottom = this.stampLog.log.length - 1; + + if (how == 'u') { + this.logScrollPos -= this.logItemsPerPage; + } else if (how == 'd') { + this.logScrollPos += this.logItemsPerPage; + } else if (how == 't') { + this.logScrollPos = top; + } else if (how == 'b') { + this.logScrollPos = bottom; + } + + this.logScrollPos = E.clip(this.logScrollPos, top, bottom); + } +} + + +stampLog = new StampLog(); +mainScreen = new MainScreen(stampLog); +mainScreen.start(); From a7024aa52b57c7659a81d2fb3fe4e615a1878e1b Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 21 Aug 2023 15:01:42 -0500 Subject: [PATCH 0011/2187] Save log on quit and display some kind of alert if error occurs saving --- apps/stamplog/app.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index df11c5f15..f14777448 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -110,6 +110,7 @@ class StampLog { this.isDirty = false; } else { console.log('stamplog: save to storage FAILED'); + this.emit('saveError'); } } else { console.log('stamplog: skipping save to storage because no changes made'); @@ -335,6 +336,15 @@ class MainScreen { } +Bangle.loadWidgets(); +Bangle.drawWidgets(); + stampLog = new StampLog(); +E.on('kill', stampLog.save.bind(stampLog)); +stampLog.on('saveError', () => { + E.showAlert('Trouble saving timestamp log: Data may be lost!', + "Can't save log"); +}); + mainScreen = new MainScreen(stampLog); mainScreen.start(); From 536da24a10e9d2e961fc2d2b73c372997452e752 Mon Sep 17 00:00:00 2001 From: Gabriele Monaco Date: Tue, 22 Aug 2023 17:56:02 +0200 Subject: [PATCH 0012/2187] Added card's colour in the card's page --- apps/cards/app.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/cards/app.js b/apps/cards/app.js index 3a2635e69..691baf987 100644 --- a/apps/cards/app.js +++ b/apps/cards/app.js @@ -50,6 +50,17 @@ function formatDay(date) { } } +function getColor(intColor) { + return "#"+(0x1000000+Number(intColor)).toString(16).padStart(6,"0"); +} +function isLight(color) { + var r = +("0x"+color.slice(1,3)); + var g = +("0x"+color.slice(3,5)); + var b = +("0x"+color.slice(5,7)); + var threshold = 0x88 * 3; + return (r+g+b) > threshold; +} + function printSquareCode(binary, size) { var padding = 5; var ratio = (g.getWidth()-(2*padding))/size; @@ -79,7 +90,6 @@ function printLinearCode(binary) { } function showCode(card) { - //FIXME tap doesn't work.. var listener = (data) => { if(data.double) showCard(card); Bangle.removeListener("tap", listener); @@ -140,14 +150,18 @@ function showCard(card) { if(card.note && card.note.trim()) lines = lines.concat("",g.wrapString(card.note, g.getWidth()-10)); lines = lines.concat("",/*LANG*/"< Back"); + var titleBgColor = card.color ? getColor(card.color) : g.theme.bg2; + var titleColor = g.theme.fg2; + if (card.color) + titleColor = isLight(titleBgColor) ? BLACK : WHITE; E.showScroller({ h : g.getFontHeight(), // height of each menu item in pixels c : lines.length, // number of menu items // a function to draw a menu item draw : function(idx, r) { // FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 - g.setBgColor(idx Date: Thu, 24 Aug 2023 18:27:22 +0200 Subject: [PATCH 0013/2187] Going back from the code view with button --- apps/cards/README.md | 2 +- apps/cards/app.js | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/cards/README.md b/apps/cards/README.md index 4bd70a0b1..bd9157d62 100644 --- a/apps/cards/README.md +++ b/apps/cards/README.md @@ -3,7 +3,7 @@ Simple app to display loyalty cards synced from Catima through GadgetBridge. The app can display the cards' info (balance, expiration, note, etc.) and tapping on the appropriate field will display the code, if the type is supported. -Double tapping on the code will come back to the visualization of the card's details. +To come back to the visualization of the card's details from the code view, simply press the button. Beware that the small screen of the Banglejs 2 cannot render properly complex barcodes (in fact the resolution is very limited to render most barcodes). diff --git a/apps/cards/app.js b/apps/cards/app.js index 691baf987..dcef7da76 100644 --- a/apps/cards/app.js +++ b/apps/cards/app.js @@ -90,12 +90,9 @@ function printLinearCode(binary) { } function showCode(card) { - var listener = (data) => { - if(data.double) showCard(card); - Bangle.removeListener("tap", listener); - }; - Bangle.on("tap", listener); E.showScroller(); + // keeping it on rising edge would come back twice.. + setWatch(()=>showCard(card), BTN, {edge:"falling"}); // theme independent g.setColor(WHITE).fillRect(0, 0, g.getWidth(), g.getHeight()); switch (card.type) { From 3b5fe9c4a8e1a55f80c45d06f7e661922aaa5214 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Thu, 24 Aug 2023 15:12:25 -0500 Subject: [PATCH 0014/2187] Fix accidental use of global var instead of this --- apps/stamplog/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index f14777448..6263fc85a 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -180,8 +180,8 @@ class MainScreen { // Launch this UI and make it live start() { this.layout = this.getLayout(); - mainScreen.scrollLog('b'); - mainScreen.render(); + this.scrollLog('b'); + this.render(); Object.assign(this.listeners, this.getTouchListeners()); Bangle.on('drag', this.listeners.drag); From 705d6619a8d1c3b960ebeb1f49850d8f57f6f353 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Thu, 24 Aug 2023 15:14:34 -0500 Subject: [PATCH 0015/2187] Make save error fit screen better and try to avoid disrupting current UI --- apps/stamplog/app.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 6263fc85a..d01102dd6 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -197,9 +197,6 @@ class MainScreen { // Kill layout handlers Bangle.removeListener('drag', this.listeners.drag); Bangle.setUI(); - - // Probably not necessary, but provides feedback for debugging :-) - g.clear(); } // Generate the layout structure for the main UI @@ -336,15 +333,25 @@ class MainScreen { } +function saveErrorAlert() { + currentUI.stop(); + E.showPrompt( + 'Trouble saving timestamp log; data may be lost!', + {title: "Can't save log", + img: '', + buttons: {'Ok': true}, + } + ).then(currentUI.start.bind(currentUI)); +} + + Bangle.loadWidgets(); Bangle.drawWidgets(); stampLog = new StampLog(); E.on('kill', stampLog.save.bind(stampLog)); -stampLog.on('saveError', () => { - E.showAlert('Trouble saving timestamp log: Data may be lost!', - "Can't save log"); -}); +stampLog.on('saveError', saveErrorAlert); + +var currentUI = new MainScreen(stampLog); +currentUI.start(); -mainScreen = new MainScreen(stampLog); -mainScreen.start(); From 575c09436303e574785607ce0dd954009992bf44 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Fri, 25 Aug 2023 23:30:54 -0500 Subject: [PATCH 0016/2187] Minor refactoring --- apps/stamplog/app.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index d01102dd6..02698cdaa 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -164,6 +164,7 @@ function renderLogItem(elem) { // Main app screen interface, launched by calling start() class MainScreen { + constructor(stampLog) { this.stampLog = stampLog; @@ -179,12 +180,11 @@ class MainScreen { // Launch this UI and make it live start() { - this.layout = this.getLayout(); + this._initLayout(); this.scrollLog('b'); this.render(); - Object.assign(this.listeners, this.getTouchListeners()); - Bangle.on('drag', this.listeners.drag); + this._initTouch(); } // Stop this UI, shut down all timers/listeners, and otherwise clean up @@ -199,8 +199,7 @@ class MainScreen { Bangle.setUI(); } - // Generate the layout structure for the main UI - getLayout() { + _initLayout() { let layout = new Layout( {type: 'v', c: [ @@ -244,7 +243,7 @@ class MainScreen { } layout.update(); - return layout; + this.layout = layout; } // Redraw a particular display `item`, or everything if `item` is falsey @@ -263,7 +262,8 @@ class MainScreen { this.layout.addBtn.label = getIcon('add') + ' ' + locale.time(new Date(), 1).trim(); this.layout.render(this.layout.buttons); - // Auto-update time of day indication on log-add button upon next minute + // Auto-update time of day indication on log-add button upon + // next minute if (!this.buttonTimeoutId) { this.buttonTimeoutId = setTimeout( () => { @@ -277,7 +277,7 @@ class MainScreen { } - getTouchListeners() { + _initTouch() { let distanceY = null; function dragHandler(ev) { @@ -300,9 +300,8 @@ class MainScreen { } } - return { - 'drag': dragHandler.bind(this), - }; + this.listeners.drag = dragHandler.bind(this); + Bangle.on('drag', this.listeners.drag); } // Add current timestamp to log and update UI display @@ -335,12 +334,12 @@ class MainScreen { function saveErrorAlert() { currentUI.stop(); + // Not `showAlert` because the icon plus message don't fit the + // screen well E.showPrompt( 'Trouble saving timestamp log; data may be lost!', {title: "Can't save log", - img: '', - buttons: {'Ok': true}, - } + buttons: {'Ok': true}} ).then(currentUI.start.bind(currentUI)); } From 1307d3b21c76bb23ac31ec8c8d3e991422867c75 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Aug 2023 21:53:03 -0500 Subject: [PATCH 0017/2187] Implement scroll bar for log display --- apps/stamplog/app.js | 84 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 02698cdaa..2e05dddc7 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -8,6 +8,9 @@ const LOG_FILENAME = 'stamplog.json'; // Min number of pixels of movement to recognize a touchscreen drag/swipe const DRAG_THRESHOLD = 10; +// Width of scroll indicators +const SCROLL_BAR_WIDTH = 12; + var settings = { logItemFont: '12x20' }; @@ -162,6 +165,42 @@ function renderLogItem(elem) { } } +// Render a scroll indicator +// `scroll` format: { +// pos: int, +// min: int, +// max: int, +// itemsPerPage: int, +// } +function renderScrollBar(elem, scroll) { + const border = 1; + const boxArea = elem.h - 2 * border; + const boxSize = E.clip( + Math.round( + scroll.itemsPerPage / (scroll.max - scroll.min + 1) * (elem.h - 2) + ), + 3, + boxArea + ); + const boxTop = (scroll.max - scroll.min) ? + Math.round( + (scroll.pos - scroll.min) / (scroll.max - scroll.min) + * (boxArea - boxSize) + elem.y + border + ) : elem.y + border; + + // Draw border + g.setColor(g.theme.fg) + .fillRect(elem.x, elem.y, elem.x + elem.w - 1, elem.y + elem.h - 1) + // Draw scroll box area + .setColor(g.theme.bg) + .fillRect(elem.x + border, elem.y + border, + elem.x + elem.w - border - 1, elem.y + elem.h - border - 1) + // Draw scroll box + .setColor(g.blendColor(g.theme.bg, g.theme.fg, 0.5)) + .fillRect(elem.x + border, boxTop, + elem.x + elem.w - border - 1, boxTop + boxSize - 1); +} + // Main app screen interface, launched by calling start() class MainScreen { @@ -207,14 +246,21 @@ class MainScreen { // vertical screen space {type: '', id: 'placeholder', fillx: 1, filly: 1}, - {type: 'v', - id: 'logItems', + {type: 'h', + c: [ + {type: 'v', + id: 'logItems', - // To be filled in with log item elements once we determine - // how many will fit on screen - c: [], + // To be filled in with log item elements once we + // determine how many will fit on screen + c: [], + }, + {type: 'custom', + id: 'logScroll', + render: elem => { renderScrollBar(elem, this.logScrollInfo()); } + }, + ], }, - {type: 'h', id: 'buttons', c: [ @@ -241,6 +287,8 @@ class MainScreen { {type: 'custom', render: renderLogItem, item: undefined, fillx: 1, height: logItemHeight} ); } + layout.logScroll.height = logItemHeight * this.logItemsPerPage; + layout.logScroll.width = SCROLL_BAR_WIDTH; layout.update(); this.layout = layout; @@ -256,6 +304,7 @@ class MainScreen { elem.item = this.stampLog.log[logIdx]; } this.layout.render(layLogItems); + this.layout.render(this.layout.logScroll); } if (!item || item == 'buttons') { @@ -311,23 +360,32 @@ class MainScreen { this.render('log'); } + // Get scroll information for log display + logScrollInfo() { + return { + pos: this.logScrollPos, + min: (this.stampLog.log.length - 1) % this.logItemsPerPage, + max: this.stampLog.log.length - 1, + itemsPerPage: this.logItemsPerPage + }; + } + // Scroll display in given direction or to given position: // 'u': up, 'd': down, 't': to top, 'b': to bottom scrollLog(how) { - top = (this.stampLog.log.length - 1) % this.logItemsPerPage; - bottom = this.stampLog.log.length - 1; + let scroll = this.logScrollInfo(); if (how == 'u') { - this.logScrollPos -= this.logItemsPerPage; + this.logScrollPos -= scroll.itemsPerPage; } else if (how == 'd') { - this.logScrollPos += this.logItemsPerPage; + this.logScrollPos += scroll.itemsPerPage; } else if (how == 't') { - this.logScrollPos = top; + this.logScrollPos = scroll.min; } else if (how == 'b') { - this.logScrollPos = bottom; + this.logScrollPos = scroll.max; } - this.logScrollPos = E.clip(this.logScrollPos, top, bottom); + this.logScrollPos = E.clip(this.logScrollPos, scroll.min, scroll.max); } } From 60a99aae53cc28436d6eac7bd086138eb3e5dfb0 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sat, 2 Sep 2023 23:25:54 +0200 Subject: [PATCH 0018/2187] Widget size no longer changes, so remove associated code. --- apps/widbat/widget.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/widbat/widget.js b/apps/widbat/widget.js index f87452893..8262537e8 100644 --- a/apps/widbat/widget.js +++ b/apps/widbat/widget.js @@ -1,10 +1,6 @@ (function(){ - function setWidth() { - WIDGETS["bat"].width = 40 + (Bangle.isCharging()?16:0); - } Bangle.on('charging',function(charging) { if(charging) Bangle.buzz(); - setWidth(); Bangle.drawWidgets(); // re-layout widgets g.flip(); }); @@ -40,5 +36,4 @@ g.setColor(g.theme.fg).fillPoly(flash); } }}; - setWidth(); })() From b64806e96a2f3cbabb7b2ba5ffc5f6b7328471e4 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Aug 2023 21:55:40 -0500 Subject: [PATCH 0019/2187] Shorten some names --- apps/stamplog/app.js | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 2e05dddc7..c979bf349 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -208,8 +208,8 @@ class MainScreen { this.stampLog = stampLog; // Values set up by start() - this.logItemsPerPage = null; - this.logScrollPos = null; + this.itemsPerPage = null; + this.scrollPos = null; this.layout = null; // Handlers/listeners @@ -220,7 +220,7 @@ class MainScreen { // Launch this UI and make it live start() { this._initLayout(); - this.scrollLog('b'); + this.scroll('b'); this.render(); this._initTouch(); @@ -257,7 +257,7 @@ class MainScreen { }, {type: 'custom', id: 'logScroll', - render: elem => { renderScrollBar(elem, this.logScrollInfo()); } + render: elem => { renderScrollBar(elem, this.scrollInfo()); } }, ], }, @@ -279,15 +279,15 @@ class MainScreen { let availableHeight = layout.placeholder.h; g.setFont(settings.logItemFont); let logItemHeight = g.getFontHeight() * 2; - this.logItemsPerPage = Math.floor(availableHeight / logItemHeight); + this.itemsPerPage = Math.floor(availableHeight / logItemHeight); // Populate log items in layout - for (i = 0; i < this.logItemsPerPage; i++) { + for (i = 0; i < this.itemsPerPage; i++) { layout.logItems.c.push( {type: 'custom', render: renderLogItem, item: undefined, fillx: 1, height: logItemHeight} ); } - layout.logScroll.height = logItemHeight * this.logItemsPerPage; + layout.logScroll.height = logItemHeight * this.itemsPerPage; layout.logScroll.width = SCROLL_BAR_WIDTH; layout.update(); @@ -298,7 +298,7 @@ class MainScreen { render(item) { if (!item || item == 'log') { let layLogItems = this.layout.logItems; - let logIdx = this.logScrollPos - this.logItemsPerPage; + let logIdx = this.scrollPos - this.itemsPerPage; for (let elem of layLogItems.c) { logIdx++; elem.item = this.stampLog.log[logIdx]; @@ -342,7 +342,7 @@ class MainScreen { } else { // Drag ended if (Math.abs(distanceY) > DRAG_THRESHOLD) { - this.scrollLog(distanceY > 0 ? 'u' : 'd'); + this.scroll(distanceY > 0 ? 'u' : 'd'); this.render('log'); } distanceY = null; @@ -356,36 +356,36 @@ class MainScreen { // Add current timestamp to log and update UI display addTimestamp() { this.stampLog.addEntry(); - this.scrollLog('b'); + this.scroll('b'); this.render('log'); } // Get scroll information for log display - logScrollInfo() { + scrollInfo() { return { - pos: this.logScrollPos, - min: (this.stampLog.log.length - 1) % this.logItemsPerPage, + pos: this.scrollPos, + min: (this.stampLog.log.length - 1) % this.itemsPerPage, max: this.stampLog.log.length - 1, - itemsPerPage: this.logItemsPerPage + itemsPerPage: this.itemsPerPage }; } // Scroll display in given direction or to given position: // 'u': up, 'd': down, 't': to top, 'b': to bottom - scrollLog(how) { - let scroll = this.logScrollInfo(); + scroll(how) { + let scroll = this.scrollInfo(); if (how == 'u') { - this.logScrollPos -= scroll.itemsPerPage; + this.scrollPos -= scroll.itemsPerPage; } else if (how == 'd') { - this.logScrollPos += scroll.itemsPerPage; + this.scrollPos += scroll.itemsPerPage; } else if (how == 't') { - this.logScrollPos = scroll.min; + this.scrollPos = scroll.min; } else if (how == 'b') { - this.logScrollPos = scroll.max; + this.scrollPos = scroll.max; } - this.logScrollPos = E.clip(this.logScrollPos, scroll.min, scroll.max); + this.scrollPos = E.clip(this.scrollPos, scroll.min, scroll.max); } } From 12f3a46a7e61598f7fabab5c4b9a4957b3ef72b9 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Aug 2023 21:58:10 -0500 Subject: [PATCH 0020/2187] Auto render log when scroll() called --- apps/stamplog/app.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index c979bf349..0b74fc2e1 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -221,7 +221,7 @@ class MainScreen { start() { this._initLayout(); this.scroll('b'); - this.render(); + this.render('buttons'); this._initTouch(); } @@ -343,7 +343,6 @@ class MainScreen { // Drag ended if (Math.abs(distanceY) > DRAG_THRESHOLD) { this.scroll(distanceY > 0 ? 'u' : 'd'); - this.render('log'); } distanceY = null; } @@ -357,7 +356,6 @@ class MainScreen { addTimestamp() { this.stampLog.addEntry(); this.scroll('b'); - this.render('log'); } // Get scroll information for log display @@ -386,6 +384,8 @@ class MainScreen { } this.scrollPos = E.clip(this.scrollPos, scroll.min, scroll.max); + + this.render('log'); } } From e8fc765943316d89dbe449f4257d5ac3f8540764 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sun, 27 Aug 2023 22:28:05 -0500 Subject: [PATCH 0021/2187] Improve scrolling: haptic feedback, with multiple steps per swipe available --- apps/stamplog/app.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 0b74fc2e1..4776e173f 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -6,7 +6,7 @@ storage = require('Storage'); const LOG_FILENAME = 'stamplog.json'; // Min number of pixels of movement to recognize a touchscreen drag/swipe -const DRAG_THRESHOLD = 10; +const DRAG_THRESHOLD = 30; // Width of scroll indicators const SCROLL_BAR_WIDTH = 12; @@ -340,10 +340,13 @@ class MainScreen { distanceY += ev.dy; } } else { - // Drag ended - if (Math.abs(distanceY) > DRAG_THRESHOLD) { - this.scroll(distanceY > 0 ? 'u' : 'd'); - } + // Drag released + distanceY = null; + } + if (Math.abs(distanceY) > DRAG_THRESHOLD) { + // Scroll threshold reached + Bangle.buzz(50, .2); + this.scroll(distanceY > 0 ? 'u' : 'd'); distanceY = null; } } From 43727c1fdbeab688aaf020e1bdaa9cc75d6e5b48 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 2 Sep 2023 17:14:50 -0500 Subject: [PATCH 0022/2187] Implement fixed log size (new entries replace old) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fix saving log to Storage file named “undefined” instead of correct name --- apps/stamplog/app.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 4776e173f..71269fb43 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -12,7 +12,8 @@ const DRAG_THRESHOLD = 30; const SCROLL_BAR_WIDTH = 12; var settings = { - logItemFont: '12x20' + logItemFont: '12x20', + maxLogLength: 6 }; @@ -68,9 +69,12 @@ function getIcon(id) { // UI for managing log entries and automatically loading/saving // changes to flash storage. class StampLog { - constructor(filename) { + constructor(filename, maxLength) { // Name of file to save log to this.filename = filename; + // Maximum entries for log before old entries are overwritten with + // newer ones + this.maxLength = maxLength; // `true` when we have changes that need to be saved this.isDirty = false; @@ -130,6 +134,13 @@ class StampLog { // Add a timestamp for the current time to the end of the log addEntry() { + // If log full, purge an old entry to make room for new one + if (this.maxLength) { + while (this.log.length + 1 > this.maxLength) { + this.log.shift(); + } + } + // Add new entry this.log.push({ stamp: new Date() }); @@ -408,7 +419,7 @@ function saveErrorAlert() { Bangle.loadWidgets(); Bangle.drawWidgets(); -stampLog = new StampLog(); +stampLog = new StampLog(LOG_FILENAME, settings.maxLogLength); E.on('kill', stampLog.save.bind(stampLog)); stampLog.on('saveError', saveErrorAlert); From a4a5e1eaf62d8571083584dd36ea5c3b15bddec5 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Sat, 2 Sep 2023 18:19:45 -0500 Subject: [PATCH 0023/2187] Fix scrollbar geometry Nitpick: scrollbar box was larger than the true percentage of screen pages displayed because we weren't taking into account that we only scroll by full screens at a time. --- apps/stamplog/app.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 71269fb43..7b5975f5f 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -268,7 +268,7 @@ class MainScreen { }, {type: 'custom', id: 'logScroll', - render: elem => { renderScrollBar(elem, this.scrollInfo()); } + render: elem => { renderScrollBar(elem, this.scrollBarInfo()); } }, ], }, @@ -382,6 +382,25 @@ class MainScreen { }; } + // Like scrollInfo, but adjust the data so as to suggest scrollbar + // geometry that accurately reflects the nature of the scrolling + // (page by page rather than item by item) + scrollBarInfo() { + const info = this.scrollInfo(); + + function toPage(scrollPos) { + return Math.floor(scrollPos / info.itemsPerPage); + } + + return { + // Define 1 “screenfull” as the unit here + itemsPerPage: 1, + pos: toPage(info.pos), + min: toPage(info.min), + max: toPage(info.max), + }; + } + // Scroll display in given direction or to given position: // 'u': up, 'd': down, 't': to top, 'b': to bottom scroll(how) { From a741e86a4145e51f539f7f98bf51622ab6ae24d6 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 4 Sep 2023 22:39:06 -0500 Subject: [PATCH 0024/2187] Avoid UTF-8 for now since there are encoding problems with the IDE --- apps/stamplog/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 7b5975f5f..1d54d8e32 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -393,7 +393,7 @@ class MainScreen { } return { - // Define 1 “screenfull” as the unit here + // Define 1 "screenfull" as the unit here itemsPerPage: 1, pos: toPage(info.pos), min: toPage(info.min), From f4b3dd78d82eb7967f303bfb1e77cc6f9105b0a7 Mon Sep 17 00:00:00 2001 From: Travis Evans Date: Mon, 4 Sep 2023 23:11:32 -0500 Subject: [PATCH 0025/2187] Change some default configuration details --- apps/stamplog/app.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/stamplog/app.js b/apps/stamplog/app.js index 1d54d8e32..344f183c8 100644 --- a/apps/stamplog/app.js +++ b/apps/stamplog/app.js @@ -3,7 +3,7 @@ locale = require('locale'); storage = require('Storage'); // Storage filename to store user's timestamp log -const LOG_FILENAME = 'stamplog.json'; +const LOG_FILENAME = 'timestamplog.json'; // Min number of pixels of movement to recognize a touchscreen drag/swipe const DRAG_THRESHOLD = 30; @@ -13,7 +13,7 @@ const SCROLL_BAR_WIDTH = 12; var settings = { logItemFont: '12x20', - maxLogLength: 6 + maxLogLength: 30 }; @@ -444,4 +444,3 @@ stampLog.on('saveError', saveErrorAlert); var currentUI = new MainScreen(stampLog); currentUI.start(); - From f6f40b70177fc02224e2c6245d4f97ac774c3564 Mon Sep 17 00:00:00 2001 From: Alex Meyer Date: Tue, 5 Sep 2023 22:51:12 -0400 Subject: [PATCH 0026/2187] add repeatCalls option --- apps/messages/ChangeLog | 1 + apps/messages/lib.js | 4 +++- apps/messages/metadata.json | 2 +- apps/messages/settings.js | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 416363c45..5f4a81d1f 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -4,3 +4,4 @@ 0.58: show/hide "messages" widget directly, instead of through library stub 0.59: fixes message timeout by using setinterval, as it was intended. So the buzz is triggered every x seconds until the timeout occours. 0.60: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called +0.61: Add repeatCalls option to allow different repeat settings for messages vs calls diff --git a/apps/messages/lib.js b/apps/messages/lib.js index f3ae253e6..59a0feea5 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -204,16 +204,18 @@ exports.buzz = function(msgSrc) { if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return Promise.resolve(); // never buzz during Quiet Mode const msgSettings = require("Storage").readJSON("messages.settings.json", true) || {}; let pattern; + let repeat; if (msgSrc && msgSrc.toLowerCase()==="phone") { // special vibration pattern for incoming calls pattern = msgSettings.vibrateCalls; + repeat = msgSettings.repeatCalls; } else { pattern = msgSettings.vibrate; + repeat = msgSettings.repeat; } if (pattern===undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here if (!pattern) return Promise.resolve(); - let repeat = msgSettings.repeat; if (repeat===undefined) repeat = 4; // repeat may be zero if (repeat) { diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index e8aacd976..0ae661fd2 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,7 +1,7 @@ { "id": "messages", "name": "Messages", - "version": "0.60", + "version": "0.61", "description": "Library to handle, load and store message events received from Android/iOS", "icon": "app.png", "type": "module", diff --git a/apps/messages/settings.js b/apps/messages/settings.js index 09c9db455..c54c6b24e 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -6,6 +6,7 @@ if (settings.vibrate===undefined) settings.vibrate=":"; if (settings.vibrateCalls===undefined) settings.vibrateCalls=":"; if (settings.repeat===undefined) settings.repeat=4; + if (settings.repeatCalls===undefined) settings.repeatCalls=4; if (settings.vibrateTimeout===undefined) settings.vibrateTimeout=60; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; if (settings.maxMessages===undefined) settings.maxMessages=3; @@ -33,6 +34,12 @@ format: v => v?v+"s":/*LANG*/"Off", onchange: v => updateSetting("repeat", v) }, + /*LANG*/'Repeat for calls': { + value: settings().repeatCalls, + min: 0, max: 10, + format: v => v?v+"s":/*LANG*/"Off", + onchange: v => updateSetting("repeatCalls", v) + }, /*LANG*/'Vibrate timer': { value: settings().vibrateTimeout, min: 0, max: settings().maxUnreadTimeout, step : 10, From a7adfe74a6bbaa7094399030e2d361583a8b8d1d Mon Sep 17 00:00:00 2001 From: Alex Meyer Date: Tue, 5 Sep 2023 23:08:42 -0400 Subject: [PATCH 0027/2187] default to existing repeat value --- apps/messages/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/messages/settings.js b/apps/messages/settings.js index c54c6b24e..fa9fbe4f2 100644 --- a/apps/messages/settings.js +++ b/apps/messages/settings.js @@ -6,7 +6,7 @@ if (settings.vibrate===undefined) settings.vibrate=":"; if (settings.vibrateCalls===undefined) settings.vibrateCalls=":"; if (settings.repeat===undefined) settings.repeat=4; - if (settings.repeatCalls===undefined) settings.repeatCalls=4; + if (settings.repeatCalls===undefined) settings.repeatCalls=settings.repeat; if (settings.vibrateTimeout===undefined) settings.vibrateTimeout=60; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; if (settings.maxMessages===undefined) settings.maxMessages=3; From 606057310a8dde3ef33927b9d095af8dda2764d6 Mon Sep 17 00:00:00 2001 From: thyttan <6uuxstm66@mozmail.com⁩> Date: Wed, 6 Sep 2023 14:17:00 +0200 Subject: [PATCH 0028/2187] fastreset: shorten the timeout to execution --- apps/fastreset/ChangeLog | 1 + apps/fastreset/README.md | 6 +++--- apps/fastreset/boot.js | 4 ++-- apps/fastreset/metadata.json | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/fastreset/ChangeLog b/apps/fastreset/ChangeLog index 5560f00bc..d777d8c28 100644 --- a/apps/fastreset/ChangeLog +++ b/apps/fastreset/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Shorten the timeout before executing to 250 ms. diff --git a/apps/fastreset/README.md b/apps/fastreset/README.md index 381d80cf5..44b855454 100644 --- a/apps/fastreset/README.md +++ b/apps/fastreset/README.md @@ -1,6 +1,6 @@ # Fast Reset -Reset the watch by holding the hardware button for half a second. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. +Reset the watch by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. Fast Reset was developed with the app history feature of 'Fastload Utils' in mind. If many apps are in the history stack, the user may want a fast way to exit directly to the clock face without using the firmwares reset function. @@ -12,11 +12,11 @@ Just install and it will run as boot code. If 'Fastload Utils' is installed fastloading will be used when possible. Otherwise a standard `load(.bootcde)` is used. -If the hardware button is held for longer the standard reset functionality of the firmware is executed as well (total 1.5 seconds). And eventually the watchdog will be kicked. +If the hardware button is held for longer the standard reset functionality of the firmware is executed as well. And eventually the watchdog will be kicked. ## Controls -Hold the hardware button for half a second to feel the buzz, loading the clock face. +Press the hardware button just a little longer than a click to feel the buzz, loading the clock face. ## Requests diff --git a/apps/fastreset/boot.js b/apps/fastreset/boot.js index 681a5ddb7..c099070e3 100644 --- a/apps/fastreset/boot.js +++ b/apps/fastreset/boot.js @@ -1,5 +1,5 @@ {let buzzTimeout; -setWatch((e)=>{ - if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);Bangle.showClock();}, 500); +setWatch((e)=>{ + if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);Bangle.showClock();}, 250); if (!e.state && buzzTimeout) clearTimeout(buzzTimeout);}, BTN,{repeat:true, edge:'both' });} diff --git a/apps/fastreset/metadata.json b/apps/fastreset/metadata.json index 098e0eeb1..2d817a91f 100644 --- a/apps/fastreset/metadata.json +++ b/apps/fastreset/metadata.json @@ -1,12 +1,12 @@ { "id": "fastreset", "name": "Fast Reset", "shortName":"Fast Reset", - "version":"0.01", - "description": "Reset the watch by holding the hardware button for half a second. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", + "version":"0.02", + "description": "Reset the watch by pressing the hardware button just a little bit longer than a click. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", "icon": "app.png", "type": "bootloader", "tags": "system", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"fastreset.boot.js","url":"boot.js"} From 22eec4a54633f48f64b25f1114dec8865bb6a68b Mon Sep 17 00:00:00 2001 From: Willems Davy Date: Thu, 7 Sep 2023 04:46:57 +0200 Subject: [PATCH 0029/2187] Initial version of waternet --- apps/waternet/ChangeLog | 1 + apps/waternet/README.md | 50 + apps/waternet/app-icon.js | 1 + apps/waternet/app.js | 2743 +++++++++++++++++++++++++++++++ apps/waternet/app.png | Bin 0 -> 740 bytes apps/waternet/metadata.json | 16 + apps/waternet/roslid-finish.png | Bin 0 -> 2822 bytes apps/waternet/roslid-legend.png | Bin 0 -> 3264 bytes apps/waternet/roslid-rotate.png | Bin 0 -> 2808 bytes apps/waternet/roslid-slide.png | Bin 0 -> 2857 bytes apps/waternet/rotate-finish.png | Bin 0 -> 2502 bytes apps/waternet/rotate-legend.png | Bin 0 -> 2321 bytes apps/waternet/rotate.png | Bin 0 -> 2279 bytes apps/waternet/screenshot1.png | Bin 0 -> 2909 bytes apps/waternet/screenshot2.png | Bin 0 -> 3471 bytes apps/waternet/screenshot3.png | Bin 0 -> 2873 bytes apps/waternet/slide-finish.png | Bin 0 -> 2776 bytes apps/waternet/slide-legend.png | Bin 0 -> 3370 bytes apps/waternet/slide.png | Bin 0 -> 2777 bytes 19 files changed, 2811 insertions(+) create mode 100644 apps/waternet/ChangeLog create mode 100644 apps/waternet/README.md create mode 100644 apps/waternet/app-icon.js create mode 100644 apps/waternet/app.js create mode 100644 apps/waternet/app.png create mode 100644 apps/waternet/metadata.json create mode 100644 apps/waternet/roslid-finish.png create mode 100644 apps/waternet/roslid-legend.png create mode 100644 apps/waternet/roslid-rotate.png create mode 100644 apps/waternet/roslid-slide.png create mode 100644 apps/waternet/rotate-finish.png create mode 100644 apps/waternet/rotate-legend.png create mode 100644 apps/waternet/rotate.png create mode 100644 apps/waternet/screenshot1.png create mode 100644 apps/waternet/screenshot2.png create mode 100644 apps/waternet/screenshot3.png create mode 100644 apps/waternet/slide-finish.png create mode 100644 apps/waternet/slide-legend.png create mode 100644 apps/waternet/slide.png diff --git a/apps/waternet/ChangeLog b/apps/waternet/ChangeLog new file mode 100644 index 000000000..7e206be86 --- /dev/null +++ b/apps/waternet/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version of Waternet diff --git a/apps/waternet/README.md b/apps/waternet/README.md new file mode 100644 index 000000000..284c18a37 --- /dev/null +++ b/apps/waternet/README.md @@ -0,0 +1,50 @@ +# Waternet +This is the Bangle Js 2 version of my Waternet game which is a multiplatform puzzle game initially written for old consoles and handhelds. + +![screenshot 1](screenshot1.png) ![screenshot 2](screenshot2.png) ![screenshot 3](screenshot3.png) + +## Game Modes +The aim of the game, in any game mode is always to connect all pipes so that water can flow through them from the water point source. How you can accomplish this depends on the game mode. The game has a help section in the main menu where you can always check up on the rules of each game mode. + +### Rotate Mode +![rotate legend](rotate-legend.png) ![rotate finish](rotate-finish.png) ![rotate](rotate.png) + +You need to connect all the pipes so water flows through them, by pressing the A button on a pipe, to rotate the single pipe. + +### Slide Mode +![slide legend](slide-legend.png) ![slide finish](slide-finish.png) ![slide](slide.png) + +You need to connect all the pipes so water flows through them, by pressing the A on the arrows of a row or column. The row or column will move all pipes in the direction the arrow is pointing at. + +### Roslid Mode +![roslid legend](roslid-legend.png) ![roslid finish](roslid-finish.png) ![roslid rotate](roslid-rotate.png) ![roslid slide](roslid-slide.png) + +You need to connect all the pipes so water flows through them, by pressing the A on the arrows of a row or column. The row or column will move all pipes in the direction the arrow is pointing at. You can also press the A on a pipe, to rotate the single pipe. This is a combination Rotate and Slide mode. + +## Features +* 3 game modes (rotate, slide, roslid) +* 5 difficulties per game mode (very easy, easy, normal, hard, very hard) +* 375 levels spread over all difficulaties and game modes (25 levels per difficulty and game mode) +* Random mode to generate random levels for each game mode +* Touch controls + option to display up / down / left / right / middle rects of where to touch (to simulate a dpad and button) + +## Controls +| Button | Action | +|--------|--------------------------------------------------------------------------| +| Upper screen Touch | Move up in menus, Move selector up during gameplay | +| Lower screen Touch | Move down in menus, Move selector down during gameplay | +| Left screen Touch | Move selector left during gameplay | +| Right screen Touch | Move selector right during gameplay | +| Middle screen Touch | Confirm in menu and level selector, rotate or slide action while playing | +| Btn or long middle screen touch | Back in menus, level selector and game | + +## Requests +You can contact me on my [github](https://github.com/joyrider3774) + +## Credits +Waternet game concept is based on the Net and Netslide game from the [Simon Tatham's Portable Puzzle Collection](https://www.chiark.greenend.org.uk/~sgtatham/puzzles/), +it's my own re-implementation of these 2 game concepts mixed into one game + +## Creator + +Willems Davy diff --git a/apps/waternet/app-icon.js b/apps/waternet/app-icon.js new file mode 100644 index 000000000..bf67a764d --- /dev/null +++ b/apps/waternet/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgImjgf/4AFQAAP+AsApBvwFIwAFCAAYF/AqZrBgF/OIYFb/jojACQ")) diff --git a/apps/waternet/app.js b/apps/waternet/app.js new file mode 100644 index 000000000..b16510fd3 --- /dev/null +++ b/apps/waternet/app.js @@ -0,0 +1,2743 @@ +// -------------------------------------------------------------------------------------------------- +// images +// -------------------------------------------------------------------------------------------------- +const blockTiles = { + width : 8, height : 8, bpp : 1, + buffer : atob("JCTnAADnJCQAAP8AAOckJCQk5AQE5CQkAAD8BATkJCQkJOcAAP8AAAAA/wAA/wAAJCTkBAT8AAAAAPwEBPwAACQkJyAgJyQkAAA/ICAnJCQkJCQkJCQkJAAAPCQkJCQkJCQnICA/AAAAAD8gID8AACQkJCQkPAAAAAA8JCQ8AAA8PP////88PAAA/////zw8PDz8/Pz8PDwAAPz8/Pw8PDw8/////wAAAAD/////AAA8PPz8/PwAAAAA/Pz8/AAAPDw/Pz8/PDwAAD8/Pz88PDw8PDw8PDw8AAA8PDw8PDw8PD8/Pz8AAAAAPz8/PwAAPDw8PDw8AAAAADw8PDwAADx+/////348AH7/////fjw8fv7+/v5+PAB+/v7+/n48PH7/////fgAAfv////9+ADx+/v7+/n4AAH7+/v7+fgA8fn9/f39+PAB+f39/f348PH5+fn5+fjwAfn5+fn5+PDx+f39/f34AAH5/f39/fgA8fn5+fn5+AAB+fn5+fn4AAAAAAAAAAADMzDMzzMwzMwD/MzPMzDMzzs4yMs7OMjLMzDMzzMz/AExMc3NMTHNzAH9zc0xMc3MA/jIyzs4yMs7OMjLOzv4ATExzc0xMfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAAAQEBAQEBAQH/AQEBAQEBAQAAAAAAAAD//wAAAAAAAP8BAQEBAQEB//8BAQEBAQH/gICAgICAgID/gICAgICAgIGBgYGBgYGB/4GBgYGBgYGAgICAgICA//+AgICAgID/gYGBgYGBgf8AAAAAAAAAAMAiROgWIUIHQEJESBYhQgcAAAAAAAAAADx+ZmZmfjwAGBgYGBgYGAB8fAx8YHx8AHx8DHwMfHwADBw8bHwMDAB8fGB8DHx8AGBgfHxsfHwAfHwMGDAwMAB8fGx8bHx8AHx8bHwMfHwAfn5mfn5mZgB8fmZsZn58AHx8YGBgfHwAeHxmZmZ8eAB8fGB8YHx8AHx8YHh4YGAAPn5gbmZ+PABsbHx8bGxsABgYGBgYGBgAPj4MDGx8OABubnx8fG5uAGBgYGBgfHwAQWN3f2tjYwBmdn5uZmZmADh8bGxsfDgAeHxsfHhgYAA8fmZmZn4+AHh8bGx4bGwAPHxwOAx8eAB+fhgYGBgYAGxsbGxsfHwAbGxsbGx8OABjY2Nja382AGZmfhh+ZmYAZmZ+PBgYGAB8fAwYMHx8AAAwMAAwMAAAPEZaRlpGPAAIBPIBAfIECDxmWkJaWjwAfufDgefn535+9/OBgfP3fn7n5+eBw+d+fu/PgYHP7348bk5ubkY8ADxOdm5eRjwA") +}; + + +const congratsTiles = { + width : 8, height : 8, bpp : 1, + buffer : atob("PCQkJDwkJAA4JCQoJCQ4ADwgICAgIDwAOCQkJCQkOAA8ICA8ICA8ADwgIDggICAAHCAgLCQkHAAkJCQ8JCQkABAQEBAQEBAAHAgICAgoEAAkJCgwKCQkACAgICAgIDwAIjYqIiIiIgAkNCwkJCQkABgkJCQkJBgAOCQkJDggIAAYJCQkJCQcADgkJCQ4JCQAHCAgGAQEOAA+CAgICAgIACQkJCQkJDwAJCQkJCQkGAAiIiIiKjYiACQkJBgkJCQAIiIUHAgICAA8BAwYMCA8AAAAAAAAAAAA") +}; + + +const congratsScreen= { + width : 128, height : 64, bpp : 1, + buffer : atob("AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAADAAAwAAAAAA/gAAAAAAHAAwAOMAAAAAAMYcBgYDDBwAMGDgBgEAAADAPj8Pjz4+ZjHx4w8Px8AAwGY7H443HGYxuOMZjsfAAMBmMxmMHxxmMPjjGYzGAADGZjMZjD8cZjH44xmMx8AA5mYzGYx3HGYzuOMZjMDAAH5+Mx+MPx5+MfjjH4zHwAA8PDMPjD8MOjHYYw8Mx8AAAAAAA4AAAAAAAAAAAAAAAAAAAB8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiGCQAPCA8PDg8OAA+JDwAIiQkACAgICQkICQACCQgABQkJAAgICAkJCAkAAgkIAAcJCQAICA8JCQ8JAAIPDwACCQkACAgIDw4ICQACCQgAAgkJAAgICAkJCAkAAgkIAAIGDwAPDw8JCQ8OAAIJDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4HAAAAAAAAAAAAAAAAAAAOBwAAAAAAAAAAAAMAAAAADgcAAAAAAAAAAAAHgAAAAA4HAAAAAAAABgAADMAAAAAPDwAAAAAAAA8GABhAAAAADw8AAAYAAAAZjwARYAAAAAeeAAAPAAAAEJmAESAAAAAHngAAGYAAADLSwBCgBgAAA/wAABLAAAAhckAQIA8AAAP8AAAyQAAAIWFAECAZgAAB+AAAIUABgCBgQBAgEIAAAfgAACBAA8AgYEAQIDLAAACQAAAgQAZgIGBAGGAhQAAB+AAAIEAMICBgQAhAIUAABw4AACBACLAwYEAMwCBAAAwDAAAgQAiQEPBAB4AgQAAYAYAAIMAIUBmQwAMAIEAAEGCAADCACBAPGYADACBAADDgwAAZgAgQBg8AAgAgwAAgYEAADwAIEAYGAAIAMIAAIGBAAAYACBAEBgABABmAACBgQAAGAAwwBAQAAQAPAAAg8EAABAAEIAIEAAEABgAAMPDAAAQABmACAgABgAYAABAAgAACAAPAAgIAAIAEAAAYAYAAAgABgAMCAAAABAAADAMAAAIAAYABAwAAAAIAAAcOAAADAAEAAAEAAAACAAAB+AAAAQABAAAAAAAAAgAAAAAAAAAAAIAAA==") +}; + + + +const selectorTiles = { + width : 8, height : 8, bpp : 1, + transparent : 0, + buffer : atob("AAAAAAAAgYHAAAAAAAAAwIGBAAAAAAAAAwAAAAAAAAMAAAAAAACAwMCAAAAAAAAAAAAAAAAAAQMDAQAAAAAAAA==") +}; + +const title = { + width : 128, height : 40, bpp : 1, + buffer : atob("AAAAAAAAAAAAAAAAAAAAAAAHh8HAAAAAAAAAAAAAAAAAD4fDwAAAAAAAAAAAPAAAAA+Hw8AA+AAAAAAAAHwAAAAPj8PAAPwAAAAAAAD8AAAAD4/Dw/n8Hwc7n4H4/gAAAA+Pw8f//3+He7/H/f+AAAAHj8OP//5/x/v/z/3/YAAAB8/Hj/38/+fz/8///iAAAAPO54w8+PHn4+PPHvwwAAADzOeAPPDx58PDzx54MAAAA/x/h/zw/+eDw8/+eCAAAAP8fw/88P/nh8PP/nhgAAAD/H4PHPDwB4/HzwB4wAAAAfx+Hhzw8AePx88AeMAAAAH4fh/8/v/Hj8fP/nzAAAAB+H4f/P7/x4/Hz/7+wAAAAfD+H/x+/8ePx8f+/sAAAADA+BwAYGAGDwcHAGBgAAAAQNg2AHBwDg2FhYB5IAAAAGCIMwDY/A8JxczAz+AAAAAhBOPjnIY5kMzY447gAAAAHgOAfgMB4OB4cB4PAAAAAAAAAAAAAAAAAAAAD4AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAfwAAAAAAAAAAAAAAAAAAAH+AAAAAAAAAAAAAAAAAAAD/4AAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAAf/AAAAAAAAAAAAAAAAAAAH/gAAAAAAAAAAAAAAAAAAA7wAAAAAAAAAAAAAAAAAAAH8AAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAA==") +}; + +// -------------------------------------------------------------------------------------------------- +// global variables and consts +// -------------------------------------------------------------------------------------------------- +//need to call this first otherwise +//Bangle.apprect is not updated and i can't calculate screenoffsety +Bangle.loadWidgets(); + +const debugMode = 0; +const debugModeRamUse = 0; +const scaleScreen = 1; + +const screenWidth = 176; +const screenHeight = 176; + +const screenOffsetX = ((screenWidth - 16 * 8) >> 1); +const screenOffsetY = ((screenHeight + Bangle.appRect.y - 8 * 8) >> 1); +const maxBoardWidth = 10; +const maxBoardHeight = 8; + +const maxBoardBgWidth = 10; +const maxBoardBgHeight = 8; + +const maxBoardSize = maxBoardWidth * maxBoardHeight; + +const tileSize = 8; + +const gsGame = 0; +const gsTitle = 1; +const gsLevelSelect = 2; +const gsLevelsCleared = 3; +const gsHelpRotate = 4; +const gsHelpRotate2 = 5; +const gsHelpRotate3 = 6; +const gsHelpRotateSlide = 7; +const gsHelpRotateSlide2 = 8; +const gsHelpRotateSlide3 = 9; +const gsHelpRotateSlide4 = 10; +const gsHelpSlide = 11; +const gsHelpSlide2 = 12; +const gsHelpSlide3 = 13; +const gsIntro = 14; + +const gsInitDiff = 50; + +const gsInitGame = gsInitDiff + gsGame; +const gsInitTitle = gsInitDiff + gsTitle; +const gsInitLevelSelect = gsInitDiff + gsLevelSelect; +const gsInitLevelsCleared = gsInitDiff + gsLevelsCleared; +const gsInitHelpRotate = gsInitDiff + gsHelpRotate; +const gsInitHelpRotate2 = gsInitDiff + gsHelpRotate2; +const gsInitHelpRotate3 = gsInitDiff + gsHelpRotate3; +const gsInitHelpRotateSlide = gsInitDiff + gsHelpRotateSlide; +const gsInitHelpRotateSlide2 = gsInitDiff + gsHelpRotateSlide2; +const gsInitHelpRotateSlide3 = gsInitDiff + gsHelpRotateSlide3; +const gsInitHelpRotateSlide4 = gsInitDiff + gsHelpRotateSlide4; +const gsInitHelpSlide = gsInitDiff + gsHelpSlide; +const gsInitHelpSlide2 = gsInitDiff + gsHelpSlide2; +const gsInitHelpSlide3 = gsInitDiff + gsHelpSlide3; +const gsInitIntro = gsInitDiff + gsIntro; + + +const diffVeryEasy = 0; +const diffEasy = 1; +const diffNormal = 2; +const diffHard = 3; +const diffVeryHard = 4; +const diffRandom = 5; +const diffCount = 6; + +const gmRotate = 0; +const gmSlide = 1; +const gmRotateSlide = 2; +const gmCount = 3; + +const mmStartGame = 0; +const mmHelp = 1; +const mmOptions = 2; +const mmCredits = 3; +const mmCount = 4; + +const opMusic = 0; +const opSound = 1; +const opCount = 2; + +const tsMainMenu = 0; +const tsGameMode = 1; +const tsDifficulty = 2; +const tsOptions = 3; +const tsCredits = 4; + +const levelCount = 25; + +const arrowDown = 122; +const arrowUp = 120; +const arrowLeft = 123; +const arrowRight = 121; +const leftMenu = 118; + +const frameRate = 15; + +var startPos; +var menuPos; +var maxLevel; +var selectedLevel; +var boardX; +var boardY; +var difficulty; +var gameState; +var boardWidth; +var boardHeight; +var boardSize; +var levelDone; +var titleStep; +var gameMode; +var posAdd; +var mainMenu; +var option; +var needRedraw; +var requiresFlip; +var selectionX, selectionY; +var moves; +var randomSeedGame; +var level = new Uint8Array(maxBoardSize); + +// Cursor +const maxCursorFrameCount = (10 * frameRate / 60); +const cursorAnimCount = 2; //blink on & off +const cursorNumTiles = 16; //for the max 2 cursors shown at once (on help screens) + +var cursorFrameCount, cursorFrame, showCursor; +var spritePos = []; +for (var i = 0; i >> 16) & 0xffff; + var al = a & 0xffff; + var bh = (b >>> 16) & 0xffff; + var bl = b & 0xffff; + // the shift by 0 fixes the sign on the high part + // the final |0 converts the unsigned value into a signed value + return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0)|0); +}; + +function cyrb128(str) { + let h1 = 1779033703, h2 = 3144134277, + h3 = 1013904242, h4 = 2773480762; + for (let i = 0, k; i < str.length; i++) { + k = str.charCodeAt(i); + h1 = h2 ^ Math.imul(h1 ^ k, 597399067); + h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); + h3 = h4 ^ Math.imul(h3 ^ k, 951274213); + h4 = h1 ^ Math.imul(h4 ^ k, 2716044179); + } + h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067); + h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233); + h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213); + h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179); + h1 ^= (h2 ^ h3 ^ h4); + h2 ^= h1; + h3 ^= h1; + h4 ^= h1; + return [h1>>>0, h2>>>0, h3>>>0, h4>>>0]; +} + +//based on code from pracrand https://pracrand.sourceforge.net/ (public domain) +function sfc32(a, b, c, d) { + return function() { + a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0; + var t = (a + b) | 0; + a = b ^ b >>> 9; + b = c + (c << 3) | 0; + c = (c << 21 | c >>> 11); + d = d + 1 | 0; + t = t + d | 0; + c = c + t | 0; + return (t >>> 0) / 4294967296; + }; +} + +function randomIntFromInterval(min, max) { // min and max included + return Math.floor(randfunc() * (max - min + 1) + min); +} + +function srand(seed) +{ + // Create cyrb128 state: + var aseed = cyrb128("applespairs" + seed.toString()); + // Four 32-bit component hashes provide the seed for sfc32. + randfunc = sfc32(aseed[0], aseed[1], aseed[2], aseed[3]); +} + +function random(value) +{ + return randomIntFromInterval(0,value-1); +} + + +// -------------------------------------------------------------------------------------------------- +// Cursor stuff +// -------------------------------------------------------------------------------------------------- + + +function move_sprite(sprite, x, y) +{ + spritePos[sprite][0] = x; + spritePos[sprite][1] = y; +} + +function drawCursors() +{ + if((showCursor == 0) || (cursorFrame & 1)) // 2nd or to add blink effect, it will skip drawing if bit 1 is set + return; + g.setColor(1,0,0); + for (var i=0; i= maxCursorFrameCount) + { + cursorFrame++; + cursorFrameCount = 0; + if (cursorFrame >= cursorAnimCount) + cursorFrame = 0; + return 1; + } + return 0; +} + +function hideCursors() +{ + //HIDE CURSOR SPRITES + //cursor 0 + setCursorPos(0, 0, (screenHeight / 8) + 1); + + //cursor 1 + setCursorPos(1, 0, (screenHeight / 8) + 1); + + showCursor = 0; +} + +function showCursors() +{ + showCursor = 1; +} + +function setCursorPos(cursorNr, xPos, yPos) +{ + if (cursorNr > 1) + return; + + move_sprite((cursorNr<<3) + 0, ((xPos) << 3), ((yPos - 1) << 3)); + move_sprite((cursorNr<<3) + 1, ((xPos + 1) << 3), ((yPos) << 3)); + move_sprite((cursorNr<<3) + 2, ((xPos) << 3), ((yPos + 1) << 3)); + move_sprite((cursorNr<<3) + 3, ((xPos - 1) << 3), ((yPos) << 3)); + //corners + move_sprite((cursorNr<<3) + 4, ((xPos + 1) << 3), ((yPos - 1) << 3)); + move_sprite((cursorNr<<3) + 5, ((xPos + 1) << 3), ((yPos + 1) << 3)); + move_sprite((cursorNr<<3) + 6, ((xPos - 1) << 3), ((yPos - 1) << 3)); + move_sprite((cursorNr<<3) + 7, ((xPos - 1) << 3), ((yPos + 1) << 3)); +} + +function initCursors() +{ + hideCursors(); + + cursorFrameCount = 0; + cursorFrame = 0; +} + +// -------------------------------------------------------------------------------------------------- +// helper funcs +// -------------------------------------------------------------------------------------------------- + +function set_bkg_tile_xy(x, y, tile) +{ + g.drawImage(currentTiles, screenOffsetX + x *8, screenOffsetY + y*8, {frame:tile}); //arduboy.drawBitmap(x * 8, y * 8, ¤tTiles[2 + (tile * 8)] , 8, 8); +} + +function set_bkg_data(tiles) +{ + currentTiles = tiles; +} + +function get_bkg_data() +{ + return currentTiles; +} + +function set_bkg_tiles(x, y, map) +{ + g.drawImage(map, screenOffsetX + x, screenOffsetY + y); //arduboy.drawBitmap(x, y, &map[2], pgm_read_byte(&map[0]), pgm_read_byte(&map[1])); +} + +function setBlockTilesAsBackground() +{ + set_bkg_data(blockTiles); +} + +// -------------------------------------------------------------------------------------------------- +// help screens +// -------------------------------------------------------------------------------------------------- + + +//LEGEND STATE +function inithelpLegend() +{ + setBlockTilesAsBackground(); + //SelectMusic(musTitle); + needRedraw = 1; +} + +//LEGEND STATE +function helpLegend(nextState) +{ + if ((gameState == gsInitHelpSlide) || + (gameState == gsInitHelpRotate) || + (gameState == gsInitHelpRotateSlide)) + { + inithelpLegend(); + gameState -= gsInitDiff; + } + + if (needRedraw) + { + g.clearRect(Bangle.appRect); + switch(gameState) + { + case gsHelpSlide: + printMessage(2, 0, "HELP: SLIDE"); + break; + case gsHelpRotate: + printMessage(2, 0, "HELP: ROTATE"); + break; + case gsHelpRotateSlide: + printMessage(2, 0, "HELP: ROSLID"); + break; + } + + set_bkg_tile_xy(0, 1, 33); + printMessage(1, 1, ":WATER SOURCE"); + set_bkg_tile_xy(0, 2, 11); + set_bkg_tile_xy(1, 2, 6); + set_bkg_tile_xy(2, 2, 12); + printMessage(3, 2, ":NOT FILLED"); + set_bkg_tile_xy(0, 3, 27); + set_bkg_tile_xy(1, 3, 22); + set_bkg_tile_xy(2, 3, 28); + printMessage(3, 3, ":FILLED"); + + if((gameState == gsHelpRotateSlide) || + (gameState == gsHelpSlide)) + { + set_bkg_tile_xy(0, 4, 121); + printMessage(1, 4, ":SLID ROW RIGHT"); + set_bkg_tile_xy(0, 5, 123); + printMessage(1, 5, ":SLID ROW LEFT"); + set_bkg_tile_xy(0, 6, 122); + printMessage(1, 6, ":SLID COL DOWN"); + set_bkg_tile_xy(0, 7, 120); + printMessage(1, 7, ":SLID COL UP"); + } + needRedraw = 0; + requiresFlip = 1; + } + if (btna) + { + //playMenuAcknowlege(); + gameState = nextState; + } +} + +//FINISH LEVEL STATE +function initHelpFinishLevel() +{ + setBlockTilesAsBackground(); + //SelectMusic(musTitle); + needRedraw = 1; +} + +//FINISH LEVEL STATE +function helpFinishLevel(nextState) +{ + if ((gameState == gsInitHelpSlide2) || + (gameState == gsInitHelpRotate2) || + (gameState == gsInitHelpRotateSlide2)) + { + initHelpFinishLevel(); + gameState -= gsInitDiff; + } + + if(needRedraw) + { + g.clearRect(Bangle.appRect); + switch(gameState) + { + case gsHelpSlide2: + printMessage(2, 0, "HELP: SLIDE"); + break; + case gsHelpRotate2: + printMessage(2, 0, "HELP: ROTATE"); + break; + case gsHelpRotateSlide2: + printMessage(2, 0, "HELP: ROSLID"); + break; + } + printMessage(0, 2, "LEVEL FINISH:"); + + if((gameState == gsHelpSlide2) || + (gameState == gsHelpRotateSlide2)) + { + //arrows top + set_bkg_tile_xy(2, 3, 122); + set_bkg_tile_xy(3, 3, 122); + set_bkg_tile_xy(4, 3, 122); + + //arrows left / right row 1 + set_bkg_tile_xy(1, 4, 121); + set_bkg_tile_xy(5, 4, 123); + + //arrows left / right row 2 + set_bkg_tile_xy(1, 5, 121); + set_bkg_tile_xy(5, 5, 123); + + //arrows left / right row 3 + set_bkg_tile_xy(1, 6, 121); + set_bkg_tile_xy(5, 6, 123); + + //arrows bottom + set_bkg_tile_xy(2, 7, 120); + set_bkg_tile_xy(3, 7, 120); + set_bkg_tile_xy(4, 7, 120); + } + + set_bkg_tile_xy(2, 4, 25); + set_bkg_tile_xy(3, 4, 23); + set_bkg_tile_xy(4, 4, 27); + printMessage(7, 4, "ALL WATER"); + + set_bkg_tile_xy(2, 5, 28); + set_bkg_tile_xy(3, 5, 33); + set_bkg_tile_xy(4, 5, 22); + printMessage(7, 5, "PIPES ARE"); + + set_bkg_tile_xy(2, 6, 29); + set_bkg_tile_xy(3, 6, 20); + set_bkg_tile_xy(4, 6, 23); + printMessage(7, 6, "FILLED"); + needRedraw = 0; + requiresFlip = 1; + } + + if (btna) + { + //playMenuAcknowlege(); + gameState = nextState; + } +} + +function initHelpDoSlideRotate() +{ + setBlockTilesAsBackground(); + //SelectMusic(musTitle); + + //DRAW CURSOR SPRITES + initCursors(); + + if((gameState == gsInitHelpRotateSlide4) || + (gameState == gsInitHelpSlide3)) + { + setCursorPos(0, 0, 5); + setCursorPos(1, 11, 5); + } + else + { + setCursorPos(0, 1, 4); + setCursorPos(1, 12, 4); + } + + showCursors(); + needRedraw = 1; +} + +function helpDoSlideRotate(nextState) +{ + if ((gameState == gsInitHelpSlide3) || + (gameState == gsInitHelpRotate3) || + (gameState == gsInitHelpRotateSlide3) || + (gameState == gsInitHelpRotateSlide4)) + { + initHelpDoSlideRotate(); + gameState -= gsInitDiff; + } + + if(needRedraw) + { + g.clearRect(Bangle.appRect); + + switch(gameState) + { + case gsHelpSlide3: + printMessage(2, 0, "HELP: SLIDE"); + break; + case gsHelpRotate3: + printMessage(2, 0, "HELP: ROTATE"); + break; + case gsHelpRotateSlide3: + case gsHelpRotateSlide4: + printMessage(2, 0, "HELP: ROSLID"); + break; + } + + if((gameState == gsHelpRotateSlide3) || + (gameState == gsHelpRotate3)) + printMessage(5, 2, "ROTATE"); + else + printMessage(6, 2, "SLIDE"); + + // 'A' + '=>' + set_bkg_tile_xy(6, 5, 119); + printMessage(5, 5, "TOUCH"); + set_bkg_tile_xy(10, 5, 118); + + if((gameState == gsHelpSlide3) || + (gameState == gsHelpRotateSlide3) || + (gameState == gsHelpRotateSlide4)) + { + //Top Arrows + set_bkg_tile_xy(1, 3, 122); + set_bkg_tile_xy(2, 3, 122); + set_bkg_tile_xy(3, 3, 122); + + //arrows 1st row + set_bkg_tile_xy(0, 4, 121); + set_bkg_tile_xy(4, 4, 123); + + //arrows 2nd row + set_bkg_tile_xy(0, 5, 121); + set_bkg_tile_xy(4, 5, 123); + + //arrows 3rd row + set_bkg_tile_xy(0, 6, 121); + set_bkg_tile_xy(4, 6, 123); + + //arrows bottom + set_bkg_tile_xy(1, 7, 120); + set_bkg_tile_xy(2, 7, 120); + set_bkg_tile_xy(3, 7, 120); + + //2nd grid + + //Top Arrows + set_bkg_tile_xy(12, 3, 122); + set_bkg_tile_xy(13, 3, 122); + set_bkg_tile_xy(14, 3, 122); + + //arrows 1st row + set_bkg_tile_xy(11, 4, 121); + set_bkg_tile_xy(15, 4, 123); + + //arrows 2nd row + set_bkg_tile_xy(11, 5, 121); + set_bkg_tile_xy(15, 5, 123); + + //arrows 3rd row + set_bkg_tile_xy(11, 6, 121); + set_bkg_tile_xy(15, 6, 123); + + //bottoms arrows + set_bkg_tile_xy(12, 7, 120); + set_bkg_tile_xy(13, 7, 120); + set_bkg_tile_xy(14, 7, 120); + } + + //1st grid + if ((gameState == gsHelpRotate3) || + (gameState == gsHelpRotateSlide3)) + { + set_bkg_tile_xy(1, 4, 12); + set_bkg_tile_xy(2, 4, 7); + set_bkg_tile_xy(3, 4, 27); + + set_bkg_tile_xy(1, 5, 28); + set_bkg_tile_xy(2, 5, 33); + set_bkg_tile_xy(3, 5, 22); + + set_bkg_tile_xy(1, 6, 29); + set_bkg_tile_xy(2, 6, 20); + set_bkg_tile_xy(3, 6, 23); + } + else + { + set_bkg_tile_xy(1, 4, 9); + set_bkg_tile_xy(2, 4, 7); + set_bkg_tile_xy(3, 4, 11); + + set_bkg_tile_xy(1, 5, 17); + set_bkg_tile_xy(2, 5, 38); + set_bkg_tile_xy(3, 5, 12); + + set_bkg_tile_xy(1, 6, 13); + set_bkg_tile_xy(2, 6, 4); + set_bkg_tile_xy(3, 6, 7); + } + + + //2nd grid + + set_bkg_tile_xy(12, 4, 25); + set_bkg_tile_xy(13, 4, 23); + set_bkg_tile_xy(14, 4, 27); + + set_bkg_tile_xy(12, 5, 28); + set_bkg_tile_xy(13, 5, 33); + set_bkg_tile_xy(14, 5, 22); + + set_bkg_tile_xy(12, 6, 29); + set_bkg_tile_xy(13, 6, 20); + set_bkg_tile_xy(14, 6, 23); + + drawCursors(); + needRedraw = 0; + requiresFlip = 1; + } + + //needRedraw = updateCursorFrame(); + + if (btna) + { + //playMenuAcknowlege(); + gameState = nextState; + hideCursors(); + } +} + + +//LEGEND STATE +function helpRotateSlide() +{ + helpLegend(gsInitHelpRotateSlide2); +} + +//FINISH LEVEL STATE +function helpRotateSlide2() +{ + helpFinishLevel(gsInitHelpRotateSlide3); +} + +//SLIDE STATE +function helpRotateSlide3() +{ + helpDoSlideRotate(gsInitHelpRotateSlide4); +} + +//ROTATE STATE +function helpRotateSlide4() +{ + helpDoSlideRotate(gsInitTitle); +} + +function helpRotate() +{ + helpLegend(gsInitHelpRotate2); +} + +//FINISH LEVEL STATE +function helpRotate2() +{ + helpFinishLevel(gsInitHelpRotate3); +} + +//ROTATE STATE +function helpRotate3() +{ + helpDoSlideRotate(gsInitTitle); +} + +//LEGEND STATE +function helpSlide() +{ + helpLegend(gsInitHelpSlide2); +} + +//FINISH LEVEL STATE +function helpSlide2() +{ + helpFinishLevel(gsInitHelpSlide3); +} + +//SLIDE STATE +function helpSlide3() +{ + helpDoSlideRotate(gsInitTitle); +} + +// -------------------------------------------------------------------------------------------------- +// Intro +// -------------------------------------------------------------------------------------------------- + +function initIntro() +{ + setBlockTilesAsBackground(); + titlePosY = g.getHeight(); + frames = 0; +} + +function intro() +{ + if (gameState == gsInitIntro) + { + initIntro(); + gameState -= gsInitDiff; + } + + frames++; + g.clearRect(Bangle.appRect); + if (frames < frameDelay) + { + //16-12 + printMessage(4 >> 1, 4, "WILLEMS DAVY"); + requiresFlip = 1; + } + else + { + if (frames < frameDelay *2) + { + //16-8 + printMessage(8 >> 1, 4, "PRESENTS"); + requiresFlip = 1; + } + else + { + requiresFlip = 1; + g.drawImage(title, screenOffsetX, titlePosY);//arduboy.drawCompressed(0, (uint16_t)titlePosY, titlescreenMap); + if(titlePosY > screenOffsetY) + { + titlePosY -= 60/frameRate; + } + else + { + gameState = gsInitTitle; + } + } + } + + if (btna || btnb) + { + gameState = gsInitTitle; + } +} + +// -------------------------------------------------------------------------------------------------- +// Level Stuff +// -------------------------------------------------------------------------------------------------- + + +function moveBlockDown(aTile) +{ + var tmp = level[aTile + boardSize - boardWidth]; + for (var i= boardSize - boardWidth; i != 0 ; i -= boardWidth) + level[aTile + i] = level[aTile + i -boardWidth]; + level[aTile] = tmp; +} + +function moveBlockUp(aTile) +{ + var tmp = level[aTile - boardSize + boardWidth]; + for (var i= boardSize - boardWidth; i != 0; i -= boardWidth) + level[aTile - i] = level[aTile - i + boardWidth]; + level[aTile] = tmp; +} + +function moveBlockRight(aTile) +{ + var tmp = level[aTile + boardWidth - 1]; + for (var i= 0; i < boardWidth -1; i++) + level[aTile + boardWidth - 1 - i] = level[aTile + boardWidth - 2 - i]; + level[aTile] = tmp; +} + +function moveBlockLeft(aTile) +{ + var tmp = level[aTile - boardWidth + 1]; + for (var i= 0; i < boardWidth-1; i++) + level[aTile - boardWidth + 1 + i] = level[aTile - boardWidth + 2 + i]; + level[aTile] = tmp; +} + +//rotates a tile by change the tilenr in the level +//there are 16 tiles per set and there are 3 sets no water, water filled, and special start tiles +function rotateBlock(aTile) +{ + switch (level[aTile]) + { + case 1: + case 17: + case 33: + level[aTile] = 2; + break; + case 2: + case 18: + case 34: + level[aTile] = 4; + break; + case 3: + case 19: + case 35: + level[aTile] = 6; + break; + case 4: + case 20: + case 36: + level[aTile] = 8; + break; + case 5: + case 21: + case 37: + level[aTile] = 10; + break; + case 6: + case 22: + case 38: + level[aTile] = 12; + break; + case 7: + case 23: + case 39: + level[aTile] = 14; + break; + case 8: + case 24: + case 40: + level[aTile] = 1; + break; + case 9: + case 25: + case 41: + level[aTile] = 3; + break; + case 10: + case 26: + case 42: + level[aTile] = 5; + break; + case 11: + case 27: + case 43: + level[aTile] = 7; + break; + case 12: + case 28: + case 44: + level[aTile] = 9; + break; + case 13: + case 29: + case 45: + level[aTile] = 11; + break; + case 14: + case 30: + case 46: + level[aTile] = 13; + break; + default: + break; + } +} + +function shuffleSlide(aTile) +{ + var rnd = random(3); + switch (rnd) + { + case 0: + moveBlockUp((aTile % boardWidth) + boardSize - boardWidth); + break; + case 1: + moveBlockDown((aTile % boardWidth)); + break; + case 2: + moveBlockLeft(boardWidth - 1 + aTile -(aTile % boardWidth)); + break; + case 3: + moveBlockRight(aTile - (aTile % boardWidth)); + break; + } +} + +function shuffleRotate(aTile) +{ + var rnd = random(3); + for (var i = 0; i < rnd; i++) + rotateBlock(aTile); +} + +function shuffleLevel() +{ + var rnd; + var j = 0; + while(j < boardSize) + { + switch(gameMode) + { + case gmRotate: + shuffleRotate(j); + j++; + break; + case gmSlide: + shuffleSlide(j); + //for speed up it should be fine as all slide levels are uneven in width / height (except random) + j+=2; + break; + case gmRotateSlide: + rnd = random(2); + if(rnd == 0) + { + shuffleSlide(j); + //for speed up + j+=2; + } + else + { + shuffleRotate(j); + j++; + } + break; + } + } +} + +function handleConnectPoint(currentPoint, cellStack, cc) +{ + var lookUpX = currentPoint % boardWidth; + var lookUpY = Math.floor(currentPoint / boardWidth); + var tmp; + var tmp2; + if ((lookUpY> 0) && (!(level[currentPoint] & 1))) + { + tmp = currentPoint - boardWidth; + tmp2 = level[tmp]; + if (((tmp2 < 16) && (!(tmp2 & 4)) ) || + ((tmp2 > 15) && (!((tmp2 - 16) & 4)))) + { + //adapt tile to filled tile + if(level[currentPoint] < 16) + { + level[currentPoint] += 16; + } + + //add neighbour to cellstack of to handle tiles + if (tmp2 < 16) + { + cellStack[cc++] = tmp; + } + } + + } + + //if tile has passage to the east and east neigbour passage to the west + if ((lookUpX + 1 < boardWidth) && (!(level[currentPoint] & 2))) + { + tmp = currentPoint + 1; + tmp2 = level[tmp]; + if (((tmp2 < 16) && (!(tmp2 & 8))) || + ((tmp2 > 15) && (!((tmp2 - 16) & 8)))) + { + //adapt tile to filled tile + if(level[currentPoint] < 16) + { + level[currentPoint] += 16; + } + + //add neighbour to cellstack of to handle tiles + if (tmp2 < 16) + { + cellStack[cc++] = tmp; + } + + } + } + + //if tile has passage to the south and south neigbour passage to the north + if ((lookUpY + 1 < boardHeight) && (!(level[currentPoint] & 4 ))) + { + tmp = currentPoint + boardWidth; + tmp2 = level[tmp]; + if (((tmp2 < 16) && (!(tmp2 & 1))) || + ((tmp2 > 15) && (!((tmp2 - 16) & 1)))) + { + //adapt tile to filled tile + if(level[currentPoint] < 16) + { + level[currentPoint] += 16; + } + + //add neighbour to cellstack of to handle tiles + if (tmp2 < 16) + { + cellStack[cc++] = tmp; + } + } + } + + //if tile has passage to the west and west neigbour passage to the east + if ((lookUpX > 0) && (!(level[currentPoint] & 8))) + { + tmp = currentPoint - 1; + tmp2 = level[tmp]; + if (((tmp2 < 16) && (!(tmp2 & 2))) || + ((tmp2 > 15) && (!((tmp2 - 16) & 2)))) + { + //adapt tile to filled tile + if(level[currentPoint] < 16) + { + level[currentPoint] += 16; + } + + //add neighbour to cellstack of to handle tiles + if(tmp2 < 16) + { + cellStack[cc++] = tmp; + } + } + } + return cc; +} + +function updateConnected() +{ + var cellStack = []; + //reset all tiles to default not filled one + for (var i= 0; i != boardSize; i++) + { + if (level[i] > 31) + { + level[i] -= 32; + } + else + { + if (level[i] > 15) + { + level[i] -= 16; + } + } + } + + //start with start tile + var cc = 1; + cc = handleConnectPoint(startPos, cellStack, cc); + while(--cc > 0) + { + //if tile is bigger then 15 we already handled this one, continue with next one + if ((level[cellStack[cc]] < 16)) + { + cc = handleConnectPoint(cellStack[cc], cellStack, cc); + } + } + + //add start pos special tile + if (level[startPos] > 15) + level[startPos] += 16; + else + if (level[startPos] < 16) + level[startPos] += 32; +} + +function generateLevel() +{ + var neighbours = new Uint8Array(4); + var cellStack = new Uint8Array(maxBoardSize+1); + var cc = 0; + var currentPoint = 0; + var visitedRooms = 1; + var tmp, tmp2; + var selectedNeighbour; + var neighboursFound; + var lookUpX, lookUpY; + var rnd; + //generate a lookup table so we don't have to use modulus or divide constantly + //generateLookupTable(boardWidth, boardHeight); + + //intial all walls value in every room we will remove bits of this value to remove walls + for(tmp = 0; tmp < boardSize; tmp++) + level[tmp] = 0xf; + + while (visitedRooms != boardSize) + { + neighboursFound = 0; + lookUpX = currentPoint % boardWidth; + lookUpY = Math.floor(currentPoint / boardWidth); + + tmp = currentPoint+1; + //tile has neighbour to the right which we did not handle yet + if (( lookUpX + 1 < boardWidth) && (level[tmp] == 0xf)) + neighbours[neighboursFound++] = tmp; + + tmp = currentPoint-1; + //tile has neighbour to the left which we did not handle yet + if ((lookUpX > 0) && (level[tmp] == 0xf)) + neighbours[neighboursFound++] = tmp; + + tmp = currentPoint - boardWidth; + //tile has neighbour the north which we did not handle yet + if ((lookUpY > 0) && (level[tmp] == 0xf)) + neighbours[neighboursFound++] = tmp; + + tmp = currentPoint + boardWidth; + //tile has neighbour the south which we did not handle yet + if ((lookUpY + 1 < boardHeight) && (level[tmp] == 0xf)) + neighbours[neighboursFound++] = tmp; + + switch (neighboursFound) + { + case 0: + currentPoint = cellStack[--cc]; + continue; + default: + rnd = random(neighboursFound); + break; + } + selectedNeighbour = neighbours[rnd]; + tmp = (selectedNeighbour % boardWidth); + //tile has neighbour to the east + if(tmp > lookUpX) + { + //remove west wall neighbour + level[selectedNeighbour] &= ~(8); + //remove east wall tile + level[currentPoint] &= ~(2); + } + else // tile has neighbour to the west + { + if(tmp < lookUpX) + { + //remove east wall neighbour + level[selectedNeighbour] &= ~(2); + //remove west wall tile + level[currentPoint] &= ~(8); + } + else // tile has neighbour to the north + { + tmp2 = selectedNeighbour / boardWidth; + if(tmp2 < lookUpY) + { + //remove south wall neighbour + level[selectedNeighbour] &= ~(4); + //remove north wall tile + level[currentPoint] &= ~(1); + } + else // tile has neighbour to the south + { + if(tmp2 > lookUpY) + { + //remove north wall neighbour + level[selectedNeighbour] &= ~(1); + //remove south wall tile + level[currentPoint] &= ~(4); + } + } + } + } + + //add tile to the cellstack + if(neighboursFound > 1) + { + cellStack[cc++] = currentPoint; + } + //set tile to the neighbour + currentPoint = selectedNeighbour; + visitedRooms++; + } +} + +//when all board tiles are not below 16, the level is cleared +//as there are 16 tiles per tilegroup (no water, water, special start with water) +function isLevelDone() +{ + for (var i=0; i != boardSize; i++) + if(level[i] < 16) + return 0; + + return 1; +} + +function initLevel(aRandomSeed) +{ + levelDone = 0; + moves = 0; + if(difficulty != diffRandom) + //use level number + fixed value based on difficulty as seed for the random function + //this makes sure every level from a difficulty will remain the same + srand(selectedLevel + (difficulty * 500) + (gameMode * 50)); + else + srand(aRandomSeed); + + maxLevel = levelCount; + //set boardsize and max level based on difficulty + switch (difficulty) + { + case diffVeryEasy: + boardWidth = 5; + boardHeight = 5; + break; + case diffEasy: + boardWidth = 6; + boardHeight = 6; + break; + case diffNormal: + boardWidth = 7; + boardHeight = 7; + break; + case diffHard: + boardWidth = 8; + boardHeight = 8; + break; + case diffVeryHard: + boardWidth = 10; + boardHeight = 8; + break; + case diffRandom: + var rnd = random(255); + boardWidth = 5 + (rnd % (maxBoardWidth - 5 + 1)); //5 is smallest level width from very easy + rnd = random(255); + boardHeight = 5 + (rnd % (maxBoardHeight - 5 + 1)); //5 is smallest level height from very easy + maxLevel = 0; //special value with random + break; + } + //add space for arrows based on same posadd value (1 or 0 depending if sliding is allowed) + boardWidth -= posAdd + posAdd; + boardHeight -= posAdd + posAdd; + boardSize = boardWidth * boardHeight; + //generate the level + generateLevel(); + //startpoint of of level in center of screen + boardX = (maxBoardBgWidth - boardWidth) >> 1; + boardY = (maxBoardBgHeight - boardHeight) >> 1; + startPos = (boardWidth >> 1) + (boardHeight >> 1) * (boardWidth); + //startpoint of tile with water and our cursor + selectionX = boardWidth >> 1; + selectionY = boardHeight >> 1; + + //level is currently the solution so we still need to shuffle it + shuffleLevel(); + //update possibly connected tiles already starting from startpoint + updateConnected(); +} + +// -------------------------------------------------------------------------------------------------- +// levels cleared +// -------------------------------------------------------------------------------------------------- + + +function initLevelsCleared() +{ + set_bkg_data(congratsTiles); + g.clearRect(Bangle.appRect); + g.drawImage(congratsScreen, screenOffsetX, screenOffsetY); //arduboy.drawCompressed(0, 0, congratsMap); + switch (difficulty) + { + case diffVeryEasy: + printCongratsScreen(0, 3, "VERY EASY LEVELS"); + break; + + case diffEasy: + printCongratsScreen(3, 3, "EASY LEVELS"); + break; + + case diffNormal: + printCongratsScreen(2, 3, "NORMAL LEVELS"); + break; + + case diffHard: + printCongratsScreen(3, 3, "HARD LEVELS"); + break; + + case diffVeryHard: + printCongratsScreen(0, 3, "VERY HARD LEVELS"); + break; + } +// SelectMusic(musAllLevelsClear); + requiresFlip = 1; +} + +function levelsCleared() +{ + if(gameState == gsInitLevelsCleared) + { + initLevelsCleared(); + gameState -= gsInitDiff; + } + + if (btna || btnb) + { + //playMenuAcknowlege(); + titleStep = tsMainMenu; + gameState = gsInitTitle; + } + needRedraw = 0; +} + + +// -------------------------------------------------------------------------------------------------- +// level select +// -------------------------------------------------------------------------------------------------- + +function drawLevelSelect() +{ + g.clearRect(Bangle.appRect); + //LEVEL: + printMessage(maxBoardBgWidth , 0 , "LEVEL:"); + + //[LEVEL NR] 2 chars + printNumber(maxBoardBgWidth + 4 , 1 , selectedLevel, 2); + + //B:BACK + printMessage(maxBoardBgWidth , 6 , "BTN:"); + printMessage(maxBoardBgWidth , 7 , "BACK"); + + //A:PLAY + printMessage(maxBoardBgWidth , 4 , "TOUCH:"); + printMessage(maxBoardBgWidth , 5 , "PLAY"); + + //Locked & Unlocked keywoard + var tmpUnlocked = levelUnlocked(gameMode, difficulty, selectedLevel -1); + if(!tmpUnlocked) + printMessage(maxBoardBgWidth , 2 , "LOCKED"); + else + printMessage(maxBoardBgWidth , 2 , "OPEN"); + + //Draw arrows for vertical / horizontal movement + if(gameMode != gmRotate) + { + for (var x = 0; x != boardWidth; x++) + { + set_bkg_tile_xy(boardX + x , boardY -1 , arrowDown); + set_bkg_tile_xy(boardX + x , boardY + boardHeight , arrowUp); + } + + for (var y = 0; y != boardHeight; y++) + { + set_bkg_tile_xy(boardX - 1 , boardY + y , arrowRight); + set_bkg_tile_xy(boardX + boardWidth , boardY + y , arrowLeft); + } + } + + var i16 = 0; + for (var yy = 0; yy < boardHeight; yy++) + { + for(var xx = 0; xx 1) + { + //playMenuSelectSound(); + selectedLevel--; + initLevel(randomSeedGame); + needRedraw = 1; + } + } + } + if (dragright) + { + if (difficulty == diffRandom) + { + //playMenuSelectSound(); + //need new seed based on time + randomSeedGame = Date.now(); + initLevel(randomSeedGame); + needRedraw = 1; + } + else + { + if (selectedLevel < maxLevel) + { + //playMenuSelectSound(); + selectedLevel++; + initLevel(randomSeedGame); + needRedraw = 1; + } + } + } + +} + +// -------------------------------------------------------------------------------------------------- +// printing functions +// -------------------------------------------------------------------------------------------------- + +function setCharAt(str,index,chr) { + if(index > str.length-1) return str; + return str.substring(0,index) + chr + str.substring(index+1); +} + +function formatInteger(valinteger) +{ + const maxDigits = 10; + var array = " "; + + const maxCharacters = (maxDigits); + + + const lastIndex = (maxCharacters - 1); + + + if(valinteger == 0) + { + array = setCharAt(array,lastIndex, '0'); + return {digits:1,string:array}; + } + + var digits = 0; + var integer = valinteger; + do + { + var digit = integer % 10; + integer = Math.floor(integer / 10); + + array = setCharAt(array,lastIndex - digits, digit.toString()); + ++digits; + } + while(integer > 0); + + return {digits:digits,string:array}; +} + +//print a number on levelselect or game screen +function printNumber(ax, ay, aNumber, maxDigits) +{ + const buffSize = 10; + + var ret = formatInteger(aNumber); + var maxFor = ret.digits; + if (ret.digits > maxDigits) + maxFor = maxDigits; + for (var c=0; c < maxFor; c++) + { + if (ret.string.charAt(buffSize - ret.digits + c) == '') + return; + set_bkg_tile_xy(ax + (maxDigits-ret.digits) + c, ay, ret.string.charCodeAt(buffSize - ret.digits + c) + 32); + } +} + +function printDebug(ax,ay, amsg) + { + if(debugMode) + { + //rememvber current tiles + var tiles = get_bkg_data(); + setBlockTilesAsBackground(); + g.clearRect(Bangle.appRect); + printMessage(ax, ay, amsg); + setTimeout(() => { g.flip(); }, 2500); + //restore the previous tiles + set_bkg_data(tiles); + } + } + +//print a message on the title screen on ax,ay, the tileset from titlescreen contains an alphabet +function printMessage(ax, ay, amsg) +{ + var index = 0; + var p = 0; + while (1) + { + var fChar = amsg.charAt(p++); + var tile = 61; + switch (fChar) + { + case '': + return; + + case '[': + tile = 70; + break; + + case ']': + tile = 64; + break; + + case '<': + tile = 73; + break; + + case '>': + tile = 67; + break; + + case '+': + tile = 63; + break; + + case '*': + tile = 62; + break; + + case '|': + tile = 69; + break; + + case '#': + tile = 65; + break; + + case ':': + tile = 116; + break; + + case 'a': + tile = 119; + break; + + case 'b': + tile = 117; + break; + + default: + if ((fChar.charCodeAt(0) >= 'A'.charCodeAt(0)) && (fChar.charCodeAt(0) <= 'Z'.charCodeAt(0))) + tile = fChar.charCodeAt(0) + 25; + + if ((fChar.charCodeAt(0) >= '0'.charCodeAt(0)) && (fChar.charCodeAt(0) <= '9'.charCodeAt(0))) + tile = fChar.charCodeAt(0) + 32; + break; + } + set_bkg_tile_xy(ax + index, ay, tile); + ++index; + } +} + +//print a message on the CongratsScreen on ax,ay, the tileset from Congrats Screen contains an alphabet in another font +function printCongratsScreen(ax, ay, amsg) +{ + // based on input form @Pharap + var index = 0; + var p = 0; + while (1) + { + var fChar = amsg.charAt(p++); + var tile = 26; + switch (fChar) + { + case '': + return; + + default: + if ((fChar.charCodeAt(0) >= 'A'.charCodeAt(0)) && (fChar.charCodeAt(0) <= 'Z'.charCodeAt(0))) + tile = fChar.charCodeAt(0) - 'A'.charCodeAt(0); + break; + } + set_bkg_tile_xy(ax + index, ay, tile); + ++index; + } +} + +// -------------------------------------------------------------------------------------------------- +// save state +// -------------------------------------------------------------------------------------------------- + + +function clearBit8(val, bit) +{ + if(bit > 7) + return val; + return val & (~(1 << bit)); +} + +function setBit8(val, bit) +{ + if (bit > 7) + return val; + return val | (1 << bit); +} + +function checkBit8(val, bit) + { + if (bit > 7) + return 0; + return ((val >> bit) & 1); + } + +function clearBit32(val, bit) +{ + if(bit > 31) + return val; + return val & (~(1 << bit)); +} + +function setBit32(val, bit) +{ + if (bit > 31) + return val; + return val | (1 << bit); +} + +function checkBit32(val, bit) + { + if (bit > 31) + return 0; + return ((val >> bit) & 1); + } + +function packLevelLock(mode, diff, level) +{ + var levelIndex = (mode * diffCount) + diff; + var pack = Math.floor(levelIndex / 6); // 6 x 5 bit nr in a pack so have to divide levelindex by 6 to get packnr + var bit = (levelIndex * 5) - (pack * 30); //6 x 5 bit nr in a pack so level index * 5 = bit nr acrross all packs - pack * 30 to know bit for current pack + for (var i = 0; i<5; i++) + { + if (checkBit8(level, i)) + levelLocksPacked[pack] = setBit32(levelLocksPacked[pack], bit + i); + else + levelLocksPacked[pack] = clearBit32(levelLocksPacked[pack], bit + i); + } +} + +function unPackLevelLock(mode, diff) +{ + var levelIndex = (mode * diffCount) + diff; + var pack = Math.floor(levelIndex / 6); // 6 x 5 bit nr in a pack so have to divide levelindex by 6 to get packnr + var bit = (levelIndex * 5) - (pack * 30); //6 x 5 bit nr in a pack so level index * 5 = bit nr acrross all packs - pack * 30 to know bit for current pack + var result = 0; + for (var i = 0; i<5; i++) + { + if (checkBit32(levelLocksPacked[pack], bit+i)) + result = setBit8(result, i); + } + return result; +} + +function validateSaveState() +{ + var levelsUnlocked = 0; + for (var j=0; j levelCount)) + return 0; + } + } + if (options > 3) //bit 0 & 1 set = 3 + return 0; + return 1; +} + +function initSaveState() +{ + //read from file + + //then + if(true || !validateSaveState()) + { + levelLocksPacked[0] = 0; + levelLocksPacked[1] = 0; + levelLocksPacked[2] = 0; + for (var j=0; j level); +} + +function lastUnlockedLevel(mode, diff) +{ + return unPackLevelLock(mode, diff); +} + +function unlockLevel(mode, diff, level) +{ + if (level + 1> lastUnlockedLevel(mode, diff)) + { + packLevelLock(mode, diff, level + 1); + saveSaveState(); + } +} + +// -------------------------------------------------------------------------------------------------- +// titlescreen +// -------------------------------------------------------------------------------------------------- + +function drawTitleScreen() +{ + g.clearRect(Bangle.appRect); + g.drawImage(title, screenOffsetX, screenOffsetY); //arduboy.drawCompressed(0, 0, titlescreenMap); + + switch (titleStep) + { + case tsMainMenu: + printMessage(5, 4, "START"); + printMessage(5, 5, "HELP"); + printMessage(5, 6, "OPTIONS"); + printMessage(5, 7, "CREDITS"); + break; + case tsDifficulty: + printMessage(3, 3, "VERY EASY"); + printMessage(3, 4, "EASY"); + printMessage(3, 5, "NORMAL"); + printMessage(3, 6, "HARD"); + if(difficulty <= diffVeryHard) + printMessage(3, 7, "VERY HARD"); + else + printMessage(3, 7, "RANDOM"); + break; + case tsGameMode: + printMessage(5, 4, "ROTATE"); + printMessage(5, 5, "SLIDE"); + printMessage(5, 6, "ROSLID"); + break; + case tsCredits: + printMessage(3, 5, "CREATED BY"); + printMessage(2, 6, "WILLEMS DAVY"); + printMessage(2, 7, "JOYRIDER3774"); + break; + case tsOptions: + //if(isMusicOn()) + printMessage(4, 4, "MUSIC ON"); + //else + // printMessage(4, 4, "MUSIC OFF"); + + //if(isSoundOn()) + printMessage(4, 5, "SOUND ON"); + //else + // printMessage(4, 5, "SOUND OFF"); + //break; + } + + //set menu tile + switch (titleStep) + { + case tsMainMenu: + set_bkg_tile_xy(4, 4 + mainMenu, leftMenu); + break; + case tsGameMode: + set_bkg_tile_xy(4, 4 + gameMode, leftMenu); + break; + case tsDifficulty: + if(difficulty >= diffVeryHard) + set_bkg_tile_xy(2, 7, leftMenu); + else + set_bkg_tile_xy(2, 3 + difficulty, leftMenu); + break; + case tsOptions: + set_bkg_tile_xy(2, 4 + option, leftMenu); + break; + } +} + +function initTitleScreen() +{ + setBlockTilesAsBackground(); + //SelectMusic(musTitle); + needRedraw = 1; +} + +function titleScreen() +{ + if(gameState == gsInitTitle) + { + initTitleScreen(); + gameState -= gsInitDiff; + } + + if(needRedraw) + { + drawTitleScreen(); + needRedraw = 0; + requiresFlip = 1; + } + + if (dragup) + { + switch (titleStep) + { + case tsMainMenu: + if(mainMenu > mmStartGame) + { + //playMenuSelectSound(); + mainMenu--; + needRedraw = 1; + } + break; + case tsGameMode: + if(gameMode > gmRotate) + { + //playMenuSelectSound(); + gameMode--; + needRedraw = 1; + } + break; + case tsDifficulty: + if(difficulty > diffVeryEasy) + { + //playMenuSelectSound(); + difficulty--; + needRedraw = 1; + } + break; + case tsOptions: + if(option > opMusic) + { + //playMenuSelectSound(); + option--; + needRedraw = 1; + } + break; + } + } + + if (dragdown) + { + switch (titleStep) + { + case tsMainMenu: + if(mainMenu < mmCount-1) + { + //playMenuSelectSound(); + mainMenu++; + needRedraw = 1; + } + break; + case tsGameMode: + if(gameMode < gmCount-1) + { + //playMenuSelectSound(); + gameMode++; + needRedraw = 1; + } + break; + case tsDifficulty: + if(difficulty < diffCount-1) + { + //playMenuSelectSound(); + difficulty++; + needRedraw = 1; + } + break; + case tsOptions: + if(option < opCount-1) + { + //playMenuSelectSound(); + option++; + needRedraw = 1; + } + break; + } + } + + if (btnb) + { + switch (titleStep) + { + case tsOptions: + case tsCredits: + titleStep = tsMainMenu; + //playMenuBackSound(); + needRedraw = 1; + break; + case tsGameMode: + case tsDifficulty: + titleStep--; + //playMenuBackSound(); + needRedraw = 1; + break; + } + } + + if (btna) + { + //playMenuAcknowlege(); + switch(mainMenu) + { + case mmOptions: + if(titleStep != tsOptions) + { + titleStep = tsOptions; + needRedraw = 1; + } + else + { + switch(option) + { + case opMusic: + //setMusicOn(!isMusicOn()); + //setMusicOnSaveState(isMusicOn()); + needRedraw = 1; + break; + case opSound: + //setSoundOn(!isSoundOn()); + //setSoundOnSaveState(isSoundOn()); + needRedraw = 1; + break; + } + } + break; + + case mmCredits: + if(titleStep != tsCredits) + { + titleStep = tsCredits; + needRedraw = 1; + } + else + { + titleStep = tsMainMenu; + needRedraw = 1; + } + break; + + case mmHelp: + if (titleStep < tsGameMode) + { + titleStep++; + needRedraw = 1; + } + else + { + switch (gameMode) + { + case gmRotate: + gameState = gsInitHelpRotate; + break; + case gmSlide: + gameState = gsInitHelpSlide; + break; + case gmRotateSlide: + gameState = gsInitHelpRotateSlide; + break; + } + } + break; + + case mmStartGame: + if (titleStep < tsDifficulty) + { + titleStep++; + needRedraw = 1; + } + else + { + if (difficulty == diffRandom) + selectedLevel = 1; + else + selectedLevel = lastUnlockedLevel(gameMode, difficulty); + + if (gameMode == gmRotate) + posAdd = 0; + else + posAdd = 1; + //set randomseet to systime here + //it will be reused all the time + //with the level generating + //but not when going back from + //level playing to level selector + //when calling init level there + randomSeedGame = Date.now(); + initLevel(randomSeedGame); + + gameState = gsInitLevelSelect; + } + break; + } + } +} + +// -------------------------------------------------------------------------------------------------- +// game +// -------------------------------------------------------------------------------------------------- + +function drawGame() +{ + //background + if(!paused && !redrawLevelDoneBit) + { + g.clearRect(Bangle.appRect); + + //LEVEL: + printMessage(maxBoardBgWidth, 0, "LEVEL:"); + + //[LEVEL NR] 2 chars + printNumber(maxBoardBgWidth + 4, 1, selectedLevel, 2); + + + //MOVES: + printMessage(maxBoardBgWidth, 2, "MOVES:"); + + printNumber(maxBoardBgWidth + 1, 3, moves, 5); + + //A:XXXXXX (XXXXXX="ROTATE" or XXXXXX="SLIDE " or XXXXXX="ROSLID") + switch (gameMode) + { + case gmRotate: + printMessage(maxBoardBgWidth, 4, "TOUCH:"); + printMessage(maxBoardBgWidth, 5, "ROTATE"); + break; + case gmSlide: + printMessage(maxBoardBgWidth, 4, "TOUCH:"); + printMessage(maxBoardBgWidth, 5, "SLIDE"); + break; + case gmRotateSlide: + printMessage(maxBoardBgWidth, 4, "TOUCH:"); + printMessage(maxBoardBgWidth, 5, "ROSLID"); + break; + } + + //B:BACK + printMessage(maxBoardBgWidth, 6, "BTN:"); + printMessage(maxBoardBgWidth, 7, "BACK"); + + //Draw arrows for vertical / horizontal movement + if(gameMode != gmRotate) + { + + for (var x = 0; x != boardWidth; x++) + { + set_bkg_tile_xy(boardX + x, boardY -1, arrowDown); + set_bkg_tile_xy(boardX + x, boardY + boardHeight, arrowUp); + } + + for (var y = 0; y != boardHeight; y++) + { + set_bkg_tile_xy(boardX - 1, boardY + y, arrowRight); + set_bkg_tile_xy(boardX + boardWidth, boardY + y, arrowLeft); + } + } + + //level + var i16 = 0; + for (var yy = 0; yy < boardHeight; yy++) + { + for(var xx = 0; xx > 1) - 3) * 8, screenOffsetX + 16*8, screenOffsetY ++ ((maxBoardBgHeight >> 1) - 3) * 8 + (6*8)); + g.setColor(1,1,1); + printMessage(0, (maxBoardBgHeight >> 1) - 3, "[**************]"); + printMessage(0, (maxBoardBgHeight >> 1) - 2, "|PLEASE CONFIRM+"); + printMessage(0, (maxBoardBgHeight >> 1) - 1, "| +"); + printMessage(0, (maxBoardBgHeight >> 1) + 0, "| TOUCH PLAY +"); + printMessage(0, (maxBoardBgHeight >> 1) + 1, "| BTN TO QUIT +"); + printMessage(0, (maxBoardBgHeight >> 1) + 2, "<##############>"); +} + +function doUnPause() +{ + paused = 0; + //setMusicOn(wasMusicOn); + //setSoundOn(wasSoundOn); + setCursorPos(0, boardX + selectionX, boardY + selectionY); + showCursors(); + needRedraw = 1; +} + +function game() +{ + if(gameState == gsInitGame) + { + initGame(); + gameState -= gsInitDiff; + } + + if(needRedraw) + { + drawGame(); + drawCursors(); + needRedraw = 0; + requiresFlip = 1; + } + + //needRedraw = updateCursorFrame(); + + if (dragdown) + { + if(!levelDone && !paused) + { + //playGameMoveSound(); + //if not touching border on bottom + if (selectionY + 1 < boardHeight + posAdd) + { + selectionY += 1; + needRedraw = 1; + } + else + //set to border on top + { + selectionY = -posAdd; + needRedraw = 1; + } + setCursorPos(0, boardX + selectionX, boardY + selectionY); + } + } + + if (dragup) + { + if (!levelDone && !paused) + { + //if not touching border on top + //playGameMoveSound(); + if (selectionY -1 >= -posAdd) + { + selectionY -= 1; + needRedraw = 1; + } + else + //set to border on bottom + { + selectionY = boardHeight -1 +posAdd; + needRedraw = 1; + } + setCursorPos(0, boardX + selectionX, boardY + selectionY); + } + } + + if (dragright) + { + if (!levelDone && !paused) + { + //playGameMoveSound(); + //if not touching border on right + if(selectionX + 1 < boardWidth + posAdd) + { + selectionX += 1; + needRedraw = 1; + } + else + //set to border on left + { + selectionX = -posAdd; + needRedraw = 1; + } + setCursorPos(0, boardX + selectionX, boardY + selectionY); + } + } + + if (dragleft) + { + if(!levelDone && !paused) + { + //playGameMoveSound(); + //if not touching border on left + if( selectionX -1 >= -posAdd) + { + selectionX -= 1; + needRedraw = 1; + } + //set to border on right + else + { + selectionX = boardWidth -1 + posAdd; + needRedraw = 1; + } + setCursorPos(0, boardX + selectionX, boardY + selectionY); + } + } + + if (btna) + { + if(paused) + { + doUnPause(); + //playMenuAcknowlege(); + needRedraw = 1; + } + else + { + if(!levelDone) + { + if ((selectionX > -1) && (selectionX < boardWidth) && + (selectionY > -1) && (selectionY < boardHeight)) + { + if (gameMode != gmSlide) + { + rotateBlock(selectionX + (selectionY * boardWidth)); + moves++; + //playGameAction(); + needRedraw = 1; + } + else + { + //playErrorSound(); + } + } + else + { + if ((selectionX > -1) && (selectionX < boardWidth)) + { + if (selectionY == -1) + { + moveBlockDown(selectionX + ((selectionY+1) * boardWidth)); + moves++; + //playGameAction(); + needRedraw = 1; + } + else + { + if (selectionY == boardHeight) + { + moveBlockUp(selectionX + ((selectionY-1) * boardWidth)); + moves++; + //playGameAction(); + needRedraw = 1; + } + } + } + else + { + if ((selectionY > -1) && (selectionY < boardHeight)) + { + if (selectionX == -1) + { + moveBlockRight((selectionX + 1) + (selectionY * boardWidth)); + moves++; + //playGameAction(); + needRedraw = 1; + } + else + { + if (selectionX == boardWidth) + { + moveBlockLeft((selectionX - 1) + (selectionY * boardWidth)); + moves++; + //playGameAction(); + needRedraw = 1; + } + } + } + else + { + //playErrorSound(); + } + } + } + updateConnected(); + levelDone = isLevelDone(); + if(levelDone) + { + //update level one last time so we are at final state + //as it won't be updated anymore as long as level done is displayed + //1 forces level to be drawn (only) one last time the other call uses levelDone + drawGame(); + //SelectMusic(musLevelClear); + //hide cursor it's only sprite we use + hideCursors(); + g.setColor(0,0,0); + g.fillRect(screenOffsetX + ((16 - 13) >> 1) * 8, screenOffsetY + ((maxBoardBgHeight >> 1) - 2) * 8, screenOffsetX + (((16 - 13) >> 1) * 8) + (14*8), screenOffsetY + (((maxBoardBgHeight >> 1) - 2) * 8) +(5*8)); + g.setColor(1,1,1); + printMessage(((16 - 13) >> 1), (maxBoardBgHeight >> 1) - 2, "[************]"); + printMessage(((16 - 13) >> 1), (maxBoardBgHeight >> 1) - 1, "| LEVEL DONE +"); + printMessage(((16 - 13) >> 1), (maxBoardBgHeight >> 1) , "| TOUCH TO +"); + printMessage(((16 - 13) >> 1), (maxBoardBgHeight >> 1) + 1, "| CONTINUE +"); + printMessage(((16 - 13) >> 1), (maxBoardBgHeight >> 1) + 2, "<############>"); + redrawLevelDoneBit = 1; + } + } + else + { + redrawLevelDoneBit = 0; + //goto next level + if (difficulty == diffRandom) + { + //ned new seed based on time + randomSeedGame = Date.now(); + initLevel(randomSeedGame); + //SelectMusic(musGame); + //show cursor again (it's actually to early but i'm not fixing that) + setCursorPos(0, boardX + selectionX, boardY + selectionY); + showCursors(); + needRedraw = 1; + } + else + { + //goto next level if any + if (selectedLevel < maxLevel) + { + selectedLevel++; + unlockLevel(gameMode, difficulty, selectedLevel-1); + initLevel(randomSeedGame); + //SelectMusic(musGame); + //show cursor again (it's actually to early but i'm not fixing that) + setCursorPos(0, boardX + selectionX, boardY + selectionY); + showCursors(); + needRedraw = 1; + } + else //Goto some congrats screen + { + gameState = gsInitLevelsCleared; + } + } + } + } + } + + if(btnb) + { + if(!levelDone) + { + if(!paused) + { + //playMenuBackSound(); + doPause(); + needRedraw = 1; + } + else + { + //need to enable early again to play backsound + //normally unpause does it but we only unpause + //after fade + //setSoundOn(wasSoundOn); + hideCursors(); + //playMenuBackSound(); + gameState = gsInitLevelSelect; + doUnPause(); + //unpause sets cursor visible ! + hideCursors(); + //need to reset the level to initial state when going back to level selector + //could not find a better way unfortunatly + //also we do not want to reset the randomseed used for random level generating + //or a new level would have been created when going back we only want the level + //with random to change when pressing left and right in the level selector + //this way it stays consistent with the normal levels + //and the player can replay the level if he wants to + initLevel(randomSeedGame); + } + } + } +} + + +// -------------------------------------------------------------------------------------------------- +// main game start +// -------------------------------------------------------------------------------------------------- +function setup() +{ + setBlockTilesAsBackground(); + option = 0; + difficulty = diffNormal; + selectedLevel = 1; + mainMenu = mmStartGame; + gameState = gsInitIntro; + titleStep = tsMainMenu; + gameMode = gmRotate; + //has to be called first because initsound and initmusic read savestate sound to set intial flags + initSaveState(); + //initSound(); + //initMusic(); + //setMusicOn(isMusicOnSaveState()); + //setSoundOn(isSoundOnSaveState()); + } + +function loop() +{ + //soundTimer(); + + g.reset(); + g.setColor(1,1,1); + g.setBgColor(0,0,0); + + //gamestate handling + var prevGameState = gameState; + + switch (gameState) + { + case gsInitTitle: + case gsTitle: + clearInterval(intervalTimer); + titleScreen(); + break; + case gsInitLevelSelect: + case gsLevelSelect: + levelSelect(); + break; + case gsInitGame: + case gsGame: + game(); + break; + case gsInitLevelsCleared: + case gsLevelsCleared: + levelsCleared(); + break; + case gsInitHelpSlide: + case gsHelpSlide: + helpSlide(); + break; + case gsInitHelpSlide2: + case gsHelpSlide2: + helpSlide2(); + break; + case gsInitHelpSlide3: + case gsHelpSlide3: + helpSlide3(); + break; + case gsHelpRotateSlide: + case gsInitHelpRotateSlide: + helpRotateSlide(); + break; + case gsInitHelpRotateSlide2: + case gsHelpRotateSlide2: + helpRotateSlide2(); + break; + case gsInitHelpRotateSlide3: + case gsHelpRotateSlide3: + helpRotateSlide3(); + break; + case gsInitHelpRotateSlide4: + case gsHelpRotateSlide4: + helpRotateSlide4(); + break; + case gsInitHelpRotate: + case gsHelpRotate: + helpRotate(); + break; + case gsInitHelpRotate2: + case gsHelpRotate2: + helpRotate2(); + break; + case gsInitHelpRotate3: + case gsHelpRotate3: + helpRotate3(); + break; + case gsInitIntro: + case gsIntro: + intro(); + break; + } + if(requiresFlip) + { + if(scaleScreen) + { + //scale whats currently on screen to full size of the screen + //128 was original games width (128x64) + //var scale = (screenWidth - 8) / 128; + //don't make this smaller than 1 or it won't work + var scale = 1.25; + g.drawImage(g.asImage(),((g.getWidth() / 2)-((g.getWidth() * scale) / 2)), Bangle.appRect.y/2 - Bangle.appRect.y/2*scale + ((g.getHeight()/2) - ((g.getHeight() * scale) / 2)),{scale:scale}); + + } + + Bangle.drawWidgets(); + + if(debugMode) + { + const offsetvalue = 0.20; + var x1 = screenWidth * offsetvalue; + var x2 = screenWidth - screenWidth * offsetvalue; + var y1 = Bangle.appRect.y + screenHeight *offsetvalue; + var y2 = screenHeight - screenHeight * offsetvalue; + g.setColor(1,0,1); + //up + g.drawRect(0,Bangle.appRect.y,screenWidth-1,y1); + //down + g.drawRect(0,y2,screenWidth-1,screenHeight-1); + //left + g.drawRect(0,Bangle.appRect.y,x1,screenHeight-1); + //right + g.drawRect(x2,Bangle.appRect.y,screenWidth-1, screenHeight-1); + } + g.flip(); + requiresFlip = 0; + } + + //when switching gamestate we need a redraw + if(gameState != prevGameState) + needRedraw = 1; + debugLog("loop"); + if(debugModeRamUse) + { + var memTmp = process.memory(false); + var used = memTmp.usage - memStart.usage; + debugLog("Udiff:"+ used.toString() + " used:" + memTmp.usage.toString() + " free:" + memTmp.free.toString() + " total:" + memTmp.total.toString() ); + } +} + +function debugLog(val) +{ + if(debugMode) + print(val); +} + +function handleTouch(button, data) +{ + const offsetvalue = 0.20; + var x1 = screenWidth * offsetvalue; + var x2 = screenWidth - screenWidth * offsetvalue; + var y1 = Bangle.appRect.y + screenHeight *offsetvalue; + var y2 = screenHeight - screenHeight * offsetvalue; + dragleft = data.x x2; + dragup = data.y < y1; + dragdown = data.y > y2; + btna = ((data.x <= x2) && (data.x >= x1) && (data.y >= y1) && (data.y <= y2) && (data.type == 0)); + btnb = ((data.x <= x2) && (data.x >= x1) && (data.y >= y1) && (data.y <= y2) && (data.type == 2)); + debugLog("tap button:" + button.toString() + " x:" + data.x.toString() + " y:" + data.y.toString() + " x1:" + x1.toString() +" x2:" + x2.toString() +" y1:" + y1.toString() +" y2:" + y2.toString() +" type:" + data.type.toString()); + debugLog("l:" + dragleft.toString() + " u:" + dragup.toString() + " r:" + dragright.toString() + " d:" + dragdown.toString() + " a:" + btna.toString() + " b:" + btnb.toString()); + loop(); + dragleft = false; + dragright = false; + dragdown = false; + dragup = false; + btna = false; + btnb = false; + while(needRedraw) + loop(); + debugLog("handleTouch done"); +} + +function btnPressed() +{ + dragleft = false; + dragright = false; + dragdown = false; + dragup = false; + btna = false; + btnb = true; + loop(); + btnb = false; + while(needRedraw) + loop(); + debugLog("btnPressed done"); +} + +var memStart; +if(debugModeRamUse) + memStart = process.memory(true); + + +//clear one time entire screen +g.clear(); +//setup game and run loop it will repeat during intro +//otherwise only as long as redraw is needed after input was detected +setup(); +//for intro only +var intervalTimer = setInterval(loop, 66); // 15 fps +//for handling input +Bangle.on('touch', handleTouch); +setWatch(btnPressed, BTN, {edge:"rising", debounce:50, repeat:true}); \ No newline at end of file diff --git a/apps/waternet/app.png b/apps/waternet/app.png new file mode 100644 index 0000000000000000000000000000000000000000..64d89f3613c0a7ae2ba4c3fa4a956f4574cefa78 GIT binary patch literal 740 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 literal 0 HcmV?d00001 diff --git a/apps/waternet/roslid-slide.png b/apps/waternet/roslid-slide.png new file mode 100644 index 0000000000000000000000000000000000000000..57dd459f6b0995d385d93412eda6a48b7282c2ed GIT binary patch literal 2857 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 literal 0 HcmV?d00001 diff --git a/apps/waternet/rotate.png b/apps/waternet/rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..15bed1b48437e2bff122ba4cf9974ec94bd9ed06 GIT binary patch literal 2279 zcmds(dpOg5AIHDHX=o#rP;$IGD0ApYaZ_n-x5K^4xsAnBo{6%R8YY<(qh=vWIiy0j z?U&PRE2ec9Zpv|O&bCrX8z$+1cs$qh|MT~AJ%4;Y@9TPBpU?HVuJ`r&3{KvD@j|9)xOSico9pXBB35%LoRKw4R~vVsJ2&fz)hQVH@~KM z(vCV;mJm+GYf;T91@qgQm=tWxyowFvUwjWCWbQP_#w3`kT*o z4f1<=)UKqo@|8Ogryl2#QU_@}Sz#EazKd&^TmCA#<6p-dxU=V896;i9tN)rKRK@)= z;(gHVQU^-K(62piWvYBAijZhe(SY4$0!ru~E?kI>_6PI=B*_6r0FER(yd4E9Yvf*Ket=#}TitdP0#yC{G47J< zf9T#`3=p+n+F9Ta0qH0AT_hyvJxVDRTLKZ@DeVIX07rEDIXn^svMsv)r;ZAc@-O_R z?jrC0MD4T3-gc3*Bu(!tO3voIl{B#*Oyt>{5?U)}k_U2+Nd~2*hNH!AG7Vm;(8P8}7v}14Ww9^gcptILUD)p&G{lfm!V@Ej;5G2wTQT?`Fp2_dEKGVQU_lcWg>+ z_RQYd*|0sjx#JYm_kqba){Cl3Tw~deC(S@nFFgb1+52yyIFDY}#yU~(P55WrjP;P< z1__SfTRthfxSL7V=$$pk1t(1}OnijDJapsH>sVD68CRF7`grATPqzY|e~3bm3&rqP zN>M!nH9Tn;lhk$y`RPdhHoaa{>G>dZNQufRM^eqS>vp+8-5hSN9?Uaz_#CQ3Pt@Du zN3c|xag9#hiW1~VN>gF1B57Y*14@5 ziZ#Ua1ZjebvAJDrb@@q7P_+4fmS|h)Fn@Q{1U{<#O&@ozBl$fvY*pzT{ZC{5z?1Hp zlQG5hRw5_+?w*OvA!g068@mDSn&x%ZnZa#n5fk<5c}@{yoQwlXEz0uLbx~GN(bHj2 z``qi-K%xDui8GgGfnP8If`N@v`!ucAwwD7T-dZ?h#N5rlBqdTqMU zvs3m12s+gW-;$uU^vM9amJ6oR7rA15O^d-2?IKMWfK(pq9kf>6WeMft@eOexC8-ky zDB*Lz6A`M5)-<&L9LYG<+8Gu2vod44Wi~+{G#s4K$rCTdI%xdT0ZF-NaJ?2mMxSO1 z4_@>8kl$jzPULv~NA~i8MRDB&=O3qtt3o2pfB4F9P|%Kgq{{MoCC(HX1b8+j=cm7# zkQ(QRuwh04Y1(`ARr@Tu1h?2%vuh2}b1u6@SV z{2mmuS(Zh%E{L~1!F^lMvd`(OFVZGBi?hjj+5GfmkN>p^HU4ZpACuBo4=)VP21k`lE*-ZY;z)V4C&Szg(iPE3UyB1`IKc hvou21{@;u)nELw0>qby9#!H_IaCgOFAGw5F`WNI~9)kb? literal 0 HcmV?d00001 diff --git a/apps/waternet/screenshot1.png b/apps/waternet/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e297a01491627cdd0294afec8573a41ffc5055 GIT binary patch literal 2909 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 literal 0 HcmV?d00001 diff --git a/apps/waternet/screenshot3.png b/apps/waternet/screenshot3.png new file mode 100644 index 0000000000000000000000000000000000000000..93c91a5440a977847dd5c205de3ac6b6f4f59551 GIT binary patch literal 2873 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 literal 0 HcmV?d00001 diff --git a/apps/waternet/slide-finish.png b/apps/waternet/slide-finish.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea7e9562ce95fbbba36a514ece53753f450ca7d GIT binary patch literal 2776 zcmd^B`8V5%7T525Qz4_Z1x4+;NVTI{w3ebYmX@LsL{hq?r7EdGY|#wH)F>@dV`^z- zN-aqZ38sX!qqN~g5o(!`*4o&D5-Q<&zrVlWy?f5RpYyrr++XfJpL_59i(W`2MQuen zIXNYFHy7U>toe^ackjfP4HLK>*cIiAbe3cAbic^SDdxDlI9(#)=8IYW{ zL*3(sJ^|RQwr4tX!^uzH2}&=Wc)-c+__$;GTQEcM`pPjDGakjeT8hsN!f)^-J1}g`}OF=yn7}m>U=pezn7P|-h|vj zQ)q}WGGl~l5Uyu_Uxn0Onf7Oi31MhH5Az+pB%84CoW4_GoFShaskwgZ?3?zU7rUR#&6q?b^M)u z=c@Hj8qU3hqz?xbf_=>p3oFx@ksnmoqm7#Dd4p*I65iVTST)k#DT(R!bOSQ-vwGY% z*K6rzZmrz3~) zc7spl9YS{%8A_b<-wf10UN|D2+h{+U+Q-bjA_x=?W!M^vZ;s_3Bpy^arL%^GFzk;r z=9NB@4qM-z)ELo7!Tu7s`Th79N~n5a_WhGOqdHjVL!xP$AqB0f?O++N`5Co8`Jh=z zTN1kKE&3Em=U+9yFq9V$)hr+HKJeGKsP&WB(Xwiv+6nwg|?fBi`VnkryRH$aUyNne#guK+T*=9C;`E^ShFD0WdiaF6qv z<34SAkAo|Af&IU*hem{rhIa0m=SXp$47RT+9&A>)QaObpF zs@_zZRr)9*i#wamMd!trE+S3?)-CvYr%T5oD&0gjAX!DLNFCa2d409F49D$OC)wup zlvct?V-A_AzE!hsTpx(P_d?YJTAFe~NI8dVn((7q`9F@8=+oC7&}x#V8oS7w$XrZC zs_0*?6OBE@`XJrOC`)*N%DD&S%fCuve^AFUEc)O95UW?$=DjnF^Bq?~zh`u&NhKjG z;>2|rJtn8?QZyB{W&`Rmm1zT^ms!YzMnD2JA6zRl(FPMiU+@lgxm9PSBPwV<`6MyA zaIelIhO1(pmri;zl>Wz zq!nsn^~+8gJTwwVdC_FOGOx37x>Y(GgJLq+3*rQd7yiX4w$Op`3c#dn0O zHAY{3-W*+a^xY#OEAEAL3cVRBeUf=rIG{vcnYg>kvELU)4PmYe>K4YDR4r}QSmxH{ z9Ckw&pIe#b=RHAHoFij+OM-WN#pcC0B)+2727w_!rSVRc^?EcQYQnR;L6yWrt%B6U z;x1{P+N~eqK2?F9N3QQ-TOi<#A4dilq%eyL)^0j3k?A#QS8XpoWC48C7rH1kFI{He z$EXC*Ir;4Sw6qV~+KE-A*@}OQE=>8uyKftkQJn&I=cJif@7jjz8`-{?ue-xa!qpe& zu+Y^5Uf2(#jRgax=!pJP`d=vhHH0i5ZQ0ZCD0dWgTOYnWB z*;JPi>u%Lg)oYjQqOLWU$N`$?@kg;D_r9=bep768aFwqu>@An!`O?NFp8SMkevvim zhfRe4rn)$%7cd_fzS%Q^?}tQZ`^}cV(tEUGixskiX)PDiDl!pO=(d#1;=%-XB$XBG z*Ec-2UognSg~l)4xFd%N=8wG2guu2uk&pqzqc`i_cQd|b!2TToQ0Vs(AUcITJG0{kVmgHuS_>mD6Ozvs)| z&tKaddcQ-M_i^1_VWGDT^B@-y;yL5NVRs9^3j7pqs{^LEfpH)(Qjh-fT~laGF^NkFxEep=wbU;r&Z) z(Y_2Ib4jg+rCf4I>;WO7g>~_hO@&=n6byCQ4c2q|u)lC%ipYG`TF0?WVz)9xD&Cr; zW1no#-1n=&qe57|lH!`f9h&yLDUmG?tPnRM@z7QhTnmk|m926ao(@ z`oka5MH%kZ#X95kAJH>C!Bq$VmYVm zrb4=;lz_P~`Da(A&SYCof~uQJW4)`(-b8OoE&csPOc1QKf4i1GEcBqm1!o`s{Vn5r9IHH# gYjE#>m5KFTpWOD|xpbd*UR61FS1%WabI4Es1}uSAzW@LL literal 0 HcmV?d00001 diff --git a/apps/waternet/slide-legend.png b/apps/waternet/slide-legend.png new file mode 100644 index 0000000000000000000000000000000000000000..c5498dea6a8a837463663933f11f79e8ffd7caa1 GIT binary patch literal 3370 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 Zagm+Rb^2FXGxo0vtjz39t4+|!{{l^<8R!52 literal 0 HcmV?d00001 From fa876f1e1a145e773bfc0cc13ba8071b52542294 Mon Sep 17 00:00:00 2001 From: Willems Davy Date: Thu, 7 Sep 2023 04:54:14 +0200 Subject: [PATCH 0030/2187] fix wrong extension used in screenshots in metadata.json --- apps/waternet/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/waternet/metadata.json b/apps/waternet/metadata.json index f0195b126..caed8a4e7 100644 --- a/apps/waternet/metadata.json +++ b/apps/waternet/metadata.json @@ -4,7 +4,7 @@ "version":"0.01", "description": "Puzzle game where water needs to flow through pipes by sliding or rotating them", "icon": "app.png", - "screenshots": [{"url":"screenshot1.jpg"},{"url":"screenshot2.jpg"},{"url":"screenshot3.jpg"}], + "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], "tags": "game", "supports" : ["BANGLEJS2"], "readme": "README.md", From a17366111fcc94021e7923e357781b0c421174df Mon Sep 17 00:00:00 2001 From: Willems Davy Date: Thu, 7 Sep 2023 05:04:53 +0200 Subject: [PATCH 0031/2187] set first screenshot as an ingame screenshot instead of titlescreen --- apps/waternet/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/waternet/metadata.json b/apps/waternet/metadata.json index caed8a4e7..65fa13c20 100644 --- a/apps/waternet/metadata.json +++ b/apps/waternet/metadata.json @@ -4,7 +4,7 @@ "version":"0.01", "description": "Puzzle game where water needs to flow through pipes by sliding or rotating them", "icon": "app.png", - "screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}], + "screenshots": [{"url":"screenshot2.png"},{"url":"screenshot1.png"},{"url":"screenshot3.png"}], "tags": "game", "supports" : ["BANGLEJS2"], "readme": "README.md", From 9f2fe22838ba84d3863869d4f6780fed06b2f3cc Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 7 Sep 2023 10:27:19 +0100 Subject: [PATCH 0032/2187] fix #2996 with new 'core' --- core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core b/core index 431a3fb74..37a22e0c4 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 431a3fb743da5c370729ab748cb2c177e70a345b +Subproject commit 37a22e0c49666ec3947ad0daaf5f5675101ca485 From 0bb063f19d68f9a72c544632367143f93f020f78 Mon Sep 17 00:00:00 2001 From: Gordon Williams Date: Thu, 7 Sep 2023 10:49:03 +0100 Subject: [PATCH 0033/2187] recommend GPS --- apps/assistedgps/custom.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/assistedgps/custom.html b/apps/assistedgps/custom.html index 30cdb3eed..994f6d053 100644 --- a/apps/assistedgps/custom.html +++ b/apps/assistedgps/custom.html @@ -31,7 +31,7 @@