diff --git a/README.md b/README.md index 38ce09f75..a6c449cc7 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer. }, ``` +### Screenshots + +In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { url:"screenshot.png" } ],` + +To get a screenshot you can: + +* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then +right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is +unable to read data back from the LCD controller). +* Run your code in the emulator and use the screenshot button in the bottom right of the window. + + ## Testing ### Online diff --git a/apps/alpinenav/app-icon.js b/apps/alpinenav/app-icon.js index dba084202..6708ee67f 100644 --- a/apps/alpinenav/app-icon.js +++ b/apps/alpinenav/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mUywkEIf4A/AHUBiAYWgcwDC0v+IYW///C6sC+c/kAYUj/xj/wDCgvBgfyVihhBAQQASh6TCMikvYoRkU/73CMicD+ZnFViJFBj5MBMiU/+IuBJoJkRCoUvfIPy/5kQVgM//7gBC4KCDFxSsDgTHCl8QWgaRKmBJBFIzmDSJXzYBECWobbJAAKNIMhYlBOoK/IMhZXCmYMLABAkCS4RkSXZoNJRBo/CgK6UBwTWBBIs/SJBAGl7UFegIXMaogHEehAAHj/yIYsfehAAGMQISFMRxbCiEDU4ZiQZY5iQZYpiSbQ8/cwzLOCiQA/AH4A1A")) \ No newline at end of file +require("heatshrink").decompress(atob("mEkgIRO4AFJgPgAocDAoswAocHAokGjAFDhgFFhgFDjEOAoc4gxSE44FDuPjAod//+AAoXfn4FCgPMjJUCmIJBAoU7AoJUCv4CBsACBtwCBuACB4w3CEQIaCKgMBFgQFBgYFCLQMDMIfAg55D4BcDg/gNAcD+B0DSIMcOgiGEjCYEjgFEhhVCUgQ")) diff --git a/apps/arrow/icon.js b/apps/arrow/icon.js index 380728484..917a5c979 100644 --- a/apps/arrow/icon.js +++ b/apps/arrow/icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mUywIebg/4AocP//AAoUf//+BYgMDh/+j/8Dol/wEAgYFBg/wgEBFIV+AQIVCh4fBnwFBgISBj8AhgJCh+Ag4BB4ED8ED+ASCAYJDBnkAvkAIYIWBjw8B/EB8AcBn//gF4DwJdBAQMA/EP738FYM8g/nz+A+EPgHx8YKBgfAjF4sAKBHIItBBQJMBFoJEBHII1BIQIDCvAUCAYYUBHIIDBMIXACgQpBRAIUBMIIrBDAIWCVYaiBTYQJCn4FBQgIIBEYKrDQ4MBVYUf8CQCCoP/w6DBAAKIBAocHAoIwBBgb5DDoYAZA=")) +require("heatshrink").decompress(atob("kkkwIEBgf8AYMB//4AgN///ggEf4E/wED+EACQN8C4Pgh4TBh8BCYMAvEcEoWD4AEBnk4gFggPHwAXBj1wgIwB88An/Ah3gg/+gF+gH/+EH8Ef/+ABAPvuAIBgnyCIQjBBAMAJAIIEuAICFgIIBh14BAMB8eAg0Ajk8KAXBKAU4jwDBg+ADoIXBg4NBnxPBEgPAgP8gZaBg//KoKLBKAIEBMQMAA")) diff --git a/apps/astral/app-icon.js b/apps/astral/app-icon.js index 19d0998ff..d10e7a498 100644 --- a/apps/astral/app-icon.js +++ b/apps/astral/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mUyxH+AH4AG3YAGF1w0oExYykEZwyhEIyRJGUAfEYpgxjLxQNEGEajMGTohPGMBTQOZwwTGKoyXDASVWGSwtHKYYAJZbYVEGR7bSGKQWkDRQbOCAoxYRI4wMCIYxXXpQSYP6L4NCRLGXLZwdVMJwAWGKgwbD6aUTSzoRKfCAxbAogcJBxQx/GP4x/GP4xNAAoKKBxwxaGRQZPSqwZmGOZ7VY8oxnPZoJPGP57TBJavWGL7gRRaiPVGJxRGBJgxcACYxfHJIRLSrTHxGODHvGSgwcAEY=")) \ No newline at end of file +require("heatshrink").decompress(atob("kUw4MA///xP5gEH/AMBh//4AHBwF4gEDwEHgEB4fw8EAsf/jEAjPh80AhngjnAgcwAIMB5kA50A+cAmfAtnAhnYmc//8zhln/+c4YjBg0w440Bxk38EB/cP/0B//Dwf/+FxwEf8EGIAJGB2BkCnhiB4EPgF//EDFQIpB+HGgOMnkxwFjh8MsEY4YQHn/x//j//8n/wHYItBCAKFBhgKBKAIQBBgIQC4AQCmAQChkD/v8gcA/wCBBoMA7+39kAPwP/WIMP4aYBCAYhCCAkHAYOAA=")) diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog index e52015f04..e9b98e08c 100644 --- a/apps/bledetect/ChangeLog +++ b/apps/bledetect/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fixed issue with wrong device informations 0.03: Ensure manufacturer:undefined doesn't overflow screen +0.04: Set Bangle.js 2 compatible, show widgets diff --git a/apps/bledetect/bledetect.js b/apps/bledetect/bledetect.js index ca8699f9a..f3fc70e92 100644 --- a/apps/bledetect/bledetect.js +++ b/apps/bledetect/bledetect.js @@ -5,6 +5,7 @@ let menu = { function showMainMenu() { menu["< Back"] = () => load(); + Bangle.drawWidgets(); return E.showMenu(menu); } @@ -55,5 +56,6 @@ function waitMessage() { E.showMessage("scanning"); } +Bangle.loadWidgets(); scan(); waitMessage(); diff --git a/apps/bledetect/metadata.json b/apps/bledetect/metadata.json index f5e0ffb19..0c30fe8f6 100644 --- a/apps/bledetect/metadata.json +++ b/apps/bledetect/metadata.json @@ -2,11 +2,11 @@ "id": "bledetect", "name": "BLE Detector", "shortName": "BLE Detector", - "version": "0.03", + "version": "0.04", "description": "Detect BLE devices and show some informations.", "icon": "bledetect.png", "tags": "app,bluetooth,tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"bledetect.app.js","url":"bledetect.js"}, diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index e75c9cc7b..ef437fe3b 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -47,3 +47,5 @@ 0.41: Add Keyboard and Mouse Bluetooth HID option 0.42: Sort *.boot.js files lexically and by optional numeric priority, e.g. appname..boot.js 0.43: Fix Gadgetbridge handling with Programmable:off +0.44: Write .boot0 without ever having it all in RAM (fix Bangle.js 1 issues with BTHRM) +0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk) diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index c9bd89f51..c2ed5458d 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -4,7 +4,7 @@ of the time. */ E.showMessage("Updating boot0..."); var s = require('Storage').readJSON('setting.json',1)||{}; var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 -var boot = ""; +var boot = "", bootPost = ""; if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; @@ -15,6 +15,7 @@ if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't chang boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; boot += `E.setFlags({pretokenise:1});\n`; boot += `var bleServices = {}, bleServiceOptions = { uart : true};\n`; +bootPost += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; // executed after other boot code if (s.ble!==false) { if (s.HID) { // Human interface device if (s.HID=="joy") boot += `Bangle.HID = E.toUint8Array(atob("BQEJBKEBCQGhAAUJGQEpBRUAJQGVBXUBgQKVA3UBgQMFAQkwCTEVgSV/dQiVAoECwMA="));`; @@ -195,8 +196,8 @@ if (!Bangle.appRect) { // added in 2v11 - polyfill for older firmwares // Append *.boot.js files // These could change bleServices/bleServiceOptions if needed -var getPriority = /.*\.(\d+)\.boot\.js$/; -require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ +var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ + var getPriority = /.*\.(\d+)\.boot\.js$/; var aPriority = a.match(getPriority); var bPriority = b.match(getPriority); if (aPriority && bPriority){ @@ -207,17 +208,39 @@ require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ return 1; } return a==b ? 0 : (a>b ? 1 : -1); -}).forEach(bootFile=>{ +}); +// precalculate file size +var fileSize = boot.length + bootPost.length; +bootFiles.forEach(bootFile=>{ + // match the size of data we're adding below in bootFiles.forEach + fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2; +}); +// write file in chunks (so as not to use up all RAM) +require('Storage').write('.boot0',boot,0,fileSize); +var fileOffset = boot.length; +bootFiles.forEach(bootFile=>{ // we add a semicolon so if the file is wrapped in (function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() // which would cause an error! - boot += "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; + // we write: + // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; + // but we need to do this without ever loading everything into RAM as some + // boot files seem to be getting pretty big now. + require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset); + fileOffset+=2+bootFile.length+1; + var bf = require('Storage').read(bootFile); + require('Storage').write('.boot0',bf,fileOffset); + fileOffset+=bf.length; + require('Storage').write('.boot0',";\n",fileOffset); + fileOffset+=2; }); -// update ble -boot += `NRF.setServices(bleServices, bleServiceOptions);delete bleServices,bleServiceOptions;\n`; -// write file -require('Storage').write('.boot0',boot); +require('Storage').write('.boot0',bootPost,fileOffset); + delete boot; +delete bootPost; +delete bootFiles; +delete fileSize; +delete fileOffset; E.showMessage("Reloading..."); eval(require('Storage').read('.boot0')); // .bootcde should be run automatically after if required, since diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index edddc6b41..c21ab6833 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.43", + "version": "0.45", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog index b7e18abe3..4f48bdd14 100644 --- a/apps/crowclk/ChangeLog +++ b/apps/crowclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.03: Fix the clock for dark mode. diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js index d06369fa8..eee1653cb 100644 --- a/apps/crowclk/crow_clock.js +++ b/apps/crowclk/crow_clock.js @@ -76,7 +76,7 @@ function draw_clock(){ // g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y); // g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius); - g.setColor(g.theme.fg); + g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg); let ticks = [0, 90, 180, 270]; ticks.forEach((item)=>{ let agl = item+180; @@ -92,13 +92,13 @@ function draw_clock(){ let minute_agl = minute_angle(date); g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); // g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); // - g.setColor(g.theme.fg); + g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg); g.fillCircle(clock_center.x, clock_center.y, 6); - g.setColor(g.theme.bg); + g.setColor(g.theme.dark ? g.theme.fg : g.theme.bg); g.fillCircle(clock_center.x, clock_center.y, 3); // draw minute ticks. Takes long time to draw! - g.setColor(g.theme.fg); + g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg); for (var i=0; i<60; i++){ let agl = i*6+180; g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180}); diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json index 752e30fb0..6985cf11a 100644 --- a/apps/crowclk/metadata.json +++ b/apps/crowclk/metadata.json @@ -1,7 +1,7 @@ { "id": "crowclk", "name": "Crow Clock", - "version": "0.02", + "version": "0.03", "description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face", "icon": "crow_clock.png", "screenshots": [{"url":"screenshot_crow.png"}], diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 811784b39..da07af798 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -8,3 +8,4 @@ 0.08: Optimize line wrapping for Bangle 2 0.09: fix the trasparent widget bar if there are no widgets for Bangle 2 0.10: added "one click exit" setting for Bangle 2 +0.11: Fix bangle.js 1 white icons not displaying diff --git a/apps/dtlaunch/app-b1.js b/apps/dtlaunch/app-b1.js index ec0569127..ed9cc778e 100644 --- a/apps/dtlaunch/app-b1.js +++ b/apps/dtlaunch/app-b1.js @@ -48,6 +48,7 @@ function draw_icon(p,n,selected) { var x = (n%3)*80; var y = n>2?130:40; (selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89); + g.setColor(g.theme.fg); g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25}); g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1); var txt = apps[p*6+n].name.split(" "); diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 7a4094e54..b3f94442f 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.10", + "version": "0.11", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/game1024/ChangeLog b/apps/game1024/ChangeLog new file mode 100644 index 000000000..ffb1f94bc --- /dev/null +++ b/apps/game1024/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version +0.02: Temporary intermediate version +0.03: Basic colors +0.04: Bug fix score reset after Game Over, new icon +0.05: Chevron marker on the randomly added square \ No newline at end of file diff --git a/apps/game1024/README.md b/apps/game1024/README.md new file mode 100644 index 000000000..500453145 --- /dev/null +++ b/apps/game1024/README.md @@ -0,0 +1,36 @@ + +# Play the game of 1024 + +Move the tiles by swiping to the lefthand, righthand or up- and downward side of the watch. + +When two tiles with the same number are squashed together they will add up as exponentials: + +**1 + 1 = 2** or **A + A = D** which is a representation of **2^1 + 2^1 = 2^1 = 4** + +**2 + 2 = 3** or **B + B = C** which is a representation of **2^2 + 2^2 = 2^3 = 8** + +**3 + 3 = 4** or **C + C = D** which is a representation of **2^3 + 2^3 = 2^4 = 16** + +After each move a new tile will be added on a random empty square. The value can be 1 or 2, and will be marked with a chevron. + +So you can continue till you reach **1024** which equals **2^(10)**. So when you reach tile **10** you have won. + +The score is maintained by adding the outcome of the sum of all pairs of squashed tiles (4+16+4+8 etc.) + +Use the side **BTN** to exit the game, score and tile positions will be saved. + +## Buttons on the screen + + - Button **U**: Undo the last move. There are currently a maximum of 4 undo levels. The level is indicated with a small number in the lower righthand corner of the Undo button + - Button **\***: Change the text on the tile to number, capitals or Roman numbers + - Button **R**: Reset the game. The Higscore will be remembered. You will be prompted first. + +### Credits + +Game 1024 is based on Saming's 2048 and Misho M. Petkovic 1024game.org and conceptually similar to Threes by Asher Vollmer. + +In Dark theme with numbers: +![Screenshot from the Banglejs 2 watch with the game in dark theme](./game1024_sc_dump_dark.png) + +In Light theme with characters: +![Screenshot from the Banglejs 2 watch with the game in light theme](./game1024_sc_dump_light.png) \ No newline at end of file diff --git a/apps/game1024/app-icon.js b/apps/game1024/app-icon.js new file mode 100644 index 000000000..8e8b56d9f --- /dev/null +++ b/apps/game1024/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBkQAWkAyVgQXx5gAMCQOqAAeiC/4X/AAXdC6HP7gECn///oXH///+QXEn4XC4f/mf/AwQXEmczmQXD74QD7/8AQZHLFIPfC4QzC4ZICC5XPngXD/4CB5oXNIYQXG+YXSCYQXKkQXWU4oXbL5mjC5M/R5evC5PfniwBa5Gvd4gXE5/z7s/DQIXGl6PJ5v//5eCC46/F4YXCAgMzAoYXFkYXFABTvMC/4X0ACkCC/4XJu4AMCQOIAAeCC+0///zC6dz/8z/83C6V/CgN/+4XSn4DCF6ZcGC6Hyv53V+Z3WCgR3OkQAWA=")) \ No newline at end of file diff --git a/apps/game1024/app.js b/apps/game1024/app.js new file mode 100644 index 000000000..9f6081376 --- /dev/null +++ b/apps/game1024/app.js @@ -0,0 +1,691 @@ +const debugMode = 'off'; // valid values are: off, test, production, development +const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)}; +const rows = 4, cols = 4; +const borderWidth = 6; +const sqWidth = (Math.floor(Bangle.appRect.w - 48) / rows) - borderWidth; +const cellColors = [{bg:'#00FFFF', fg: '#000000'}, + {bg:'#FF00FF', fg: '#000000'}, {bg:'#808000', fg: '#FFFFFF'}, {bg:'#0000FF', fg: '#FFFFFF'}, {bg:'#008000', fg: '#FFFFFF'}, + {bg:'#800000', fg: '#FFFFFF'}, {bg:'#00FF00', fg: '#000000'}, {bg:'#000080', fg: '#FFFFFF'}, {bg:'#FFFF00', fg: '#000000'}, + {bg:'#800080', fg: '#FFFFFF'}, {bg:'#FF0000', fg: '#FFFFFF'}]; +const cellFonts = ["12x20", "12x20", "Vector:14"]; +const cellChars = [ + [0,1,2,3,4,5,6,7,8,9,10], + ['0','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'], + ['0','I', 'II', 'III', 'IV', 'V', 'VI', 'VII','VIII', 'IX', 'X'] +]; +// const numInitialCells = 2; +const maxUndoLevels = 4; +const noExceptions = true; +let charIndex = 0; // plain numbers on the grid +const themeBg = g.theme.bg; + + +const scores = { + currentScore: 0, + highScore: 0, + lastScores: [0], + add: function(val) { + this.currentScore = this.currentScore + Math.pow(2, val); + debug(() => console.log("new score=",this.currentScore)); + }, + addToUndo: function () { + this.lastScores.push(this.currentScore); + if (this.lastScores.length > maxUndoLevels) this.lastScores.shift(); + }, + undo: function () { + this.currentScore = this.lastScores.pop(); + debug(() => console.log("undo score =", this.currentScore, "rest:", this.lastScores)); + }, + reset: function () { + this.currentScore = 0; + this.lastScores = [0]; + }, + draw: function () { + g.setColor(btnAtribs.fg); + let ulCorner = {x: Bangle.appRect.x + 6, y: Bangle.appRect.y2 -22 }; + let lrCorner = {x: Bangle.appRect.x2, y: Bangle.appRect.y2 - 1}; + g.fillRect(ulCorner.x, ulCorner.y, lrCorner.x, lrCorner.y) + .setFont12x20(1) + .setFontAlign(0,0,0); + let scrX = Math.floor((ulCorner.x + lrCorner.x)/3); + let scrY = Math.floor((ulCorner.y + lrCorner.y)/2) + 1; + g.setColor('#000000') + .drawString(this.currentScore, scrX+1, scrY+1) + .setColor(btnAtribs.bg) + .drawString(this.currentScore, scrX, scrY); + scrX = Math.floor(4*(ulCorner.x + lrCorner.x)/5); + g.setFont("6x8:1x2") + .drawString(this.highScore, btnAtribs.x + Math.floor(btnAtribs.w/2), scrY); + }, + hsContents: function () { + return {"highScore": this.highScore, "lastScore": this.currentScore}; + }, + check: function () { + this.highScore = (this.currentScore > this.highScore) ? this.currentScore : this.highScore; + debug(() => console.log('highScore =', this.highScore)); + } +}; + +// snapshot interval is the number of moves after wich a snapshot is wriiten to file +const snInterval = 1; + +const snReadOnInit = true; +// a snapshot contains a json file dump of the last positions of the tiles on the board, including the scores +const snapshot = { + interval: snInterval, + snFileName: 'game1024.json', + counter: 0, + updCounter: function() { + this.counter = ++this.counter > this.interval ? 0 : this.counter; + }, + dump: {gridsize: rows * cols, expVals: [], score: 0, highScore: 0, charIndex: charIndex}, + write: function() { + require("Storage").writeJSON(this.snFileName, this.dump); + }, + read: function () { + let sn = require("Storage").readJSON(this.snFileName, noExceptions); + if ((typeof sn == "undefined") || (sn.gridsize !== rows * cols)) { + require("Storage").writeJSON(this.snFileName, this.dump); + return false; + } else { + if ((typeof sn !== "undefined") && (sn.gridsize == rows * cols)){ + this.dump = sn; + return true; + } + } + }, + setDump: function () { + this.dump.expVals = []; + allSquares.forEach(sq => { + this.dump.expVals.push(sq.expVal); + }); + this.dump.score = scores.currentScore; + this.dump.highScore = scores.highScore; + this.dump.charIndex = charIndex; + }, + make: function () { + this.updCounter(); + if (this.counter == this.interval) { + this.setDump(); + this.write(); + debug(() => console.log("snapped the state of the game:", this.dump)); + } + }, + recover: function () { + if (this.read()) { + this.dump.expVals.forEach((val, idx) => { + allSquares[idx].setExpVal(val); + }); + scores.currentScore = this.dump.score ? this.dump.score : 0; + scores.highScore = this.dump.highScore ? this.dump.highScore : 0 ; + charIndex = this.dump.charIndex ? this.dump.charIndex : 0 ; + } + }, + reset: function () { + this.dump.gridsize = rows * cols; + this.dump.expVals = []; + for (let i = 0; i< this.dump.gridsize; i++) { + this.dump.expVals[i] = 0; + } + this.dump.score = 0; + this.dump.highScore = scores.highScore; + this.dump.charIndex = charIndex; + this.write(); + debug(() => console.log("reset D U M P E D!", this.dump)); + } +}; +const btnAtribs = {x: 134, w: 42, h: 42, fg:'#C0C0C0', bg:'#800000'}; +const buttons = { + all: [], + draw: function () { + this.all.forEach(btn => { + btn.draw(); + }); + }, + add: function(btn) { + this.all.push(btn); + } +}; +/** + * to the right = -1 + all tiles move to the left, begin with the outer righthand side tiles + moving 0 to max 3 places to the right + + find first tile beginning with bottom row, righthand side + */ + +const mover = { + direction: { + up: {name: 'up', step: 1, innerBegin: 0, innerEnd: rows-1, outerBegin: 0, outerEnd: cols-1, iter: rows -1, + sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m < rows -1 ? (m+1)*(cols) + n : -1;} + }, + down: {name: 'down', step:-1, innerBegin: rows-1, innerEnd: 0, outerBegin: cols-1, outerEnd: 0, iter: rows -1, + sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m > 0 ? (m-1)*(cols) + n : -1;} + }, + left: {name: 'left', step: 1, innerBegin: 0, innerEnd: cols-1, outerBegin: 0, outerEnd: rows-1, iter: cols -1, + sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m < cols -1 ? n*(rows) + m +1 : -1;} + }, + right: {name: 'right', step:-1, innerBegin: cols-1, innerEnd: 0, outerBegin: rows-1, outerEnd: 0, iter: cols -1, + sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m > 0 ? n*(rows) + m -1: -1;} + } + }, + anyLeft: function() { + let canContinue = false; + [this.direction.up,this.direction.left].forEach (dir => { + const step = dir.step; + // outer loop for all colums/rows + for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) { + // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up + for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) { + const idx = dir.sqIndex(m,n); + const nextIdx = dir.sqNextIndex(m,n); + if (allSquares[idx].expVal == 0) { + canContinue = true; // there is an empty cell found + break; + } + if (nextIdx >= 0) { + if (allSquares[idx].expVal == allSquares[nextIdx].expVal) { + canContinue = true; // equal adjacent cells > 0 found + break; + } + if (allSquares[nextIdx].expVal == 0) { + canContinue = true; // there is an empty cell found + break; + } + } + if (canContinue) break; + } + if (canContinue) break; + } + }); + return canContinue; + }, + nonEmptyCells: function (dir) { + debug(() => console.log("Move: ", dir.name)); + const step = dir.step; + // outer loop for all colums/rows + for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) { + // let rowStr = '| '; + + // Move a number of iteration with the squares to move them all to one side + for (let iter = 0; iter < dir.iter; iter++) { + + // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up + for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) { + // get the array of squares index for current cell + const idx = dir.sqIndex(m,n); + const nextIdx = dir.sqNextIndex(m,n); + + if (allSquares[idx].expVal == 0 && nextIdx >= 0) { + allSquares[idx].setExpVal(allSquares[nextIdx].expVal); + allSquares[nextIdx].setExpVal(0); + } + } + } + } + }, + // add up the conjacent squares with identical values en set next square to empty in the process + mergeEqlCells: function(dir) { + const step = dir.step; + // outer loop for all colums/rows + for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) { + // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up + for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) { + const idx = dir.sqIndex(m,n); + const nextIdx = dir.sqNextIndex(m,n); + + if ((allSquares[idx].expVal > 0) && nextIdx >= 0) { + if (allSquares[idx].expVal == allSquares[nextIdx].expVal) { + let expVal = allSquares[idx].expVal; + allSquares[idx].setExpVal(++expVal); + allSquares[idx].addToScore(); + allSquares[nextIdx].setExpVal(0); + } + } + } + } + } +}; +// Minimum number of pixels to interpret it as drag gesture +const dragThreshold = 10; + +// Maximum number of pixels to interpret a click from a drag event series +const clickThreshold = 3; + +let allSquares = []; +// let buttons = []; + +class Button { + constructor(name, x0, y0, width, height, text, bg, fg, cb, enabled) { + this.x0 = x0; + this.y0 = y0; + this.x1 = x0 + width; + this.y1 = y0 + height; + this.name = name; + this.cb = cb; + this.text = text; + this.bg = bg; + this.fg = fg; + this.font = "6x8:3"; + this.enabled = enabled; + } + disable() { + this.enabled = false; + } + enable() { + this.enabled = true; + } + draw() { + g.setColor(this.bg) + .fillRect(this.x0, this.y0, this.x1, this.y1) + .setFont(this.font) + .setFontAlign(0,0,0); + let strX = Math.floor((this.x0+this.x1)/2); + let strY = Math.floor((this.y0+this.y1)/2); + g.setColor("#000000") + .drawString(this.text, strX+2, strY+2) + .setColor(this.fg) + .drawString(this.text, strX, strY); + // buttons.push(this); + } + onClick() {if (typeof this.cb === 'function' && this.enabled) { + this.cb(this); + } + } +} + +class Cell { + constructor(x0, y0, width, idx, cb) { + this.x0 = x0; + this.y0 = y0; + this.x1 = x0 + width; + this.y1 = y0 + width; + this.expVal = 0; + this.previousExpVals=[]; + this.idx = idx; + this.cb = cb; + this.isRndm = false; + this.ax = x0; + this.ay = Math.floor(0.2*width+y0); + this.bx = Math.floor(0.3*width+x0); + this.by = Math.floor(0.5*width+y0); + this.cx = x0; + this.cy = Math.floor(0.8*width+y0); + } + getColor(i) { + return cellColors[i >= cellColors.length ? cellColors.length -1 : i]; + } + drawBg() { + debug(()=>console.log("Drawbg!!")); + if (this.isRndm == true) { + debug(()=>console.log('Random: (ax)', this.ax)); + g.setColor(this.getColor(this.expVal).bg) + .fillRect(this.x0, this.y0, this.x1, this.y1) + .setColor(themeBg) + .fillPoly([this.cx,this.cy,this.bx,this.by,this.ax,this.ay]); + } else { + g.setColor(this.getColor(this.expVal).bg) + .fillRect(this.x0, this.y0, this.x1, this.y1); + } + } + drawNumber() { + if (this.expVal !== 0) { + g.setFont(cellFonts[charIndex]) + .setFontAlign(0,0,0); + let char = cellChars[charIndex][this.expVal]; + let strX = Math.floor((this.x0 + this.x1)/2); + let strY = Math.floor((this.y0 + this.y1)/2); + g.setColor(this.getColor(this.expVal).fg) + .drawString(char, strX, strY); + } + } + setExpVal(val) { + this.expVal = val; + } + getIdx() {return this.idx;} + pushToUndo() { + // remember this new step + this.previousExpVals.push(this.expVal); + // keep the undo list not longer than max undo levels + if (this.previousExpVals.length > maxUndoLevels) this.previousExpVals.shift(); + } + popFromUndo() { + // take one step back + if (this.previousExpVals.length > 0) { + this.expVal = this.previousExpVals.pop(); + } + } + removeUndo() { + this.previousExpVals=[0]; + } + addToScore() {if (typeof this.cb === 'function') { + this.cb(this.expVal); + } + } + setRndmFalse() { + this.isRndm = false; + } + setRndmTrue() { + this.isRndm = true; + } + drawRndmIndicator(){ + if (this.isRndm == true) { + debug(()=>console.log('Random: (ax)', this.ax)); + g.setColor(this.getColor(0).bg) + .fillPoly(this.ax,this.ay,this.bx,this.by,this.cx,this.cy); + } + } +} + +function undoGame() { + g.clear(); + if (scores.lastScores.length > 0) { + allSquares.forEach(sq => { + sq.popFromUndo(); + sq.drawBg(); + sq.drawNumber(); + }); + scores.undo(); + scores.draw(); + buttons.draw(); + updUndoLvlIndex(); + snapshot.make(); + } + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} +function addToUndo() { + allSquares.forEach(sq => { + sq.pushToUndo(); + }); + scores.addToUndo(); +} +function addToScore (val) { + scores.add(val); + if (val == 10) messageYouWin(); +} +function createGrid () { + let cn =0; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + let x0 = borderWidth + c*(borderWidth + sqWidth) - (rows/2)*(2*borderWidth + sqWidth) + middle.x + Math.floor(sqWidth/3); + let y0 = borderWidth + r*(borderWidth + sqWidth) - (cols/2)*(2*borderWidth + sqWidth) + middle.y + Math.floor(sqWidth/3); + let cell = new Cell(x0, y0, sqWidth, c + r*cols, addToScore); + allSquares.push(cell); + } + } +} +function messageGameOver () { + const c = (g.theme.dark) ? {"fg": "#FFFFFF", "bg": "#808080"} : {"fg": "#FF0000", "bg": "#000000"}; + g.setColor(c.bg) + .setFont12x20(2).setFontAlign(0,0,0) + .drawString("G A M E", middle.x+13, middle.y-24) + .drawString("O V E R !", middle.x+13, middle.y+24); + g.setColor(c.fg) + .drawString("G A M E", middle.x+12, middle.y-25) + .drawString("O V E R !", middle.x+12, middle.y+25); +} +function messageYouWin () { + g.setColor("#1a0d00") + .setFont12x20(2) + .setFontAlign(0,0,0) + .drawString("YOU HAVE", middle.x+18, middle.y-24) + .drawString("W O N ! !", middle.x+18, middle.y+24); + g.setColor("#FF0808") + .drawString("YOU HAVE", middle.x+17, middle.y-25) + .drawString("W O N ! !", middle.x+17, middle.y+25); + Bangle.buzz(200, 1); +} +function makeRandomNumber () { + return Math.ceil(2*Math.random()); +} +function addRandomNumber() { + let emptySquaresIdxs = []; + allSquares.forEach(sq => { + if (sq.expVal == 0) emptySquaresIdxs.push(sq.getIdx()); + }); + if (emptySquaresIdxs.length > 0) { + let randomIdx = Math.floor( emptySquaresIdxs.length * Math.random() ); + allSquares[emptySquaresIdxs[randomIdx]].setExpVal(makeRandomNumber()); + allSquares[emptySquaresIdxs[randomIdx]].setRndmTrue(); + } +} +function drawGrid() { + allSquares.forEach(sq => { + sq.drawBg(); + // sq.drawRndmIndicator(); + sq.drawNumber(); + }); +} +function initGame() { + g.clear(); + // scores.read(); + createGrid(); + if (snReadOnInit) { + snapshot.recover(); + debug(() => console.log("R E C O V E R E D !", snapshot.dump)); + let sum = allSquares.reduce(function (tv, sq) {return (sq.expVal + tv) ;}, 0); + if (!sum) { + addRandomNumber(); + } + } else { + addRandomNumber(); + // addToUndo(); + } + addRandomNumber(); + drawGrid(); + scores.draw(); + buttons.draw(); + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} +function drawPopUp(message,cb) { + g.setColor('#FFFFFF'); + let rDims = Bangle.appRect; + g.fillPoly([rDims.x+10, rDims.y+20, + rDims.x+20, rDims.y+10, + rDims.x2-30, rDims.y+10, + rDims.x2-20, rDims.y+20, + rDims.x2-20, rDims.y2-40, + rDims.x2-30, rDims.y2-30, + rDims.x+20, rDims.y2-30, + rDims.x+10, rDims.y2-40 + ]); + buttons.all.forEach(btn => {btn.disable();}); + const btnYes = new Button('yes', rDims.x+16, rDims.y2-80, 54, btnAtribs.h, 'YES', btnAtribs.fg, btnAtribs.bg, cb, true); + const btnNo = new Button('no', rDims.x2-80, rDims.y2-80, 54, btnAtribs.h, 'NO', btnAtribs.fg, btnAtribs.bg, cb, true); + btnYes.draw(); + btnNo.draw(); + g.setColor('#000000'); + g.setFont12x20(1); + g.setFontAlign(-1,-1,0); + g.drawString(message, rDims.x+20, rDims.y+20); + buttons.add(btnYes); + buttons.add(btnNo); +} +function handlePopUpClicks(btn) { + const name = btn.name; + buttons.all.pop(); // remove the no button + buttons.all.pop(); // remove the yes button + buttons.all.forEach(b => {b.enable();}); // enable the remaining buttons again + debug(() => console.log("Button name =", name)); + switch (name) { + case 'yes': + resetGame(); + break; + default: + g.clear(); + drawGrid(); + scores.draw(); + buttons.draw(); + updUndoLvlIndex(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + } +} +function resetGame() { + g.clear(); + scores.reset(); + allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();sq.setRndmFalse();}); + addRandomNumber(); + addRandomNumber(); + drawGrid(); + scores.draw(); + buttons.draw(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +/** + * Function that can be used in test or development environment, or production. + * Depends on global constant debugMode + * @param {function} func function to call like console.log() + */ + const debug = (func) => { + switch (debugMode) { + case "development": + if (typeof func === 'function') { + func(); + } + break; + case "off": + default: break; + } +}; + +// Handle a "click" event (only needed for menu button) +function handleclick(e) { + buttons.all.forEach(btn => { + if ((e.x >= btn.x0) && (e.x <= btn.x1) && (e.y >= btn.y0) && (e.y <= btn.y1)) { + btn.onClick(); + debug(() => console.log(btn.name)); + } + }); +} + +// Handle a drag event (moving the stones around) +function handledrag(e) { + /*debug(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => console.log('To the right') : e => console.log('To the left') ) : + (e.dy > 0 ? e => console.log('Move down') : e => console.log('Move up') )); + */ + // [move.right, move.left, move.up, move.down] + runGame((Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? mover.direction.right : mover.direction.left ) : + (e.dy > 0 ? mover.direction.down : mover.direction.up ))); +} +// Evaluate "drag" events from the UI and call handlers for drags or clicks +// The UI sends a drag as a series of events indicating partial movements +// of the finger. +// This class combines such parts to a long drag from start to end +// If the drag is short, it is interpreted as click, +// otherwise as drag. +// The approprate method is called with the data of the drag. +class Dragger { + + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { + this.clickHandler = clickHandler; + this.dragHandler = dragHandler; + this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold); + this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold); + this.dx = 0; + this.dy = 0; + this.enabled = true; + } + + // Enable or disable the Dragger + setEnabled(b) { + this.enabled = b; + } + + // Handle a raw drag event from the UI + handleRawDrag(e) { + if (!this.enabled) + return; + this.dx += e.dx; // Always accumulate + this.dy += e.dy; + if (e.b === 0) { // Drag event ended: Evaluate full drag + if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold) + this.clickHandler({ + x: e.x - this.dx, + y: e.y - this.dy + }); // take x and y from the drag start + else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold) + this.dragHandler({ + x: e.x - this.dx, + y: e.y - this.dy, + dx: this.dx, + dy: this.dy + }); + this.dx = 0; // Clear the drag accumulator + this.dy = 0; + } + } + + // Attach the drag evaluator to the UI + attach() { + Bangle.on("drag", e => this.handleRawDrag(e)); + } +} + +// Dragger is needed for interaction during the game +var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +// Disable dragger as board is not yet initialized +dragger.setEnabled(false); + +// Nevertheless attach it so that it is ready once the game starts +dragger.attach(); + +function runGame(dir){ + addToUndo(); + updUndoLvlIndex(); + mover.nonEmptyCells(dir); + mover.mergeEqlCells(dir); + mover.nonEmptyCells(dir); + allSquares.forEach(sq => {sq.setRndmFalse();}); + addRandomNumber(); + drawGrid(); + scores.check(); + scores.draw(); + // scores.write(); + snapshot.make(); + dragger.setEnabled(true); + if (!(mover.anyLeft())) { + debug(() => console.log("G A M E O V E R !!")); + snapshot.reset(); + messageGameOver(); + } +} + +function updUndoLvlIndex() { + let x = 170; + let y = 60; + g.setColor(btnAtribs.fg) + .fillRect(x-6,y-6, 176, 67); + if (scores.lastScores.length > 0) { + g.setColor("#000000") + .setFont("4x6:2") + .drawString(scores.lastScores.length, x, y); + } +} +function incrCharIndex() { + charIndex++; + if (charIndex >= cellChars.length) charIndex = 0; + drawGrid(); +} +buttons.add(new Button('undo', btnAtribs.x, 25, btnAtribs.w, btnAtribs.h, 'U', btnAtribs.fg, btnAtribs.bg, undoGame, true)); +buttons.add(new Button('chars', btnAtribs.x, 71, btnAtribs.w, 31, '*', btnAtribs.fg, btnAtribs.bg, function(){incrCharIndex();}, true)); +buttons.add(new Button('restart', btnAtribs.x, 106, btnAtribs.w, btnAtribs.h, 'R', btnAtribs.fg, btnAtribs.bg, function(){drawPopUp('Do you want\nto restart?',handlePopUpClicks);}, true)); + +initGame(); + +dragger.setEnabled(true); + +E.on('kill',function() { + this.write(); + debug(() => console.log("1024 game got killed!")); +}); \ No newline at end of file diff --git a/apps/game1024/game1024.app.info b/apps/game1024/game1024.app.info new file mode 100644 index 000000000..b1c9d84ce --- /dev/null +++ b/apps/game1024/game1024.app.info @@ -0,0 +1,6 @@ +require("Storage").write("timer.info",{ + "id":"game1024", + "name":"1024 Game", + "src":"game1024.app.js", + "icon":"game1024.img" +}); \ No newline at end of file diff --git a/apps/game1024/game1024.json b/apps/game1024/game1024.json new file mode 100644 index 000000000..3749649ee --- /dev/null +++ b/apps/game1024/game1024.json @@ -0,0 +1 @@ +{"gridsize": 16, "expVals": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], "score": 0, "highScore": 0, "charIndex": 1} \ No newline at end of file diff --git a/apps/game1024/game1024.png b/apps/game1024/game1024.png new file mode 100644 index 000000000..c0f7eaf21 Binary files /dev/null and b/apps/game1024/game1024.png differ diff --git a/apps/game1024/game1024_sc_dump_dark.png b/apps/game1024/game1024_sc_dump_dark.png new file mode 100644 index 000000000..87577ecfa Binary files /dev/null and b/apps/game1024/game1024_sc_dump_dark.png differ diff --git a/apps/game1024/game1024_sc_dump_light.png b/apps/game1024/game1024_sc_dump_light.png new file mode 100644 index 000000000..06ada65ac Binary files /dev/null and b/apps/game1024/game1024_sc_dump_light.png differ diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json new file mode 100644 index 000000000..557d77b89 --- /dev/null +++ b/apps/game1024/metadata.json @@ -0,0 +1,17 @@ +{ "id": "game1024", + "name": "1024 Game", + "shortName" : "1024 Game", + "version": "0.05", + "icon": "game1024.png", + "screenshots": [ {"url":"screenshot.png" } ], + "readme":"README.md", + "description": "Swipe the squares up, down, to the left or right, join the numbers and get to the 10 (2^1024), J or X tile!", + "type": "app", + "tags": "game,puzzle", + "allow_emulator": true, + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"game1024.app.js","url":"app.js"}, + {"name":"game1024.img","url":"app-icon.js","evaluate":true} + ] + } diff --git a/apps/game1024/screenshot.png b/apps/game1024/screenshot.png new file mode 100644 index 000000000..8be52f8cb Binary files /dev/null and b/apps/game1024/screenshot.png differ diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js index a701ef3a9..0cecad73b 100644 --- a/apps/gbdebug/app-icon.js +++ b/apps/gbdebug/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA=")) +require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA=")) \ No newline at end of file diff --git a/apps/hidjoystick/ChangeLog b/apps/hidjoystick/ChangeLog index 5560f00bc..625daf4bb 100644 --- a/apps/hidjoystick/ChangeLog +++ b/apps/hidjoystick/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Make Bangle.js 2 compatible diff --git a/apps/hidjoystick/app.js b/apps/hidjoystick/app.js index 134814cee..69f56463d 100644 --- a/apps/hidjoystick/app.js +++ b/apps/hidjoystick/app.js @@ -1,7 +1,86 @@ -var storage = require('Storage'); +const storage = require('Storage'); +const Layout = require("Layout"); const settings = storage.readJSON('setting.json',1) || { HID: false }; +const BANGLEJS2 = process.env.HWVERSION == 2; +const sidebarWidth=18; +const buttonWidth = (Bangle.appRect.w-sidebarWidth)/2; +const buttonHeight = (Bangle.appRect.h-16)/2*0.85; // subtract text row and add a safety margin var sendInProgress = false; // Only send one message at a time, do not flood +var touchBtn2 = 0; +var touchBtn3 = 0; +var touchBtn4 = 0; +var touchBtn5 = 0; + +function renderBtnArrows(l) { + const d = g.getWidth() - l.width; + + function c(a) { + return { + width: 8, + height: a.length, + bpp: 1, + buffer: (new Uint8Array(a)).buffer + }; + } + + g.drawImage(c([0,8,12,14,255,14,12,8]),d,g.getHeight()/2); + if (!BANGLEJS2) { + g.drawImage(c([16,56,124,254,16,16,16,16]),d,40); + g.drawImage(c([16,16,16,16,254,124,56,16]),d,194); + } +} + +const layoutChilden = []; +if (BANGLEJS2) { // add virtual buttons in display + layoutChilden.push({type:"h", c:[ + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN2", id:"touchBtn2" }, + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN3", id:"touchBtn3" }, + ]}); +} +layoutChilden.push({type:"h", c:[ + {type:"txt", font:"6x8:2", label:"Joystick" }, +]}); +if (BANGLEJS2) { // add virtual buttons in display + layoutChilden.push({type:"h", c:[ + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN4", id:"touchBtn4" }, + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN5", id:"touchBtn5" }, + ]}); +} + +const layout = new Layout( + {type:"h", c:[ + {type:"v", width:Bangle.appRect.w-sidebarWidth, c: layoutChilden}, + {type:"custom", width:18, height: Bangle.appRect.h, render:renderBtnArrows } + ]} +); + +function isInBox(box, x, y) { + return x >= box.x && x < box.x+box.w && y >= box.y && y < box.y+box.h; +} + +if (BANGLEJS2) { + Bangle.on('drag', function(event) { + if (event.b == 0) { // release + touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn2, event.x, event.y)) { + touchBtn2 = 1; + touchBtn3 = touchBtn4 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn3, event.x, event.y)) { + touchBtn3 = 1; + touchBtn2 = touchBtn4 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn4, event.x, event.y)) { + touchBtn4 = 1; + touchBtn2 = touchBtn3 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn5, event.x, event.y)) { + touchBtn5 = 1; + touchBtn2 = touchBtn3 = touchBtn4 = 0; + } else { + // outside any buttons, release all + touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0; + } + }); +} const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) { try { @@ -20,31 +99,17 @@ const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) { function drawApp() { g.clear(); - g.setFont("6x8",2); - g.setFontAlign(0,0); - g.drawString("Joystick", 120, 120); - const d = g.getWidth() - 18; - - function c(a) { - return { - width: 8, - height: a.length, - bpp: 1, - buffer: (new Uint8Array(a)).buffer - }; - } - - g.drawImage(c([16,56,124,254,16,16,16,16]),d,40); - g.drawImage(c([16,16,16,16,254,124,56,16]),d,194); - g.drawImage(c([0,8,12,14,255,14,12,8]),d,116); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + layout.render(); } function update() { - const btn1 = BTN1.read(); - const btn2 = BTN2.read(); - const btn3 = BTN3.read(); - const btn4 = BTN4.read(); - const btn5 = BTN5.read(); + const btn1 = BTN1 ? BTN1.read() : 0; + const btn2 = !BANGLEJS2 ? BTN2.read() : touchBtn2; + const btn3 = !BANGLEJS2 ? BTN3.read() : touchBtn3; + const btn4 = !BANGLEJS2 ? BTN4.read() : touchBtn4; + const btn5 = !BANGLEJS2 ? BTN5.read() : touchBtn5; const acc = Bangle.getAccel(); var x = acc.x*-127; var y = acc.y*-127; diff --git a/apps/hidjoystick/metadata.json b/apps/hidjoystick/metadata.json index e2b78a97b..c13ae2efa 100644 --- a/apps/hidjoystick/metadata.json +++ b/apps/hidjoystick/metadata.json @@ -2,11 +2,11 @@ "id": "hidjoystick", "name": "Bluetooth Joystick", "shortName": "Joystick", - "version": "0.01", - "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.", + "version": "0.02", + "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5. On Bangle.js 2 buttons 2-5 are emulated with the touchscreen.", "icon": "app.png", "tags": "bluetooth", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"hidjoystick.app.js","url":"app.js"}, {"name":"hidjoystick.img","url":"app-icon.js","evaluate":true} diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index 7d8fecb1e..9d43f6575 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -15,4 +15,5 @@ 0.15: Using wpedom to count steps. 0.16: Improved stability. Wind can now be shown. 0.17: Settings for mph/kph and other minor improvements. -0.18: Fullscreen mode can now be enabled or disabled in the settings. \ No newline at end of file +0.18: Fullscreen mode can now be enabled or disabled in the settings. +0.19: Alarms can not go bigger than 100. diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index 7d5da2d8e..433d33427 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -626,7 +626,7 @@ Bangle.on('charging',function(charging) { function increaseAlarm(){ - if(isAlarmEnabled()){ + if(isAlarmEnabled() && getAlarmMinutes() < 95){ settings.alarm += 5; } else { settings.alarm = getCurrentTimeInMinutes() + 5; diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json index e6ca10f79..1335b8e1d 100644 --- a/apps/lcars/metadata.json +++ b/apps/lcars/metadata.json @@ -3,7 +3,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.18", + "version":"0.19", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog index 939ac3b5d..040e62671 100644 --- a/apps/mmind/ChangeLog +++ b/apps/mmind/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Make sure to reset turns diff --git a/apps/mmind/metadata.json b/apps/mmind/metadata.json index c2ed474b6..ea970ee23 100644 --- a/apps/mmind/metadata.json +++ b/apps/mmind/metadata.json @@ -3,7 +3,7 @@ "name": "Classic Mind Game", "shortName":"Master Mind", "icon": "mmind.png", - "version":"0.01", + "version":"0.02", "description": "This is the classic game for masterminds", "screenshots": [{"url":"screenshot_mmind.png"}], "type": "app", diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js index e7def025d..10d315285 100644 --- a/apps/mmind/mmind.app.js +++ b/apps/mmind/mmind.app.js @@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) { break; case 4: //new game + turn = 0; play = [-1,-1,-1,-1]; game = []; endgame=false; @@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) { game = []; get_secret(); draw(); -//Bangle.loadWidgets(); -//Bangle.drawWidgets(); - - - - - diff --git a/apps/recorder/app.js b/apps/recorder/app.js index e5e732fe3..99252e0e2 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -219,7 +219,7 @@ function viewTrack(filename, info) { f.erase(); viewTracks(); } else - viewTrack(n, info); + viewTrack(filename, info); }); }; menu['< Back'] = () => { viewTracks(); }; diff --git a/apps/run/ChangeLog b/apps/run/ChangeLog index 032ebdc1a..401a68de9 100644 --- a/apps/run/ChangeLog +++ b/apps/run/ChangeLog @@ -8,3 +8,4 @@ 0.07: Fix crash if an odd number of active boxes are configured (fix #1473) 0.08: Added support for notifications from exstats. Support all stats from exstats 0.09: Fix broken start/stop if recording not enabled (fix #1561) +0.10: Don't allow the same setting to be chosen for 2 boxes (fix #1578) diff --git a/apps/run/metadata.json b/apps/run/metadata.json index 01a85ed05..51239d297 100644 --- a/apps/run/metadata.json +++ b/apps/run/metadata.json @@ -1,6 +1,6 @@ { "id": "run", "name": "Run", - "version":"0.09", + "version":"0.10", "description": "Displays distance, time, steps, cadence, pace and more for runners.", "icon": "app.png", "tags": "run,running,fitness,outdoors,gps", diff --git a/apps/run/settings.js b/apps/run/settings.js index 29a2f43cc..949f7a235 100644 --- a/apps/run/settings.js +++ b/apps/run/settings.js @@ -42,6 +42,11 @@ value: Math.max(statsIDs.indexOf(settings[boxID]),0), format: v => statsList[v].name, onchange: v => { + for (var i=1;i<=6;i++) + if (settings["B"+i]==statsIDs[v]) { + settings["B"+i]=""; + boxMenu["Box "+i].value=0; + } settings[boxID] = statsIDs[v]; saveSettings(); }, @@ -60,7 +65,7 @@ '': { 'title': 'Run' }, '< Back': back, }; - if (WIDGETS["recorder"]) + if (global.WIDGETS&&WIDGETS["recorder"]) menu[/*LANG*/"Record Run"] = { value : !!settings.record, format : v => v?/*LANG*/"Yes":/*LANG*/"No", @@ -87,7 +92,7 @@ notificationsMenu[/*LANG*/"Dist Pattern"] = { value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.dist.notifications))), min: 0, max: vibPatterns.length, - format: v => vibPatterns[v]||"Off", + format: v => vibPatterns[v]||/*LANG*/"Off", onchange: v => { settings.notify.dist.notifications = vibTimes[v]; sampleBuzz(vibTimes[v]); @@ -97,7 +102,7 @@ notificationsMenu[/*LANG*/"Step Pattern"] = { value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.step.notifications))), min: 0, max: vibPatterns.length, - format: v => vibPatterns[v]||"Off", + format: v => vibPatterns[v]||/*LANG*/"Off", onchange: v => { settings.notify.step.notifications = vibTimes[v]; sampleBuzz(vibTimes[v]); @@ -107,7 +112,7 @@ notificationsMenu[/*LANG*/"Time Pattern"] = { value: Math.max(0,vibPatterns.findIndex((p) => JSON.stringify(p) === JSON.stringify(settings.notify.time.notifications))), min: 0, max: vibPatterns.length, - format: v => vibPatterns[v]||"Off", + format: v => vibPatterns[v]||/*LANG*/"Off", onchange: v => { settings.notify.time.notifications = vibTimes[v]; sampleBuzz(vibTimes[v]); diff --git a/apps/s7clk/icon.js b/apps/s7clk/icon.js index d5d9aaf68..dbb4fc6d3 100644 --- a/apps/s7clk/icon.js +++ b/apps/s7clk/icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mUygP/AC5BlH4MAn/gAwN/4EP/AFBsEMhkBwEAjEDgYJBgEGgHA4EYDwOAmEwBIIYyj/wgf+AoMH/kA/4eBJXwYLVxgAjh//AC3w")) +require("heatshrink").decompress(atob("mEqgInkn/gg/8Ao/AjEYgYF/AoZT/Kb4AiA=")) diff --git a/apps/s7clk/icon.png b/apps/s7clk/icon.png index cb08aec5e..cfb1c0349 100644 Binary files a/apps/s7clk/icon.png and b/apps/s7clk/icon.png differ diff --git a/apps/seiko-5actus/ChangeLog b/apps/seiko-5actus/ChangeLog new file mode 100644 index 000000000..978e5d6ea --- /dev/null +++ b/apps/seiko-5actus/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Release +0.02: Shrink hand images to save memory diff --git a/apps/seiko-5actus/README.md b/apps/seiko-5actus/README.md new file mode 100644 index 000000000..4f09bf3c6 --- /dev/null +++ b/apps/seiko-5actus/README.md @@ -0,0 +1,16 @@ +# Seiko 5actus + +![](screenshot.png) + +This is built on the knowledge of what I gained through designing the rolex watch face and improves on it by getting more done right at the start. + +This watch is modeled after one I personally own and love, I have spent quite a bit of time designing this in a pixel art editor to try and make it as clean as possible and am quite happy with how it came out. + +This watch face works in both the light and dark themes but I personally think it looks a lot cleaner in the dark them. + +This watch whilst technically designed in a way that would work with the BangleJs has been only listed to work with the BangleJs2, if someones wants to test it on a first gen and let me know if it works then i'll allow it to be installed on both devices but I assume with how the images have been designed it would look strange on a first gen watch. + +Special thanks to: +* rozek (for his updated widget draw code for utilization with background images) +* Gordon Williams (Bangle.js, watchapps for reference code and documentation) +* The community (for helping drive such a wonderful project) diff --git a/apps/seiko-5actus/app-icon.js b/apps/seiko-5actus/app-icon.js new file mode 100644 index 000000000..796f24122 --- /dev/null +++ b/apps/seiko-5actus/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkEBpMB+fziAjRCQQXBHyoXEgIRLgMwC5EAj8gC5MC+QXJn4XKBgJHJMhkfJAYXEh/xC5cDBofzJocvIxKiCHpBGNExMCIxi2KeRIJFgMiiYBCkQ1Jh67EAAMSCgICBiQjFn8xDYX/AgQANn4qEgf/JIcDkcxiUSiMRY4cv+ZaFj6bDgZGBkMRDIIXD/7CHn5TDFYIADFIcRSxgAvXQwAQgRyDACcje4wAESQ4RDmMQgSGBj8zAAnyTgauH/65Cj7EBkMicAPyBYIABCgcRkYWCmYvCewMhmUiiMyF4gUBDoXzn7/Dj4RBF4IXB+QrDj40DmJgBiEyBYMDL4qcEgUikYqDCgSGKAAUSn8hbh8BgICBl/yFggwEbhMC/4sHAAcTIhIsBGYLcJGAQOHgLAEGBM/Jg0vRgsQZAMQBAQUBif/AwLJDfoQ1DkDOBkIOCgQDBFAKCDaIRPGSwqGBgM/dwUDeQiZEEAowCgXxb5jGGgQ+GaAjaHeIbmISYToHRYTPKYQIODE4cfEA4AEPghtEgQJCI5Mv+AXHJAiIIBggXFj/xDBMCcAYXGga7LcYIXIUpY1GC4SiJVRAXCiAWJA")) diff --git a/apps/seiko-5actus/app.js b/apps/seiko-5actus/app.js new file mode 100644 index 000000000..078b6e5c2 --- /dev/null +++ b/apps/seiko-5actus/app.js @@ -0,0 +1,181 @@ +var imgBg = { + width : 176, height : 176, bpp : 2, + transparent : 1, + buffer : require("heatshrink").decompress(atob("qoASv/8//1C6YASrorD6opjuorHBAIriQYwriV9FXFZldFjrSEFY6FjQcorPSYqtZFZaxaVoiDkZRArLv4rV/orT/5rGABtf/4rSq//+79UFaptIK5puHFZQUBFda5IABZuBCw4rKTAPVq4rUNw4rK/4rBroqRQAIrVQSd1N4QrQNZIAOFdbzBQ4IrOCQIIDWKQYFFZhqBHwaeBACBwBFazZQThQrJ/7CHABaSEFaTaWOIYrONJArTToorIdpDdRDQLJOCBDhRIxBoPABdXAwwrRVKQ+GZR7aZDYRHNM4IraOZyTQZbQrKaIwAKr7LOHRNdFaJzOr56RuppZACBgRAEtW1Wl1WqzWm1Nqy2qC5hyTrWq1IrFzWqyoXLv7mFQRlqqtp0tVy2V0uptNVA4JWK/72Fr4yFFY9VLINWyqJBFZtfFYwGGQZIrS////peF/6xM1KDDQQIrMKwJQFA4SEKQYLbGAoIrKv5PGGY4rGEYIAHyrZKQQoIDQhoARKwR6GBJIAXJpKECMIoAXUpaELBYQAICZR4CPYo3Kq4IBr7RITIzZFq7mOGwXXBYIjB//3/9drt/+5AGZ4RKHuoNEFZVdupQBuorBupjCJIyNIrqELr4mBr99vorEK476PBxAYC79//orCr4QBu5XIXAwiIcwxXCKYJlBFYSvIQRI7HTiLjBFYzZHAAzdBDA4AKDY6CMQgYQOABT8CEQjdJFTAAKuo8MADokkAH4A/AH4A/ABn1FldXFlfVTX4A/AH4A/AH4APr//ABVVrorcv4rL6tXFbgqL//1Qbv/1QAE1AED14re/wrK1Yr/Ff4rEcY3VFf4r/Ff4r/EaoAH/4rK14r/FZYALFb1/FZbTWAA9fFZYNBFjoA/AH4A/AH4A/vots+pu/AH4A/AH4A/ADdX6ousr4GFrohgABP/FbN/+o7O/6NZv5HOB4IrZ///LBlXB5wrO/qCNB5pzOOhgNBK7RICDhQNCX5P96td/91u4FBvpJLAoQPDrplEQRNdFYPVu93qvVO5JYJurZDSJV1FYP9FYP16oXBfJRKIbJpQB7vVv/3AwJvCbpTZVVIP9/9d6/9AALdTbJgAVEJDZMACoiCLAjZNAAqSKbpjZNSoo7PQg4zCIx9/FaBQBJ4rZRHqJQBFYzZRLCN/HooYRCQIRQYQ1dDCFVQR4A/ADFXCSK5RDJ40Iq6nPW6LcIq//fh39SrNf/6EN/47OFZp0NFbd/K5wPPI5obNM5F1FaYPNdYLcGfpA3IDQIrXABZ6FDSLcZTxAIBW4zcBFa4ZBFaLsNFZYZGFZBpIACCdBFZ7BFq7dSZJArOfQwAHHQYYBFaA+JABwhBIggrObirIJFZLuBbioXJFZI/JWJorBEJIJJS4KFRrqbKFZLvDupYSeZIrJbiwrBNwIrnTQYrReBIrNCpArKBRQAKIJQrLNhCvNJidXQSZYCPCiCTIQS6KEI4pVAAddFaF1FbAZHfioAVFYyUJWbRXHLrqxFFYhVeLI4rq6orCMAoAhFa4")) +}; + +/* Set hour hand image */ + +var imgHour = { + width : 14, height : 114, bpp : 2, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AH4A/AB8P/4DB//wAz8D//8BIIKBn4DB54CBACPzAQP8EoImBD4PAJkQG/A34GIgbUBA")) +}; + +/* Set minute hand image */ + +var imgMin = { + width : 4, height : 168, bpp : 2, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AH4AE/4A/AEI")) +}; + +/* Set second hand image */ + +var imgSec = { + width : 6, height : 176, bpp : 2, + transparent : 1, + buffer : require("heatshrink").decompress(atob("qoA/ADFf6v9AU1c6vNFlICWvtXAXlVA=")) +}; + +/* Sets the font for the date at an appropriate size of 14 */ + +Graphics.prototype.setFontWorkSans = function(scale) { + // Actual height 12 (12 - 1) + this.setFontCustom(atob("AAAAAAADwAAAPAAAAAAAAAvAAC/0AH/gAL/QAD9AAAIAAAAACQAAL/9AC/r9APAA8A8ADwDwAfAH//wAH/8AAAAAADwAAAtAAAH//8Af//wAAAAAAAAAABwAUAfgHwDwA/APAP8A8DzwD/9PAD/Q8AAABQAAA0AHwDwA9AHwDw4PAPDw8A///gA/f8AAAAAAAAQAAAvAAAP8AALzwAD8PAAv//wB///AAAPAAAAUAAAAAAC/88AP/y8A8NDwDywPAPD18A8L/AAAGgAAAAAAC/8AA//9ALTx8A8ODwDw4PALz/8ALD/AAAAAAPAAAA8AAADwB/APC/8A9/gAD/AAAPgAAAAAAAAAAAAB8fwAf//wDw8PAPDw8A8PDwB///AC8vwAAAAAAAAAAD/DgAv/PQDwsPAPC08A8PDwB//8AB//QAAAAAAAAAAA8DwADwPAABAE"), 46, atob("BAYJBggICQgJCAkJBA=="), 14+(scale<<8)+(2<<16)); + return this; +}; + +/* Sets the font for the day at an appropriate size of 12 */ + +Graphics.prototype.setFontWorkSansSmall = function(scale) { + // Actual height 12 (11 - 0) + this.setFontCustom(atob("AAAAAAAAAAAAAAAAAABAP/zwGqjQAAAAP0AAEAAAP4AAGAAAAEoAAs/gL//QL88AAt/gL/+QK88AAYAAAUFAD+PAHPDgv//8fr70Hj7QCx+AAAAAC8AAL/AAPHBgL/PQB68AAPgAC9/APT7wEDjwAC/QAAAAAAuAC7/gL/DwPPywL9/gCwvAAD7wAABgAAAAP0AAEAAAACkAC//wP0C8sAAPYAAGPQA+D//0Af9ABQAADzAAB/AAv8AAB/AADyAABAAAACAAADgAADgAC//AAr5AADgAADQAAABAAAD9AAD0AAAAACwAACwAACwAACwAAAgAAABAAADwAADQAAAkAAv0Af9AL+AAvAAAAKgAD//ALgLgPADwPADwD//QA/9AAAAAAwAADwAAL//gL//gAAAAAgBQD4DwPAPwPA/wLnzwD/TwAUBQAAFADwPQLADwPDDwPvjwD+/AAQYAAAoAAD8AAv8AD08AP//gL//gAA8AAAAAL/PAPrDwPPDwPPDwPH/AAAoAAKgAC/+AHzrgPPDwPPDwH3/gBS+AKAAAPAAAPAbgPL/gP/QAPwAALAAAAAYAD+/ALvjwPLDwPLDwH//gBk+AAYAAD/PALTzwPDzwPDjwD//AAv8AAQBAA8DwA4DgAAAAAEBAAPD9AOD4AAAAABQAADwAAP4AANsAA8OAA4PABwHAAIUAAM8AAM8AAM8AAM8AAM8AAMoABgCAA4LAA8OAAdsAAP8AAHwAADgABgAADwAAPCzgPDzwLvBAD9AAAQAAABoAAv/wC0A8DD/OLPX3OMDjONHDHP/7Dfi5B4HQAP9AAACgAC/gB/8AP48AP48AB/8AAC/gAACgAAAAL//gP//wPDDwPDDwLv3gD+/AAAYAAGQAB/+AH0fQPADwPADwPADwDwPQBgNAAAAAL//gP//wPADwPADwHQDgD//AA/8AAAAAAAAAL//gP//wPDDwPDDwPDDwPADwAAAAAAAAL//gP//gPDAAPDAAPDAAPAAAAGQAB/+AD0fQPADwPCjwPDzwHz/QBy/gAAAAAAAAL//gL//gADAAADAAADAAL//gL//gAAAAAAAAL//gL//gAAAAAAeAAAvgAADwAADwL//gP//AAAAAAAAAL//gL//gAPAAA/0ADx/APAPwIABgAAAAL//gL//wAADwAADwAADwAACgAAAAL//gP6qQC/gAAH/QAAvwAv9AL9AAP//wGqqQAAAAL//gL6qQC+AAAP0AAB/AL//wL//gAAAAAGQAB/+AH0fQPADwPADwPADwD6vQB/9AAGQAAAAAL//gP//gPDwAPDwAH7gAD/AAAAAAAGQAB/+AH0fQPADwPAD9PAD/D6vbB/9PAGQAAAAAL//gP//gPDgAPD0ALr/AD/LwAUAgAUFAD+PALvDwPHDwPDjwLT7gDx/AAAAALAAAPAAAPAAAP//wPqqQPAAAPAAAAAAAP/+AGqvgAADwAADwAADwL//AL/4AAAAAKAAAL+AAAv9AAAvwAB/gAv8AL9AAKAAAKQAAL/QAAf/gAAPwAv/QL+AAL/gAAL/gAAvwC/+AP9AAEAAAEABgPgLwD9+AAfwAA/8AL0fgPADwOAAAL0AAB/AAAH/wA/qQL4AAPAAAFACgPAPwPA/wPHzwPvDwP4DwLQDwAAAAAAAAv///uqqvsAAPdAAAf4AAB/0AAC/wAAC4cAAKsAAPv///Kqqp"), 32, atob("BAMFCAgLCAMEBAcHAwYDBQgFBwcHBwcHBwcEBAcHBwcLCAgICQgHCQkEBwgHCgkJCAkICAcJCAwHBwgEBQQ="), 12+(scale<<8)+(2<<16)); + return this; +}; + +/* Set variables to get screen width, height and center points */ + +let W = g.getWidth(); +let H = g.getHeight(); +let cx = W/2; +let cy = H/2; +let Timeout; + +Bangle.loadWidgets(); + +/* Custom version of Bangle.drawWidgets (does not clear the widget areas) Thanks to rozek */ + +Bangle.drawWidgets = function () { + var w = g.getWidth(), h = g.getHeight(); + + var pos = { + tl:{x:0, y:0, r:0, c:0}, // if r==1, we're right->left + tr:{x:w-1, y:0, r:1, c:0}, + bl:{x:0, y:h-24, r:0, c:0}, + br:{x:w-1, y:h-24, r:1, c:0} + }; + + if (global.WIDGETS) { + for (var wd of WIDGETS) { + var p = pos[wd.area]; + if (!p) continue; + + wd.x = p.x - p.r*wd.width; + wd.y = p.y; + + p.x += wd.width*(1-2*p.r); + p.c++; + } + + g.reset(); // also loads the current theme + + try { + for (var wd of WIDGETS) { + g.setClipRect(wd.x,wd.y, wd.x+wd.width-1,23); + wd.draw(wd); + } + } catch (e) { print(e); } + + g.reset(); // clears the clipping rectangle! + } + }; + +/* Draws the clock hands and date */ + +function drawHands() { + let d = new Date(); + + let hour = d.getHours() % 12; + let min = d.getMinutes(); + let sec = d.getSeconds(); + + let twoPi = 2*Math.PI; + let Pi = Math.PI; + + let hourAngle = (hour+(min/60))/12 * twoPi - Pi; + let minAngle = (min/60) * twoPi - Pi; + let secAngle = (sec/60) * twoPi - Pi; + + g.setFontWorkSans(); + g.setColor(g.theme.bg); + g.setFontAlign(0,0,0); + g.drawString(d.getDate(),162,90); + g.setFontWorkSansSmall(); + let weekDay = d.toString().split(" "); + if (weekDay[0] == "Sat"){g.setColor(0,0,1);} + else if (weekDay[0] == "Sun"){g.setColor(1,0,0);} + else {g.setColor(g.theme.bg);} + g.drawString(weekDay[0].toUpperCase(), 137, 90); + + handLayers = [ + {x:cx, + y:cy, + image:imgHour, + rotate:hourAngle, + center:true + }, + {x:cx, + y:cy, + image:imgMin, + rotate:minAngle, + center:true + }, + {x:cx, + y:cy, + image:imgSec, + rotate:secAngle, + center:true + }]; + + g.setColor(g.theme.fg); + g.drawImages(handLayers); +} + +function drawBackground() { + g.clear(1); + g.setBgColor(g.theme.bg); + g.setColor(g.theme.fg); + bgLayer = [ + {x:cx, + y:cy, + image:imgBg, + center:true + }]; + g.drawImages(bgLayer); + g.reset(); +} + +/* Refresh the display every second */ + +function displayRefresh() { + g.clear(true); + drawBackground(); + drawHands(); + Bangle.drawWidgets(); + + let Pause = 1000 - (Date.now() % 1000); + Timeout = setTimeout(displayRefresh,Pause); +} + +Bangle.on('lcdPower', (on) => { + if (on) { + if (Timeout != null) { clearTimeout(Timeout); Timeout = undefined;} + displayRefresh(); + } +}); + +Bangle.setUI("clock"); +// load widgets after 'setUI' so they're aware there is a clock active +Bangle.loadWidgets(); +displayRefresh(); diff --git a/apps/seiko-5actus/metadata.json b/apps/seiko-5actus/metadata.json new file mode 100644 index 000000000..33de8213b --- /dev/null +++ b/apps/seiko-5actus/metadata.json @@ -0,0 +1,17 @@ +{ "id": "seiko-5actus", + "name": "Seiko 5actus", + "shortName":"5actus", + "icon": "seiko-5actus.png", + "screenshots": [{"url":"screenshot.png"}], + "version":"0.02", + "description": "A watch designed after then Seiko 5actus from the 1970's", + "tags": "clock", + "type": "clock", + "supports":["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"seiko-5actus.app.js","url":"app.js"}, + {"name":"seiko-5actus.img","url":"app-icon.js","evaluate":true} + ] + } diff --git a/apps/seiko-5actus/screenshot.png b/apps/seiko-5actus/screenshot.png new file mode 100644 index 000000000..fb8638999 Binary files /dev/null and b/apps/seiko-5actus/screenshot.png differ diff --git a/apps/seiko-5actus/seiko-5actus.png b/apps/seiko-5actus/seiko-5actus.png new file mode 100644 index 000000000..73f1b8164 Binary files /dev/null and b/apps/seiko-5actus/seiko-5actus.png differ diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index 39f9b59db..e963f2c40 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -29,11 +29,11 @@ function calc_ess(val) { if (nonmot) { slsnds+=1; if (slsnds >= sleepthresh) { - return true; // awake + return true; // sleep } } else { slsnds=0; - return false; // sleep + return false; // awake } } } diff --git a/apps/slomoclock/app-icon.js b/apps/slomoclock/app-icon.js index 22e264124..46f668745 100644 --- a/apps/slomoclock/app-icon.js +++ b/apps/slomoclock/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("oFAwhC/ABOIABgfymYAKD+Z/9hGDL5c4wAf/XzjASTxqgQhAfPMB2IPxiACIBo+BDxqACIBg+CLxpANHwQPBABgvCIBT8CJ5owDD5iPOOAQfLBojiDCYQGFGIQfICIQfdBYJNMOI6SHD8jeNOIYzID8hfRD9LfEAoTdFBIifLAAIffBoQRBAJpxMD84JCD+S/GL56fID8ALBb6ZhID8qtJCZ4fgT4YDBABq/PD7RNEL6IRKD8WID5pfCD5kzNhKSFmYfMBwSeOGBoPDABgvCJ5wAON5pADABivPIAIAOd5xABABweOD4J+OD58IQBj8LD/6gUDyAfhXzgfiP/wA2")) +require("heatshrink").decompress(atob("mEw4UA///7k8//GnldDZ9RosUqNABQsFqoACqALFg2qAAWQBaMVEYdUBYseC4e0BYsaBYekBYt6BYetBYouDAAIKEgPqC4erNgkFBQYABNgke2oiDrxIEvXUBYcXHgl7FIkB9oEDBYKYBTwILEi4LCoEBBYUQHQX7EYyRCBYJrF95ICBYNFBQdRBYcWEYwLDit7otUHQMVqIvDL4L5BgL8CI4YLDqILDO4gXGBQUEEZQ7CEYprEI4prFoLGBqkFoILFNZaPFF4ZHCR4hrFa5ILMfYJeDfYse2ovDrxGCAAMF1QAEMgIpD9QKD1Y1EgBfFBQg8BC4Y6EAAMaBYekBYseBYZGESIQuDfIYACgwXDyALRgojDNQhsCBYZqFABI=")) diff --git a/apps/smclock/ChangeLog b/apps/smclock/ChangeLog index 0300d5ceb..2a3874d34 100644 --- a/apps/smclock/ChangeLog +++ b/apps/smclock/ChangeLog @@ -1,4 +1,6 @@ 0.01: Initial version 0.02: Add battery level -0.03: Fix battery display when full +0.03: Fix battery display when full (incorporating code by Ronin0000) 0.04: Add support for settings +0.05: Add ability to change background (3bit or 4bit) +0.06: Replace battery text with image diff --git a/apps/smclock/README.md b/apps/smclock/README.md index 635292d0c..2fc239ab2 100644 --- a/apps/smclock/README.md +++ b/apps/smclock/README.md @@ -4,13 +4,16 @@ Just a simple watch face for the Banglejs2. It shows battery level in the upper left corner, date information in the upper right, and time information in the bottom. -![](screenshot.png) +![](screenshot0.png) +![](screenshot1.png) ## Settings -**Analog Clock:** +**Analog Clock:** *Not yet implemented.* -**Human Readable Date:** When the setting is on, the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off. +**Background:** When the setting is set as "3bit", a background with more accurate colors is chosen for the watchface. Otherwise, it uses a background following the 16-bit Mac Color Palette. + +**Date Format:** When the setting is set as "Long", the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off. **Show Week Info:** When the setting is on, the weekday and week number are shown in the upper right box. When the setting is off, the full year is shown instead. Default is off. @@ -20,4 +23,4 @@ It shows battery level in the upper left corner, date information in the upper r Monogram Watch Face can be selected as the default clock or it can be run manually from the launcher. Its settings can be accessed and changed via the relevant menu. -Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert. +*Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert.* - *Feature not implemented yet.* diff --git a/apps/smclock/app.js b/apps/smclock/app.js index 350c0dd07..41bc2b5e4 100644 --- a/apps/smclock/app.js +++ b/apps/smclock/app.js @@ -1,23 +1,23 @@ const SETTINGSFILE = "smclock.json"; -const background = { - width: 176, - height: 176, - bpp: 3, - transparent: 1, - buffer: require("heatshrink").decompress( - atob( - "/4A/AH4ACUb8H9MkyVJAThB/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INP/AH4A/AAX8Yz4Afn5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INI=" - ) - ), +const image3bit = { + width : 176, height : 176, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4A/AH4AC23btoCct/pkmSpICcIP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5Bp/4A/AH4AC/kAAH0/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5BpA=")) +}; +const image4bit = { + width : 176, height : 176, bpp : 4, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4A/AH4Au1QAp1/2swApK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K+//AH4A/AF8AAH4AUK/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/AA==")) }; const monthName = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; // dynamic variables var batLevel = -1; -var batColor = [0, 0, 0]; +var batColor = ""; // settings variables +var backgroundImage; var dateFormat; var drawInterval; var pollInterval; @@ -31,6 +31,7 @@ function loadSettings() { function def(value, def) {return value !== undefined ? value : def;} var settings = require("Storage").readJSON(SETTINGSFILE, true) || {}; + backgroundImage = def(settings.backgroundImage, "3bit"); dateFormat = def(settings.dateFormat, "Short"); drawInterval = def(settings.drawInterval, 10); pollInterval = def(settings.pollInterval, 60); @@ -67,23 +68,29 @@ function getBatteryColor(level) { level = batLevel; } if (level > 80) { - color = [0, 0, 1]; + color = "#00f"; } else if (level > 60) { - color = [0, 1, 1]; + color = "#0ff"; } else if (level > 40) { - color = [0, 1, 0]; + color = "#0f0"; } else if (level > 20) { - color = [1, 1, 0]; + color = "#f40"; } else { - color = [1, 0, 0]; + color = "f00"; } return color; } function draw() { + var background; + if (backgroundImage == "3bit") { + background = image3bit; + } else { + background = image4bit; + } g.drawImage(background); - const color = getBatteryColor(batLevel); + batColor = getBatteryColor(batLevel); var bat = ""; const d = new Date(); const day = d.getDate(); @@ -95,32 +102,38 @@ function draw() { const m = d.getMinutes(); const time = d02(h) + ":" + d02(m); - if (E.getBattery() < 100) { - bat = d02(E.getBattery()) + "%"; - } else { - bat = E.getBattery() + "%"; - } - g.reset(); // draw battery info - g.setColor(1, 1, 1); + var x = 12; + var y = 16; + if (Bangle.isCharging()) { + g.setColor("#ff0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); + } else { + g.clearRect(x,y,x+14,y+24); + g.setColor("#000").fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2); + g.setColor(batColor).fillRect(x+4,y+20-(batLevel*16/100),x+10,y+20); + } + if (Bangle.isCharging()) { + g.setColor("#ff0"); + } else { + g.setColor(batColor); + } if (useVectorFont == true) { g.setFont("Vector", 16); - g.drawString("Bat:", 12, 22, false); } else { - g.setFont("4x6", 2); - g.drawString("Bat:", 10, 22, false); + g.setFont("4x6", 3); } - g.setColor(color[0], color[1], color[2]); if (batLevel < 100) { - g.drawString(bat, 52, 22, false); + bat = d02(batLevel) + "%"; + g.drawString(bat, 50, 22, false); } else { - g.drawString(bat, 46, 22, false); + bat = "100%"; + g.drawString(bat, 40, 22, false); } // draw date info - g.setColor(0, 0, 0); + g.setColor("#000"); if (useVectorFont == true) { g.setFont("Vector", 20); } else { @@ -136,7 +149,7 @@ function draw() { // draw week info if (showWeekInfo == true) { - date2 = weekday[d.getDay()] + " " + d02(week) + date2 = weekday[d.getDay()] + " " + d02(week); if (useVectorFont == true) { g.setFont("Vector", 18); } else { @@ -155,7 +168,7 @@ function draw() { } // draw time - g.setColor(1, 1, 1); + g.setColor("#fff"); if (useVectorFont == true) { g.setFont("Vector", 60); g.drawString(time, 10, 108, false); diff --git a/apps/smclock/metadata.json b/apps/smclock/metadata.json index cc995d587..55668adcc 100644 --- a/apps/smclock/metadata.json +++ b/apps/smclock/metadata.json @@ -3,13 +3,13 @@ "name": "Monogram Watch Face", "shortName": "MonoClock", "icon": "app.png", - "screenshots": [{ "url": "screenshot.png" }], + "screenshots": [{ "url": "screenshot0.png" }, {"url": "screenshot1.png" }], "version": "0.04", "description": "A simple watchface based on my stylised monogram.", "type": "clock", "tags": "clock", "readme": "README.md", - "supports": ["BANGLEJS", "BANGLEJS2"], + "supports": ["BANGLEJS2"], "allow_emulator": true, "storage": [ { "name": "smclock.app.js", "url": "app.js" }, diff --git a/apps/smclock/screenshot.png b/apps/smclock/screenshot.png deleted file mode 100644 index c0e0bd0ee..000000000 Binary files a/apps/smclock/screenshot.png and /dev/null differ diff --git a/apps/smclock/screenshot0.png b/apps/smclock/screenshot0.png new file mode 100644 index 000000000..07eff8ddf Binary files /dev/null and b/apps/smclock/screenshot0.png differ diff --git a/apps/smclock/screenshot1.png b/apps/smclock/screenshot1.png new file mode 100644 index 000000000..da25b2579 Binary files /dev/null and b/apps/smclock/screenshot1.png differ diff --git a/apps/smclock/settings.js b/apps/smclock/settings.js index a6c7d1b98..ee4a35a26 100644 --- a/apps/smclock/settings.js +++ b/apps/smclock/settings.js @@ -52,6 +52,7 @@ writeSettings(); }, }, + "Background": stringInSettings("backgroundImage", ["3bit", "4bit"]), Date: stringInSettings("dateFormat", ["Long", "Short"]), "Draw Interval": { value: settings.drawInterval, diff --git a/apps/sunclock/README.md b/apps/sunclock/README.md new file mode 100644 index 000000000..4076767d9 --- /dev/null +++ b/apps/sunclock/README.md @@ -0,0 +1,6 @@ +# Sun Clock +Clock showing date/time, sunset/sunrise, H = current sun height/noon sun height, Az = sun azimuth + +![](screenshot_sunclock.png) + +Location set with mylocation app, time zone set with settings app. diff --git a/apps/sunclock/app-icon.js b/apps/sunclock/app-icon.js new file mode 100644 index 000000000..977aec98d --- /dev/null +++ b/apps/sunclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("kEgwhC/AC8N6APo7oPJBQndBQYPEhoaFAogZIEokO93u8AuGAAYOCCAgOLCBQOFAAIeNEBAPPBw4wHB5wuIGAwPthGIxwIC8UowUuB4eIwAPBxEk91CAgIGGwAhBBYeCAwMoA4ZwEBIIOCAxAA/ABwA=")) diff --git a/apps/sunclock/app.js b/apps/sunclock/app.js new file mode 100644 index 000000000..4609565a2 --- /dev/null +++ b/apps/sunclock/app.js @@ -0,0 +1,79 @@ +/* sclock.app.js for Bangle2 +Peter Bernschneider 30.12.2021 +Update current latitude and longitude in My Location app +Update current Timezone in Settings app, menu item "System" +Update for summer time by incrementing Timezone += 1 */ +setting = require("Storage").readJSON("setting.json",1); +E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ +SunCalc = require("suncalc.js"); +loc = require('locale'); +const LOCATION_FILE = "mylocation.json"; +const xyCenter = g.getWidth() / 2 + 3; +const yposTime = 60; +const yposDate = 100; +const yposRS = 135; +const yposPos = 160; +var rise = "07:00"; +var set = "20:00"; +var pos = {altitude: 20, azimuth: 135}; +var noonpos = {altitude: 37, azimuth: 180}; +let idTimeout = null; + +function updatePos() { + coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":53.3,"lon":10.1,"location":"Pattensen"}; + pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon); + times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon); + rise = times.sunrise.toString().split(" ")[4].substr(0,5); + set = times.sunset.toString().split(" ")[4].substr(0,5); + noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon); +} + +function drawSimpleClock() { + var d = new Date(); // get date + var da = d.toString().split(" "); + g.clear(); + Bangle.drawWidgets(); + g.reset(); // default draw styles + g.setFontAlign(0, 0); // drawSting centered + + var time = da[4].substr(0, 5); // draw time + + g.setFont("Vector",60); + g.drawString(time, xyCenter, yposTime, true); + + var date = [loc.dow(new Date(),1), loc.date(d,1)].join(" "); // draw day of week, date + g.setFont("Vector",24); + g.drawString(date, xyCenter, yposDate, true); + + g.setFont("Vector",25); + g.drawString(`${rise} ${set}`, xyCenter, yposRS, true); // draw riseset + g.drawImage(require("Storage").read("sunrise.img"), xyCenter-16, yposRS-16); + + g.setFont("Vector",21); + g.drawString(`H${pos.altitude}/${noonpos.altitude} Az${pos.azimuth}`, xyCenter, yposPos, true); // draw sun pos + + let t = d.getSeconds()*1000 + d.getMilliseconds(); + idTimeout = setTimeout(drawSimpleClock, 60000 - t); // time till next minute +} + +// special function to handle display switch on +Bangle.on('lcdPower', function(on){ + if (on) { + drawSimpleClock(); + } else { + if(idTimeout) { + clearTimeout(idTimeout); + } + } +}); + +g.clear(); // clean app screen +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +setInterval(updatePos, 60*5E3); // refesh every 5 mins + +updatePos(); +drawSimpleClock(); // draw now + +setWatch(Bangle.showLauncher, BTN1, { repeat: false, edge: "falling" }); // Show launcher when button pressed diff --git a/apps/sunclock/app.png b/apps/sunclock/app.png new file mode 100644 index 000000000..72c5b10d5 Binary files /dev/null and b/apps/sunclock/app.png differ diff --git a/apps/sunclock/metadata.json b/apps/sunclock/metadata.json new file mode 100644 index 000000000..a39343992 --- /dev/null +++ b/apps/sunclock/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "sunclock", + "name": "Sun Clock", + "version": "0.01", + "description": "A clock with sunset/sunrise, sun height/azimuth", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"sunclock.app.js","url":"app.js"}, + {"name":"sunclock.img","url":"app-icon.js","evaluate":true}, + {"name":"suncalc.js","url":"suncalc.js"} + ] +} diff --git a/apps/sunclock/screenshot_sunclock.png b/apps/sunclock/screenshot_sunclock.png new file mode 100644 index 000000000..a24af2116 Binary files /dev/null and b/apps/sunclock/screenshot_sunclock.png differ diff --git a/apps/sunclock/suncalc.js b/apps/sunclock/suncalc.js new file mode 100644 index 000000000..b1af0a0d9 --- /dev/null +++ b/apps/sunclock/suncalc.js @@ -0,0 +1,298 @@ +/* Module suncalc.js + (c) 2011-2015, Vladimir Agafonkin + SunCalc is a JavaScript library for calculating sun/moon position and light phases. + https://github.com/mourner/suncalc + +PB: Usage: +E.setTimeZone(2); // 1 = MEZ, 2 = MESZ +SunCalc = require("suncalc.js"); +pos = SunCalc.getPosition(Date.now(), 53.3, 10.1); +times = SunCalc.getTimes(Date.now(), 53.3, 10.1); +rise = times.sunrise; // Date object +rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm +*/ +var exports={}; + +// shortcuts for easier to read formulas + +var PI = Math.PI, + sin = Math.sin, + cos = Math.cos, + tan = Math.tan, + asin = Math.asin, + atan = Math.atan2, + acos = Math.acos, + rad = PI / 180; + +// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas + +// date/time constants and conversions + +var dayMs = 1000 * 60 * 60 * 24, + J1970 = 2440588, + J2000 = 2451545; + +function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } +function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021 +function toDays(date) { return toJulian(date) - J2000; } + + +// general calculations for position + +var e = rad * 23.4397; // obliquity of the Earth + +function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } +function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } + +function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } +function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } + +function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } + +function astroRefraction(h) { + if (h < 0) // the following formula works for positive altitudes only. + h = 0; // if h = -0.08901179 a div/0 would occur. + + // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. + // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: + return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179)); +} + +// general sun calculations + +function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } + +function eclipticLongitude(M) { + + var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center + P = rad * 102.9372; // perihelion of the Earth + + return M + C + P + PI; +} + +function sunCoords(d) { + + var M = solarMeanAnomaly(d), + L = eclipticLongitude(M); + + return { + dec: declination(L, 0), + ra: rightAscension(L, 0) + }; +} + +// calculates sun position for a given date and latitude/longitude + +exports.getPosition = function (date, lat, lng) { + + var lw = rad * -lng, + phi = rad * lat, + d = toDays(date), + + c = sunCoords(d), + H = siderealTime(d, lw) - c.ra; + + return { + azimuth: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg + altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg + }; +}; + + +// sun times configuration (angle, morning name, evening name) + +var times = [ + [-0.833, 'sunrise', 'sunset' ] +]; + +// calculations for sun times +var J0 = 0.0009; + +function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } + +function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; } +function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } + +function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); } +function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } + +// returns set time for the given sun altitude +function getSetJ(h, lw, phi, dec, n, M, L) { + + var w = hourAngle(h, phi, dec), + a = approxTransit(w, lw, n); + return solarTransitJ(a, M, L); +} + + +// calculates sun times for a given date, latitude/longitude, and, optionally, +// the observer height (in meters) relative to the horizon + +exports.getTimes = function (date, lat, lng, height) { + + height = height || 0; + + var lw = rad * -lng, + phi = rad * lat, + + dh = observerAngle(height), + + d = toDays(date), + n = julianCycle(d, lw), + ds = approxTransit(0, lw, n), + + M = solarMeanAnomaly(ds), + L = eclipticLongitude(M), + dec = declination(L, 0), + + Jnoon = solarTransitJ(ds, M, L), + + i, len, time, h0, Jset, Jrise; + + + var result = { + solarNoon: fromJulian(Jnoon), + nadir: fromJulian(Jnoon - 0.5) + }; + + for (i = 0, len = times.length; i < len; i += 1) { + time = times[i]; + h0 = (time[0] + dh) * rad; + + Jset = getSetJ(h0, lw, phi, dec, n, M, L); + Jrise = Jnoon - (Jset - Jnoon); + + result[time[1]] = fromJulian(Jrise); + result[time[2]] = fromJulian(Jset); + } + + return result; +}; + + +// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas + +function moonCoords(d) { // geocentric ecliptic coordinates of the moon + + var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude + M = rad * (134.963 + 13.064993 * d), // mean anomaly + F = rad * (93.272 + 13.229350 * d), // mean distance + + l = L + rad * 6.289 * sin(M), // longitude + b = rad * 5.128 * sin(F), // latitude + dt = 385001 - 20905 * cos(M); // distance to the moon in km + + return { + ra: rightAscension(l, b), + dec: declination(l, b), + dist: dt + }; +} + +getMoonPosition = function (date, lat, lng) { + + var lw = rad * -lng, + phi = rad * lat, + d = toDays(date), + + c = moonCoords(d), + H = siderealTime(d, lw) - c.ra, + h = altitude(H, phi, c.dec), + // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. + pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H)); + + h = h + astroRefraction(h); // altitude correction for refraction + + return { + azimuth: azimuth(H, phi, c.dec), + altitude: h, + distance: c.dist, + parallacticAngle: pa + }; +}; + + +// calculations for illumination parameters of the moon, +// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and +// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. + +getMoonIllumination = function (date) { + + var d = toDays(date || new Date()), + s = sunCoords(d), + m = moonCoords(d), + + sdist = 149598000, // distance from Earth to Sun in km + + phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)), + inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)), + angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) - + cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra)); + + return { + fraction: (1 + cos(inc)) / 2, + phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI, + angle: angle + }; +}; + + +function hoursLater(date, h) { + return new Date(date.valueOf() + h * dayMs / 24); +} + +// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article + +getMoonTimes = function (date, lat, lng, inUTC) { + var t = new Date(date); + if (inUTC) t.setUTCHours(0, 0, 0, 0); + else t.setHours(0, 0, 0, 0); + + var hc = 0.133 * rad, + h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc, + h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx; + + // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) + for (var i = 1; i <= 24; i += 2) { + h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc; + h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc; + + a = (h0 + h2) / 2 - h1; + b = (h2 - h0) / 2; + xe = -b / (2 * a); + ye = (a * xe + b) * xe + h1; + d = b * b - 4 * a * h1; + roots = 0; + + if (d >= 0) { + dx = Math.sqrt(d) / (Math.abs(a) * 2); + x1 = xe - dx; + x2 = xe + dx; + if (Math.abs(x1) <= 1) roots++; + if (Math.abs(x2) <= 1) roots++; + if (x1 < -1) x1 = x2; + } + + if (roots === 1) { + if (h0 < 0) rise = i + x1; + else set = i + x1; + + } else if (roots === 2) { + rise = i + (ye < 0 ? x2 : x1); + set = i + (ye < 0 ? x1 : x2); + } + + if (rise && set) break; + + h0 = h2; + } + + var result = {}; + + if (rise) result.rise = hoursLater(t, rise); + if (set) result.set = hoursLater(t, set); + + if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; + + return result; +}; \ No newline at end of file diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog index 6515ab627..14159bc19 100644 --- a/apps/terminalclock/ChangeLog +++ b/apps/terminalclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Rename "Activity" in "Motion" and display the true values for it +0.02: Rename "Activity" in "Motion" and display the true values for it +0.03: Add Banglejs 1 compatibility diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js index ab83a696f..d219b84d8 100644 --- a/apps/terminalclock/app.js +++ b/apps/terminalclock/app.js @@ -1,16 +1,28 @@ var locale = require("locale"); var fontColor = g.theme.dark ? "#0f0" : "#000"; -var paddingY = 2; -var font6x8At4Size = 32; -var font6x8At2Size = 18; var heartRate = 0; +// handling the differents versions of the Banglejs smartwatch +if (process.env.HWVERSION == 1){ + var paddingY = 3; + var font6x8At4Size = 48; + var font6x8At2Size = 27; + var font6x8FirstTextSize = 6; + var font6x8DefaultTextSize = 3; +} +else{ + var paddingY = 2; + var font6x8At4Size = 32; + var font6x8At2Size = 18; + var font6x8FirstTextSize = 4; + var font6x8DefaultTextSize = 2; +} function setFontSize(pos){ if(pos == 1) - g.setFont("6x8", 4); + g.setFont("6x8", font6x8FirstTextSize); else - g.setFont("6x8", 2); + g.setFont("6x8", font6x8DefaultTextSize); } function clearField(pos){ diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json index de0244318..de369bf10 100644 --- a/apps/terminalclock/metadata.json +++ b/apps/terminalclock/metadata.json @@ -3,11 +3,12 @@ "name": "Terminal Clock", "shortName":"Terminal Clock", "description": "A terminal cli like clock displaying multiple sensor data", - "version":"0.02", + "version":"0.03", "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, "readme": "README.md", "storage": [ {"name": "terminalclock.app.js","url": "app.js"}, diff --git a/apps/widclose/ChangeLog b/apps/widclose/ChangeLog new file mode 100644 index 000000000..4be6afb16 --- /dev/null +++ b/apps/widclose/ChangeLog @@ -0,0 +1 @@ +0.01: New widget! \ No newline at end of file diff --git a/apps/widclose/README.md b/apps/widclose/README.md new file mode 100644 index 000000000..55c8de483 --- /dev/null +++ b/apps/widclose/README.md @@ -0,0 +1,7 @@ +# Close Button + +Adds a ![X](preview.png) button to close the current app and go back to the clock. +(Widget is not visible on the clock screen) + +![Light theme screenshot](screenshot_light.png) +![Dark theme screenshot](screenshot_dark.png) \ No newline at end of file diff --git a/apps/widclose/icon.png b/apps/widclose/icon.png new file mode 100644 index 000000000..1d95ba0ce Binary files /dev/null and b/apps/widclose/icon.png differ diff --git a/apps/widclose/metadata.json b/apps/widclose/metadata.json new file mode 100644 index 000000000..e044a2d39 --- /dev/null +++ b/apps/widclose/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widclose", + "name": "Close Button", + "version": "0.01", + "description": "A button to close the current app", + "readme": "README.md", + "icon": "icon.png", + "type": "widget", + "tags": "widget,tools", + "supports": ["BANGLEJS2"], + "screenshots": [{"url":"screenshot_light.png"},{"url":"screenshot_dark.png"}], + "storage": [ + {"name":"widclose.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widclose/preview.png b/apps/widclose/preview.png new file mode 100644 index 000000000..d90a3b4c5 Binary files /dev/null and b/apps/widclose/preview.png differ diff --git a/apps/widclose/screenshot_dark.png b/apps/widclose/screenshot_dark.png new file mode 100644 index 000000000..58067a3b9 Binary files /dev/null and b/apps/widclose/screenshot_dark.png differ diff --git a/apps/widclose/screenshot_light.png b/apps/widclose/screenshot_light.png new file mode 100644 index 000000000..32817ea8d Binary files /dev/null and b/apps/widclose/screenshot_light.png differ diff --git a/apps/widclose/widget.js b/apps/widclose/widget.js new file mode 100644 index 000000000..3a354018b --- /dev/null +++ b/apps/widclose/widget.js @@ -0,0 +1,14 @@ +if (!Bangle.CLOCK) WIDGETS.close = { + area: "tr", width: 24, sortorder: 10, // we want the right-most spot please + draw: function() { + Bangle.removeListener("touch", this.touch); + Bangle.on("touch", this.touch); + g.reset().setColor("#f00").drawImage(atob( // hardcoded red to match setUI back button + // b/w version of preview.png, 24x24 + "GBgBABgAAf+AB//gD//wH//4P//8P//8fn5+fjx+fxj+f4H+/8P//8P/f4H+fxj+fjx+fn5+P//8P//8H//4D//wB//gAf+AABgA" + ), this.x, this.y); + }, touch: function(_, c) { + const w = WIDGETS.close; + if (w && c.x>=w.x && c.x<=w.x+24 && c.y>=w.y && c.y<=w.y+24) load(); + } +}; \ No newline at end of file diff --git a/backup.js b/backup.js index b0159036d..75e236049 100644 --- a/backup.js +++ b/backup.js @@ -22,10 +22,14 @@ function bangleDownload() { var promise = Promise.resolve(); // Normal files normalFiles.forEach((filename,n) => { + if (filename==".firmware") { + console.log("Ignoring .firmware file"); + return; + } promise = promise.then(() => { Progress.hide({sticky: true}); var percent = n/fileCount; - Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent,percent:0}); + Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0}); return Comms.readFile(filename).then(data => zip.file(filename,data)); }); }); @@ -36,7 +40,7 @@ function bangleDownload() { promise = promise.then(() => { Progress.hide({sticky: true}); var percent = (normalFiles.length+n)/fileCount; - Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent,percent:0}); + Progress.show({title:`Download ${filename}`,sticky:true,min:percent,max:percent+(1/fileCount),percent:0}); return Comms.readStorageFile(filename).then(data => zipStorageFiles.file(filename,data)); }); }); diff --git a/core b/core index 6e94cf77a..27c7db603 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 6e94cf77a2b355389dcd5efa55e2249a9b31983c +Subproject commit 27c7db6035832837ca3909ea52939f60803df72f diff --git a/modules/Layout.js b/modules/Layout.js index 20fa2be8b..0bfbc28ad 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -59,6 +59,7 @@ options is an object containing: * `label` - the text on the button * `cb` - a callback function * `cbl` - a callback function for long presses +* `back` - a callback function, passed as `back` into Bangle.setUI If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically determine what objects have changed or moved, clear their previous locations, and re-render just those objects. @@ -89,7 +90,7 @@ function Layout(layout, options) { options = options || {}; this.lazy = options.lazy || false; - var btnList; + var btnList, uiSet; Bangle.setUI(); // remove all existing input handlers if (process.env.HWVERSION!=2) { // no touchscreen, find any buttons in 'layout' @@ -104,7 +105,7 @@ function Layout(layout, options) { this.physBtns = 0; this.buttons = btnList; this.selectedButton = -1; - Bangle.setUI("updown", dir=>{ + Bangle.setUI({mode:"updown", back:options.back}, dir=>{ var s = this.selectedButton, l=this.buttons.length; if (dir===undefined && this.buttons[s]) return this.buttons[s].cb(); @@ -119,8 +120,10 @@ function Layout(layout, options) { } this.selectedButton = s; }); + uiSet = true; } } + if (options.back && !uiSet) Bangle.setUI({mode: "custom", back: options.back}); if (options.btns) { var buttons = options.btns;