diff --git a/apps.json b/apps.json index 49397479a..01bdaf84f 100644 --- a/apps.json +++ b/apps.json @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.39", + "version": "0.40", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -77,7 +77,7 @@ { "id": "messages", "name": "Messages", - "version": "0.16", + "version": "0.17", "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", "type": "app", @@ -116,7 +116,7 @@ { "id": "ios", "name": "iOS Integration", - "version": "0.07", + "version": "0.08", "description": "Display notifications/music/etc from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", @@ -167,7 +167,7 @@ { "id": "setting", "name": "Settings", - "version": "0.39", + "version": "0.40", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -845,7 +845,7 @@ { "id": "weather", "name": "Weather", - "version": "0.14", + "version": "0.15", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], @@ -936,7 +936,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", - "version": "0.14", + "version": "0.15", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "icon": "widget.png", "type": "widget", @@ -1324,7 +1324,7 @@ "icon": "gesture.png", "type": "app", "tags": "gesture,ai", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"gesture.app.js","url":"gesture.js"}, {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, @@ -1749,8 +1749,9 @@ "icon": "grocery.png", "type": "app", "tags": "tool,outdoors,shopping,list", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "custom": "grocery.html", + "allow_emulator": true, "storage": [ {"name":"grocery.app.js","url":"app.js"}, {"name":"grocery.img","url":"grocery-icon.js","evaluate":true} @@ -2970,11 +2971,11 @@ { "id": "cprassist", "name": "CPR Assist", - "version": "0.01", + "version": "0.02", "description": "Provides assistance while performing a CPR", "icon": "cprassist-icon.png", "tags": "tool,firstaid", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "screenshots": [{"url":"bangle1-CPR-assist-screenshot.png"}], @@ -3532,7 +3533,7 @@ "id": "mclockplus", "name": "Morph Clock+", "shortName": "Morph Clock+", - "version": "0.02", + "version": "0.03", "description": "Morphing Clock with more readable seconds and date and additional stopwatch", "icon": "mclockplus.png", "type": "clock", @@ -3986,11 +3987,12 @@ "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "storage": [ - {"name":"doztime.app.js","url":"app.js"}, + {"name":"doztime.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]}, + {"name":"doztime.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}, {"name":"doztime.img","url":"app-icon.js","evaluate":true} ] }, @@ -4210,13 +4212,13 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.09", - "description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", + "version": "0.10", + "description": "A Configurable clock with custom fonts, background and weather display. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", "icon": "pastel.png", - "dependencies": {"mylocation":"app", "widpedom":"app"}, - "screenshots": [{"url":"screenshot_pastel.png"}], + "dependencies": {"mylocation":"app", "widpedom":"app","weather":"app"}, + "screenshots": [{"url":"screenshot_pastel.png"}, {"url":"weather_icons.png"}], "type": "clock", - "tags": "clock", + "tags": "clock, weather, tool", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ @@ -4382,7 +4384,7 @@ { "id": "gpstouch", "name": "GPS Touch", - "version": "0.01", + "version": "0.02", "description": "A touch based GPS watch, shows OS map reference", "icon": "gpstouch.png", "screenshots": [{"url":"screenshot4.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"},{"url":"screenshot1.png"}], @@ -4487,7 +4489,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.08", + "version":"0.09", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", @@ -4621,7 +4623,7 @@ "shortName":"93 Dub", "icon": "93dub.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.05", + "version":"0.06", "description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo", "tags": "clock", "type": "clock", @@ -4710,7 +4712,7 @@ "icon": "mylocation.png", "type": "app", "screenshots": [{"url":"screenshot_1.png"}], - "version":"0.01", + "version":"0.02", "description": "Sets and stores the lat and long of your preferred City or it can be set from the GPS. mylocation.json can be used by other apps that need your main location lat and lon. See README", "readme": "README.md", "tags": "tool,utility", @@ -4727,7 +4729,7 @@ "id": "pebble", "name": "Pebble Clock", "shortName": "Pebble", - "version": "0.06", + "version": "0.07", "description": "A pebble style clock to keep the rebellion going", "dependencies": {"widpedom":"app"}, "readme": "README.md", @@ -4735,7 +4737,7 @@ "screenshots": [{"url":"pebble_screenshot.png"}], "type": "clock", "tags": "clock", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"pebble.app.js","url":"pebble.app.js"}, {"name":"pebble.settings.js","url":"pebble.settings.js"}, @@ -4769,7 +4771,7 @@ "screenshots": [{"url":"screenshot_widbata_1.png"}], "version":"0.01", "type": "widget", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "description": "Shows the current battery level status in the top right using the clocks colour theme", "tags": "widget,battery", @@ -4914,7 +4916,7 @@ "id": "rebble", "name": "Rebble Clock", "shortName": "Rebble", - "version": "0.03", + "version": "0.04", "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion", "readme": "README.md", "icon": "rebble.png", @@ -5099,7 +5101,7 @@ "tags": "clock", "allow_emulator":true, "supports" : ["BANGLEJS2"], - "type": "clock", + "type": "clock", "storage": [ {"name":"contourclock.app.js","url":"app.js"}, {"name":"contourclock.img","url":"app-icon.js","evaluate":true} @@ -5121,6 +5123,24 @@ {"name":"ltherm.img","url":"icon.js","evaluate":true} ] }, + { + "id": "presentor", + "name": "Presentor", + "version": "3.0", + "description": "Use your Bangle to present!", + "icon": "app.png", + "type": "app", + "tags": "tool,bluetooth", + "interface": "interface.html", + "readme":"README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"presentor.app.js","url":"app.js"}, + {"name":"presentor.img","url":"app-icon.js","evaluate":true}, + {"name":"presentor.json","url":"settings.json"} + ] + }, { "id": "slash", "name": "Slash Watch", @@ -5335,7 +5355,7 @@ "icon": "andark_icon.png", "type": "clock", "tags": "clock", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"andark.app.js","url":"app.js"}, @@ -5358,5 +5378,55 @@ { "name": "diract.app.js", "url": "diract.js" }, { "name": "diract.img", "url": "diract-icon.js", "evaluate": true } ] + }, + { + "id": "sonicclk", + "name": "Sonic Clock", + "version": "1.01", + "description": "A classic sonic clock featuring run, stop and wait animations.", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"sonicclk.app.js","url":"app.js"}, + {"name":"sonicclk.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id": "touchmenu", + "name": "TouchMenu", + "version": "0.01", + "description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2", + "screenshots": [{"url":"touchmenu.gif"}], + "icon": "touchmenu.png", + "type": "bootloader", + "tags": "tool", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"touchmenu.boot.js","url":"touchmenu.boot.js"} + ] + }, + { + "id": "puzzle15", + "name": "15 puzzle", + "version": "0.05", + "description": "A 15 puzzle game with drag gesture interface", + "readme":"README.md", + "icon": "puzzle15.app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "app", + "tags": "game", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "storage": [ + {"name":"puzzle15.app.js","url":"puzzle15.app.js"}, + {"name":"puzzle15.settings.js","url":"puzzle15.settings.js"}, + {"name":"puzzle15.img","url":"puzzle15.app-icon.js","evaluate":true} + ], + "data": [{"name":"puzzle15.json"}] } ] diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog index c1b2588bb..1c18ca59b 100644 --- a/apps/93dub/ChangeLog +++ b/apps/93dub/ChangeLog @@ -3,3 +3,4 @@ 0.03: Code style cleanup 0.04: Set 00:00 to 12:00 for 12 hour time 0.05: Display time, even on Thursday +0.06: Fix light theme issue, where widgets would end up on a light strip diff --git a/apps/93dub/app.js b/apps/93dub/app.js index 1b0f69a94..f970eec5d 100644 --- a/apps/93dub/app.js +++ b/apps/93dub/app.js @@ -122,7 +122,13 @@ function draw(){ queueDraw(); } - +/** + * This watch is mostly dark, it does not make sense to respect the + * light theme as you end up with a white strip at the top for the + * widgets and black watch. So set the colours to the dark theme. + * + */ +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); draw(); //the following section is also from waveclk diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 5c929421b..d6619822b 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -43,3 +43,4 @@ 0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app 0.38: Option to log to file if settings.log==2 0.39: Fix passkey support (fix https://github.com/espruino/Espruino/issues/2035) +0.40: Bootloader now rebuilds for new firmware versions diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index e338d9020..664d64ee7 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -6,11 +6,11 @@ var s = require('Storage').readJSON('setting.json',1)||{}; var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 var boot = ""; 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/); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)!=${CRC})`; + 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})`; } else { - var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/)); - boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))!=${CRC})`; + var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); + boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; boot += `E.setFlags({pretokenise:1});\n`; diff --git a/apps/cprassist/ChangeLog b/apps/cprassist/ChangeLog index 5560f00bc..529010aa8 100644 --- a/apps/cprassist/ChangeLog +++ b/apps/cprassist/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Ported to Banglejs2 diff --git a/apps/cprassist/cprassist.js b/apps/cprassist/cprassist.js index 862ae54d6..128ae7407 100644 --- a/apps/cprassist/cprassist.js +++ b/apps/cprassist/cprassist.js @@ -35,23 +35,24 @@ function provideFeedback() { } function drawHeart() { - g.fillCircle(40, 92, 12); - g.fillCircle(60, 92, 12); - g.fillPoly([29, 98, 50, 120, 71, 98]); + var lowestPoint = g.getHeight()*3/5; + g.fillCircle(40, lowestPoint-29, 12); + g.fillCircle(60, lowestPoint-29, 12); + g.fillPoly([29, lowestPoint-22, 50, lowestPoint, 71, lowestPoint-22]); } function updateScreen() { - const colors = [0xFFFF, 0x9492]; - g.reset().clearRect(0, 50, 250, 150); + const colors = [0xFFFF-g.getBgColor(), 0x9492]; + g.reset().clearRect(0, 24, g.getWidth(), g.getHeight()*5/6); if (counter > 0) { g.setFont("Vector", 40).setFontAlign(0, 0); g.setColor(colors[counter%2]); drawHeart(); - g.drawString(counter + "", g.getWidth()/2, 100); + g.drawString(counter, 120, g.getHeight()*3/5-20); } else { g.setFont("Vector", 20).setFontAlign(0, 0); - g.drawString("RESCUE", g.getWidth()/2, 70); - g.drawString("BREATHS", g.getWidth()/2, 120); + g.drawString("RESCUE", g.getWidth()/2, g.getHeight()/3); + g.drawString("BREATHS", g.getWidth()/2, g.getHeight()*3/5); } } @@ -73,7 +74,7 @@ function tick() { interval = setInterval(tick, 60000/setting('compression_rpm')); g.clear(1).setFont("6x8"); -g.drawString(setting('compression_count') + ' / ' + setting('breath_count'), 30, 200); +g.drawString(setting('compression_count') + ' / ' + setting('breath_count'), 30, g.getHeight()*5/6); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/doztime/app.js b/apps/doztime/app-bangle1.js similarity index 100% rename from apps/doztime/app.js rename to apps/doztime/app-bangle1.js diff --git a/apps/doztime/app-bangle2.js b/apps/doztime/app-bangle2.js new file mode 100644 index 000000000..313ad9e48 --- /dev/null +++ b/apps/doztime/app-bangle2.js @@ -0,0 +1,244 @@ +// Positioning values for graphics buffers +const g_height = 80; // total graphics height +const g_x_off = 0; // position from left was 16, then 8 here +const g_y_off = (184 - g_height)/2; // vertical center for graphics region was 240 +const g_width = 240 - 2 * g_x_off; // total graphics width +const g_height_d = 28; // height of date region was 32 +const g_y_off_d = 0; // y position of date region within graphics region +const spacing = 0; // space between date and time in graphics region +const g_y_off_t = g_y_off_d + g_height_d + spacing; // y position of time within graphics region +const g_height_t = 44; // height of time region was 48 + +// Other vars +const A1 = [30,30,30,30,31,31,31,31,31,31,30,30]; +const B1 = [30,30,30,30,30,31,31,31,31,31,30,30]; +const B2 = [30,30,30,30,31,31,31,31,31,30,30,30]; +const timeColour = "#ffffff"; +const dateColours = ["#ff0000","#ff8000","#ffff00","#00ff00","#0080ff","#ff00ff","#ffffff"]; +const calen10 = {"size":26,"pt0":[18-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for usual calendar line ft w 32, 32-g, step 20 +const calen7 = {"size":26,"pt0":[48-g_x_off,16],"step":[16,0],"dx":-4.5,"dy":-4.5}; // positioning for S-day calendar line ft w 32, 62-g, step 20 +const time5 = {"size":36,"pt0":[46-g_x_off,24],"step":[22,0],"dx":-6.5,"dy":-6.5}; // positioning for lull time line ft w 48, 64-g, step 30 +const time6 = {"size":36,"pt0":[36-g_x_off,24],"step":[22,0],"dx":-6.5,"dy":-6.5}; // positioning for twinkling time line ft w 48, 48-g, step 30 +const baseYear = 11584; +const baseDate = Date(2020,11,21); // month values run from 0 to 11 +let accum = new Date(baseDate.getTime()); +let sequence = []; +let timeActiveUntil; +let addTimeDigit = false; +let dateFormat = false; +let lastX = 999999999; +let res = {}; +//var last_time_log = 0; + +var drawtime_timeout; + +// Date and time graphics buffers +var dateColour = "#ffffff"; // override later +var timeColour2 = timeColour; +var g_d = Graphics.createArrayBuffer(g_width,g_height_d,1,{'msb':true}); +var g_t = Graphics.createArrayBuffer(g_width,g_height_t,1,{'msb':true}); +// Set screen mode and function to write graphics buffers +//Bangle.setLCDMode(); +g.clear(); // start with blank screen +g.flip = function() +{ + g.setBgColor(0,0,0); + g.setColor(dateColour); + g.drawImage( + { + width:g_width, + height:g_height_d, + buffer:g_d.buffer + }, g_x_off, g_y_off + g_y_off_d); + g.setColor(timeColour2); + g.drawImage( + { + width:g_width, + height:g_height_t, + buffer:g_t.buffer + }, g_x_off, g_y_off + g_y_off_t); +}; + +setWatch(function(){ modeTime(); }, BTN, {repeat:true} ); //was BTN1 +setWatch(function(){ Bangle.showLauncher(); }, BTN, { repeat: false, edge: "falling" }); //was BTN2 +//setWatch(function(){ modeWeather(); }, BTN3, {repeat:true}); +//setWatch(function(){ toggleTimeDigits(); }, BTN4, {repeat:true}); +//setWatch(function(){ toggleDateFormat(); }, BTN5, {repeat:true}); + +Bangle.on('touch', function(button, xy) { //from Gordon Williams + if (button==1) toggleTimeDigits(); + if (button==2) toggleDateFormat(); +}); + +function buildSequence(targ){ + for(let i=0;i n > dt)-1; + let year = baseYear+parseInt(index/12); + let month = index % 12; + let day = parseInt((dt-sequence[index])/86400000); + let colour = dateColours[day % 6]; + if(day==30){ colour=dateColours[6]; } + return({"year":year,"month":month,"day":day,"colour":colour}); +} +function toggleTimeDigits(){ + addTimeDigit = !addTimeDigit; + modeTime(); +} +function toggleDateFormat(){ + dateFormat = !dateFormat; + modeTime(); +} +function formatDate(res,dateFormat){ + let yyyy = res.year.toString(12); + calenDef = calen10; + if(!dateFormat){ //ordinal format + let mm = ("0"+(res.month+1).toString(12)).substr(-2); + let dd = ("0"+(res.day+1).toString(12)).substr(-2); + if(res.day==30){ + calenDef = calen7; + let m = ((res.month+1).toString(12)).substr(-2); + return(yyyy+"-"+"S"+m); // ordinal format + } + return(yyyy+"-"+mm+"-"+dd); + } + let m = res.month.toString(12); // cardinal format + let w = parseInt(res.day/6); + let d = res.day%6; + //return(yyyy+"-"+res.month+"-"+w+"-"+d); + return(yyyy+"-"+m+"-"+w+"-"+d); +} + +function writeDozTime(text,def){ + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_t.clear(); + g_t.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_t.setFontAlign(0,0,2); g_t.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s are new + else if(text[i]=="b"){ g_t.setFontAlign(0,0,2); g_t.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s are new + else{ g_t.setFontAlign(0,0,0); g_t.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} +function writeDozDate(text,def,colour){ + + dateColour = colour; + let pts = def.pts; + let x=def.pt0[0]; + let y=def.pt0[1]; + g_d.clear(); + g_d.setFont("Vector",def.size); + for(let i in text){ + if(text[i]=="a"){ g_d.setFontAlign(0,0,2); g_d.drawString("2",x+2+def.dx,y+1+def.dy); } //+1s new + else if(text[i]=="b"){ g_d.setFontAlign(0,0,2); g_d.drawString("3",x+2+def.dx,y+1+def.dy); } //+1s new + else{ g_d.setFontAlign(0,0,0); g_d.drawString(text[i],x,y); } + x = x+def.step[0]; + y = y+def.step[1]; + } +} + +// Functions for time mode +function drawTime() +{ + let dt = new Date(); + let date = ""; + let timeDef; + let x = 0; + dt.setDate(dt.getDate()); + if(addTimeDigit){ + x = + 10368*dt.getHours()+172.8*dt.getMinutes()+2.88*dt.getSeconds()+0.00288*dt.getMilliseconds(); + let msg = "00000"+Math.floor(x).toString(12); + let time = msg.substr(-5,3)+"."+msg.substr(-2); + let wait = 347*(1-(x%1)); + timeDef = time6; + } else { + x = + 864*dt.getHours()+14.4*dt.getMinutes()+0.24*dt.getSeconds()+0.00024*dt.getMilliseconds(); + let msg = "0000"+Math.floor(x).toString(12); + let time = msg.substr(-4,3)+"."+msg.substr(-1); + let wait = 4167*(1-(x%1)); + timeDef = time5; + } + if(lastX > x){ res = getDate(dt); } // calculate date once at start-up and once when turning over to a new day + date = formatDate(res,dateFormat); + if(dt2200)) { + } else { + // We have a GPS time. Set time + setTime(g.time.getTime()/1000); + } + }); + Bangle.setGPSPower(1,"time"); + setTimeout(fixTime, 10*60*1000); // every 10 minutes +} +// Start time fixing with GPS on next 10 minute interval +setTimeout(fixTime, ((60-(new Date()).getMinutes()) % 10) * 60 * 1000); diff --git a/apps/gpstouch/Changelog b/apps/gpstouch/Changelog index 7f837e50e..e4a0bdfe8 100644 --- a/apps/gpstouch/Changelog +++ b/apps/gpstouch/Changelog @@ -1 +1,2 @@ 0.01: First version +0.02: Enchanced contrast of icon image diff --git a/apps/gpstouch/gpstouch.icon.js b/apps/gpstouch/gpstouch.icon.js index c4cf85676..3e05da0ff 100644 --- a/apps/gpstouch/gpstouch.icon.js +++ b/apps/gpstouch/gpstouch.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4UA///j+EAYO/uYDB//wCYcPBA4AFh/ABZMDBbkX6gLIgtX6tQBY9VBYNVBY0BBYdABYsFqoACEgQLDitVtWpqtUBYtVq2q1WVGAQLErQLB0oLFHQNqBYIkBHgMDIwYKBAAJIDIweqz/2BYJtDBYI6Bv/9HgILHYwILGh4gBBYWfbooLF6AjPBYW//wLGL4Wv/RfGNZaDIBYibEBYizIBYjLDBYzXBd4TXCBZ60BBYRqEBZpUBBYRSFJAQLCA4b7BHgQLFgYLGIwYLEgoLBHQYLEgILBHQYLEgALBAoYLFi/UBZMHBZUD6ALKApQAFBbHwBZMP/4ABBwgIDA=")) +require("heatshrink").decompress(atob("mEw4UA///iADCn+EqoAWqAuJgoLcn/8BZENGwNwBY/VBYNXBY0DJ4fABYoiCEggLDmtX1Wq6tcBYvVrQLB0owCBYdVtQLB1NVBYg6BBQIABHgQLCgIuCGAVABYcNqwtBGIOVJAILFyoCCBY5eBBdo7IgIIB1t6BYJfENZaDB9QKB1aDFBYKbEBYizBrwLB2qnFdwSmCX401cYdUBZTjGfYgHCBZB2BBYhUBAARSBBYhICAAIGCBYkVBQJSCBYpICIwQLFHgQ6CBYo8CHQQLFHgQFDBYsVQIQLHgo6DBY0BHQYLGgY6DBYwAFBbCjDACY")) diff --git a/apps/gpstouch/gpstouch.png b/apps/gpstouch/gpstouch.png index c411356ae..a40419a3f 100644 Binary files a/apps/gpstouch/gpstouch.png and b/apps/gpstouch/gpstouch.png differ diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index 5e60068aa..1873649f9 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -2,9 +2,7 @@ 0.02: Remove messages on disconnect 0.03: Handling of message actions (ok/clear) 0.04: Added common bundleId's -0.05: Added more bundleId's (app-id's which can be used to - determine a friendly app name in the notifications) +0.05: Added more bundleId's (app-id's which can be used to determine a friendly app name in the notifications) 0.06: Fix (not) popupping up old messages -0.07: Added more details from music (instead of Undefined) - Added more app identifiers - +0.07: Added more details from music (instead of Undefined), added more app identifiers +0.08: Added more app identifiers, added 'cannot display' in case a message goes empty because of replacements diff --git a/apps/ios/boot.js b/apps/ios/boot.js index 8ccfb617d..d317c23b0 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -66,6 +66,7 @@ E.on('notify',msg=>{ "com.apple.mobilecal": "Calendar", "com.apple.mobilemail": "Mail", "com.apple.mobilephone": "Phone", + "com.apple.mobileslideshow": "Pictures", "com.apple.MobileSMS": "SMS Message", "com.apple.Passbook": "iOS Wallet", "com.apple.podcasts": "Podcasts", @@ -83,6 +84,7 @@ E.on('notify',msg=>{ "com.ifttt.ifttt" : "IFTTT", "com.jumbo.app" : "Jumbo", "com.linkedin.LinkedIn" : "LinkedIn", + "com.marktplaats.iphone": "Marktplaats", "com.microsoft.Office.Outlook" : "Outlook Mail", "com.nestlabs.jasper.release" : "Nest", "com.netflix.Netflix" : "Netflix", @@ -90,6 +92,7 @@ E.on('notify',msg=>{ "com.skype.skype": "Skype", "com.skype.SkypeForiPad": "Skype", "com.spotify.client": "Spotify", + "com.storytel.iphone": "Storytel", "com.strava.stravaride": "Strava", "com.tinyspeck.chatlyio": "Slack", "com.toyopagroup.picaboo": "Snapchat", @@ -98,6 +101,8 @@ E.on('notify',msg=>{ "com.vilcsak.bitcoin2": "Coinbase", "com.wordfeud.free": "WordFeud", "com.zhiliaoapp.musically": "TikTok", + "io.robbie.HomeAssistant": "Home Assistant", + "net.weks.prowl": "Prowl", "net.whatsapp.WhatsApp": "WhatsApp", "nl.ah.Appie": "Albert Heijn", "nl.postnl.TrackNTrace": "PostNL", @@ -118,7 +123,7 @@ E.on('notify',msg=>{ new : msg.new, title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer), subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer), - body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) + body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) || "Cannot display" }); // TODO: posaction/negaction? }); diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index c171ec3d4..f5d8346da 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -5,4 +5,5 @@ 0.05: Additional icons for (1) charging and (2) bat < 30%. 0.06: Fix - Alarm disabled, if clock was closed. 0.07: Added settings to adjust data that is shown for each row. -0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. \ No newline at end of file +0.08: Support for multiple screens. 24h graph for steps + HRM. Fullscreen Mode. +0.09: Tab anywhere to open the launcher. \ No newline at end of file diff --git a/apps/lcars/README.md b/apps/lcars/README.md index 97695a408..4bf5218f6 100644 --- a/apps/lcars/README.md +++ b/apps/lcars/README.md @@ -8,13 +8,15 @@ To contribute you can open a PR at this [GitHub Repo]( https://github.com/peerda * LCARS Style watch face. * Full screen mode - widgets are still loaded. * Supports multiple screens with different data. + * Tab anywhere to open the launcher. * [Screen 1] Date + Time + Lock status. * [Screen 1] Shows randomly images of real planets. * [Screen 1] Shows different states such as (charging, out of battery, GPS on etc.) * [Screen 1] Swipe up/down to activate an alarm. * [Screen 1] Shows 3 customizable datapoints on the first screen. * [Screen 1] The lower orange line indicates the battery level. - * [Screen 2] Display month graphs for steps + hrm on the second screen. + * [Screen 2] Display graphs for steps + hrm on the second screen. + * [Screen 2] Switch between day/month via swipe up/down. ## Multiple screens support diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index 74d0450c0..167adad2d 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -24,6 +24,7 @@ let cOrange = "#FF9900"; let cPurple = "#FF00DC"; let cWhite = "#FFFFFF"; let cBlack = "#000000"; +let cGrey = "#9E9E9E"; /* * Global lcars variables @@ -147,14 +148,17 @@ function printData(key, y, c){ } g.setColor(c); + g.fillRect(79, y-2, 87 ,y+18); + + g.setFontAlign(1,-1,0); + g.drawString(value, 131, y); + + g.setColor(c); + g.setFontAlign(-1,-1,0); g.fillRect(133, y-2, 165 ,y+18); g.fillCircle(161, y+8, 10); g.setColor(cBlack); g.drawString(text, 135, y); - - g.setColor(c); - g.setFontAlign(1,-1,0); - g.drawString(value, 130, y); } function drawHorizontalBgLine(color, x1, x2, y, h){ @@ -191,13 +195,14 @@ function drawState(){ return; } - g.clearRect(20, 93, 77, 170); - g.setColor(cWhite); - var bat = E.getBattery(); - var current = new Date(); - var hours = current.getHours(); + g.clearRect(20, 93, 75, 170); + g.setFontAlign(0, 0, 0); + g.setFontAntonioMedium(); if(!isAlarmEnabled()){ + var bat = E.getBattery(); + var current = new Date(); + var hours = current.getHours(); var iconImg = Bangle.isCharging() ? iconCharging : bat < 30 ? iconNoBattery : @@ -206,16 +211,16 @@ function drawState(){ hours % 4 == 1 ? iconMars : hours % 4 == 2 ? iconMoon : iconEarth; - g.drawImage(iconImg, 29, 104); + g.drawImage(iconImg, 24, 118); + g.setColor(cWhite); + g.drawString("STATUS", 24+25, 108); } else { // Alarm within symbol - g.setFontAntonioMedium(); - g.setFontAlign(0, 0, 0); g.setColor(cOrange); - g.drawString("ALARM", 29+25, 107); + g.drawString("ALARM", 24+25, 108); g.setColor(cWhite); g.setFontAntonioLarge(); - g.drawString(getAlarmMinutes(), 29+25, 107+35); + g.drawString(getAlarmMinutes(), 24+25, 108+35); } g.setFontAlign(-1, -1, 0); @@ -236,7 +241,7 @@ function drawPosition0(){ var bat = E.getBattery() / 100.0; var batX2 = parseInt((172 - 35) * bat + 35); drawHorizontalBgLine(cOrange, 35, batX2, 171, 5); - drawHorizontalBgLine(cPurple, batX2+10, 172, 171, 5); + drawHorizontalBgLine(cGrey, batX2+10, 172, 171, 5); // Draw logo drawLock(); @@ -247,7 +252,7 @@ function drawPosition0(){ var currentDate = new Date(); var timeStr = locale.time(currentDate,1); g.setFontAntonioLarge(); - g.drawString(timeStr, 28, 10); + g.drawString(timeStr, 29, 10); // Write date g.setColor(cWhite); @@ -255,7 +260,7 @@ function drawPosition0(){ var dayStr = locale.dow(currentDate, true).toUpperCase(); dayStr += " " + currentDate.getDate(); dayStr += " " + currentDate.getFullYear(); - g.drawString(dayStr, 29, 56); + g.drawString(dayStr, 32, 56); // Draw data g.setFontAlign(-1, -1, 0); @@ -401,7 +406,7 @@ function draw(){ * Step counter via widget */ function getSteps() { - var steps = 0 + var steps = 0; try { health = require("health"); } catch(ex) { @@ -553,6 +558,10 @@ Bangle.on("drag", e => { } }); +Bangle.on("touch", e => { + Bangle.showLauncher(); +}); + /* * Lets start widgets, listen for btn etc. diff --git a/apps/lcars/screenshot.png b/apps/lcars/screenshot.png index b3dfd4200..fba55a9f7 100644 Binary files a/apps/lcars/screenshot.png and b/apps/lcars/screenshot.png differ diff --git a/apps/mclockplus/ChangeLog b/apps/mclockplus/ChangeLog index a1cecc698..097545ba8 100644 --- a/apps/mclockplus/ChangeLog +++ b/apps/mclockplus/ChangeLog @@ -1,2 +1,3 @@ 0.01: Created app 0.02: Use Bangle.setUI for button/launcher handling +0.03: Allow widgets to detect this is a clock diff --git a/apps/mclockplus/mclockplus.app.js b/apps/mclockplus/mclockplus.app.js index 4c74ce1be..970397478 100644 --- a/apps/mclockplus/mclockplus.app.js +++ b/apps/mclockplus/mclockplus.app.js @@ -304,15 +304,14 @@ Bangle.on('lcdPower',function(on) { }); g.clear(); +// Show launcher when button pressed +Bangle.setUI("clock"); Bangle.loadWidgets(); Bangle.drawWidgets(); // Update time once a second timeInterval = setInterval(showTime, 1000); showTime(); -// Show launcher when button pressed -Bangle.setUI("clock"); - // Start stopwatch when BTN3 is pressed setWatch(() => {swInterval=setInterval(stopWatch, 1000);stopWatch();}, BTN3, {repeat:false,edge:"falling"}); B3 = 1; // BTN3 is bound to start the stopwatch diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index fe46ba97a..4f0498e92 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -23,3 +23,4 @@ 0.14: Hide widget when all unread notifications are dismissed from phone 0.15: Don't buzz when Quiet Mode is active 0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147) +0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font diff --git a/apps/messages/widget.js b/apps/messages/widget.js index e831e5b68..1239ef262 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,8 +1,9 @@ -WIDGETS["messages"]={area:"tl",width:0,draw:function() { +WIDGETS["messages"]={area:"tl", width:0, iconwidth:23, +draw:function() { Bangle.removeListener('touch', this.touch); if (!this.width) return; var c = (Date.now()-this.t)/1000; - g.reset().clearRect(this.x,this.y,this.x+this.width,this.y+23); + g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+this.iconwidth); g.drawImage((c&1) ? atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+DAADDAADDAADDwAPD8A/DOBzDDn/DA//DAHvDAPvjAPvjAPvjAPvh///gf/vAAD+AAB8AAAAA==") : atob("GBiBAAAAAAAAAAAAAAAAAAAAAB//+D///D///A//8CP/xDj/HD48DD+B8D/D+D/3vD/vvj/vvj/vvj/vvh/v/gfnvAAD+AAB8AAAAA=="), this.x, this.y); //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute let settings = require('Storage').readJSON("messages.settings.json", true) || {}; @@ -17,7 +18,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { WIDGETS["messages"].t=Date.now(); // first time WIDGETS["messages"].l=Date.now()-10000; // last buzz if (quiet) WIDGETS["messages"].t -= 500000; // if quiet, set last time in the past so there is no buzzing - WIDGETS["messages"].width=64; + WIDGETS["messages"].width=this.iconwidth; Bangle.drawWidgets(); Bangle.setLCDPower(1);// turns screen on },hide:function() { @@ -37,7 +38,7 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { b(); },touch:function(b,c) { var w=WIDGETS["messages"]; - if (!w||!w.width||c.xw.x+w.width||c.yw.y+23) return; + if (!w||!w.width||c.xw.x+w.width||c.yw.y+w.iconwidth) return; load("messages.app.js"); }}; /* We might have returned here if we were in the Messages app for a @@ -46,4 +47,4 @@ want to buzz but should still show that there are unread messages. */ if (global.MESSAGES===undefined) (function() { var messages = require("Storage").readJSON("messages.json",1)||[]; if (messages.some(m=>m.new)) WIDGETS["messages"].show(true); -})(); +})(); \ No newline at end of file diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog index 7b83706bf..653f859ae 100644 --- a/apps/mylocation/ChangeLog +++ b/apps/mylocation/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Enhanced icon, make it bolder diff --git a/apps/mylocation/mylocation.icon.js b/apps/mylocation/mylocation.icon.js index bfb38d5ac..b79f5875f 100644 --- a/apps/mylocation/mylocation.icon.js +++ b/apps/mylocation/mylocation.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4UA///t/7j/P3/vB4cBqtVoAbHBQIABBQ0FBYdQBYsVBYdUERIkGHIQADHoguEGAwuEGAwKFBZg8DHQw8EBYNf/1Vq3/8oLDIwNf/Wpv//0oLG9Wq3/qBYJUCBYuqBaBqBBYW+BepHEBbybCBYP+BYSnErYLDyoLFAANq/r8Ga5T7MBZZUBAAhSCfhA6DBZhIGBQg8FHQg8GHQgwGFwowFBQwwDFwwLMlS7Bqta1AKEn2q1K1C1WgBYf/1WqBYIDB1QKCgYLC0taBYoXB/QICBY0//7vBAAQ8EEgIABCwwME9QVEA")) +require("heatshrink").decompress(atob("mEw4UA///gH4AYPO/QPDgNVqtADY/1BYNfBQ0PBQIAB+ALFmoLDrgLF6oLDq4KEgYKDBYPABYcNBYlVuAuIGAwuEAANUBYYKFHgg6Bq4ZCr4DBHgQLBvWq2te1WlBYZGBBYOr1Wq1qSDBYNqBIILDKgQLLgoLHqBqDBfJHLBZBrOgKPCBYiPCU4NaBYe1WYrABBQLCCfgYGCrwVBa4kAirvKNgIAErgLDKgIAEKQQ8EAAY6DBZhIDIww8GHQg8GHQgwGFwowEFwx5EOog8GHQ0AlWpBYNq1AKFWIILBAYOgBYbICytWAgQKCgTgDcwYXGAAgvGAAY8EEgYWGBgoVEA==")) diff --git a/apps/mylocation/mylocation.png b/apps/mylocation/mylocation.png index 7148990a4..038ee177e 100644 Binary files a/apps/mylocation/mylocation.png and b/apps/mylocation/mylocation.png differ diff --git a/apps/pastel/ChangeLog b/apps/pastel/ChangeLog index afeb305c5..627531f03 100644 --- a/apps/pastel/ChangeLog +++ b/apps/pastel/ChangeLog @@ -7,3 +7,4 @@ 0.07: Added info line that cycles on BTN1/BTN3 (or vitual buttons on a bangle 2) 0.08: Added dependancy on MyLocation 0.09: Added dependancy on Pedometer Widget +0.10: Added Weather line, fixed issues on a Bangle 1, update every minute diff --git a/apps/pastel/README.md b/apps/pastel/README.md index 66ae0e189..b396386af 100644 --- a/apps/pastel/README.md +++ b/apps/pastel/README.md @@ -1,45 +1,83 @@ # Pastel Clock - *a configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times* + *a configurable clock with custom fonts, background and optional weather icons. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times* * Designed specifically for Bangle 1 and Bangle 2 * A choice of 7 different custom fonts * Supports the Light and Dark themes -* Has a settings menu, change font, enable/disable the grid +* Has a settings menu, change font, enable/disable the grid, weather icons * On Bangle 1 use BTN1,BTN3 to cycle through the info display (Date, ID, Batt %, Ram % etc) * On Bangle 2 touch the top right/top left to cycle through the info display (Date, ID, Batt %, Ram % etc) +* The information display will cycle on each screen update * Uses mylocation.json from MyLocation app to calculate sunrise and sunset times for your location * Uses pedometer widget to get latest step count +* Use the weather widget to get weather status * Dependant apps are installed when Pastel installs +* The screen is updated every minute to save battery power +* The weather display will display temperature and wind speed on alternate screen refreshes I came up with the name Pastel due to the shade of the grid background. Written by: [Hugh Barney](https://github.com/hughbarney) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/) -## Lato + +## Weather Support + + +![](screenshot_pastel.png) + +Pastel installs the weather app and weather widget. You may want to +hide the weather widget display, this can be done through the weather +widget settings. You should first get the weather app working. If +the weather App is not working, then it is not going to work for +Pastel. + +The following weather icons are supported. + +![](weather_icons.png) + +Mostly cloudy, Sunny, Mostly Sunny, Snow, Rain. + +The triangle icon shows there is a problem connecting to GadgetBridge and the weather service. +You should follow the setup and trouble shooting guide for the Weather App. + +If you find the weather / gadgetbridge service unreliable you can +disable weather updates to pastel through the settings app. + + +## Fonts + +### Lato ![](screenshot_lato.png) -## Architect +### Architect ![](screenshot_architect.png) -## Gochihand +### Gochihand ![](screenshot_gochihand.png) -## Monoton +### Monoton ![](screenshot_monoton.png) -## Elite +### Elite ![](screenshot_elite.png) -## Cabin Sketch +### Cabin Sketch ![](screenshot_cabinsketch.png) -## Orbitron +### Orbitron ![](screenshot_orbitron.png) +### The Grid + +Setting the grid on provides a graph paper style background to the App. +The grid is not supported on a Bangle 1 due to flicker issues. + +![](screenshot_grid.png) + diff --git a/apps/pastel/pastel.app.js b/apps/pastel/pastel.app.js index aa4f6abf8..db60a2738 100644 --- a/apps/pastel/pastel.app.js +++ b/apps/pastel/pastel.app.js @@ -1,10 +1,22 @@ var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); require("f_latosmall").add(Graphics); +const storage = require('Storage'); +const locale = require("locale"); const SETTINGS_FILE = "pastel.json"; const LOCATION_FILE = "mylocation.json"; let settings; let location; +// cloud, sun, partSun, snow, rain, storm, error +// create 1 bit, max contrast, brightness set to 85 +var cloudIcon = require("heatshrink").decompress(atob("kEggIfcj+AAYM/8ADBuFwAYPAmADCCAMBwEf8ADBhFwg4aBnEPAYMYjAVBhgDDDoQDHCYc4jwDB+EP///FYIDBMTgA==")); +var sunIcon = require("heatshrink").decompress(atob("kEggILIgOAAZkDAYPAgeBwPAgIFBBgPhw4TBp/yAYMcnADBnEcAYMwhgDBsEGgE/AYP8AYYLDCYgbDEYYrD8fHIwI7CIYZLDL54AHA==")); +var sunPartIcon = require("heatshrink").decompress(atob("kEggIHEmADJjEwsEAjkw8EAh0B4EAg35wEAgP+CYMDwv8AYMDBAP2g8HgH+g0DBYMMgPwAYX8gOMEwMG3kAg8OvgSBjg2BgcYGQIcBAY5CBg0Av//HAM///4MYgNBEIMOCoUMDoUAnBwGkEA")); +var snowIcon = require("heatshrink").decompress(atob("kEggITQj/AAYM98ADBsEwAYPAjADCj+AgOAj/gAYMIuEHwEAjEPAYQVChk4AYQhCAYcYBYQTDnEPgEB+EH///IAQACE4IAB8EICIPghwDB4EeBYNAjgDBg8EAYQYCg4bCgZuFA==")); +var rainIcon = require("heatshrink").decompress(atob("kEggIPMh+AAYM/8ADBuFwAYPgmADB4EbAYOAj/ggOAhnwg4aBnAeCjEcCIMMjADCDoQDHjAPCnAXCuEP///8EDAYJECAAXBwkAgPDhwDBwUMgEEhkggEOjFgFgMQLYQAOA==")); +var errIcon = require("heatshrink").decompress(atob("kEggILIgOAAYsD4ADBg/gAYMGsADBhkwAYsYjADCjgDBmEMAYNxxwDBsOGAYPBwYDEgOBwOAgYDB4EDHYPAgwDBsADDhgDBFIcwjAHBjE4AYMcmADBhhNCKIcG/4AGOw4A==")); + + function loadSettings() { settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings.grid = settings.grid||false; @@ -93,7 +105,49 @@ function prevInfo() { } } -var mm_prev = "xx"; + +/** +Choose weather icon to display based on condition. +Based on function from the Bangle weather app so it should handle all of the conditions +sent from gadget bridge. +*/ +function chooseIcon(condition) { + condition = condition.toLowerCase(); + if (condition.includes("thunderstorm")) return stormIcon; + if (condition.includes("freezing")||condition.includes("snow")|| + condition.includes("sleet")) { + return snowIcon; + } + if (condition.includes("drizzle")|| + condition.includes("shower")) { + return rainIcon; + } + if (condition.includes("rain")) return rainIcon; + if (condition.includes("clear")) return sunIcon; + if (condition.includes("few clouds")) return partSunIcon; + if (condition.includes("scattered clouds")) return cloudIcon; + if (condition.includes("clouds")) return cloudIcon; + if (condition.includes("mist") || + condition.includes("smoke") || + condition.includes("haze") || + condition.includes("sand") || + condition.includes("dust") || + condition.includes("fog") || + condition.includes("ash") || + condition.includes("squalls") || + condition.includes("tornado")) { + return cloudIcon; + } + return cloudIcon; +} + +/** +Get weather stored in json file by weather app. +*/ +function getWeather() { + let jsonWeather = storage.readJSON('weather.json'); + return jsonWeather; +} function draw() { var d = new Date(); @@ -114,20 +168,28 @@ function draw() { var h = g.getHeight(); var x = (g.getWidth()/2); var y = (g.getHeight()/3); - - g.reset(); - if (process.env.HWVERSION == 1) { - // avoid flicker on a bangle 1 by comparing with previous minute - if (mm_prev != mm) { - mm_prev = mm; - g.clearRect(0, 30, w, h - 24); - } + var weatherJson = getWeather(); + var w_temp; + var w_icon; + var w_wind; + + if (settings.weather && weatherJson && weatherJson.weather) { + var currentWeather = weatherJson.weather; + const temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/); + w_temp = temp[1] + " " + temp[2]; + w_icon = chooseIcon(currentWeather.txt); + const wind = locale.speed(currentWeather.wind).match(/^(\D*\d*)(.*)$/); + w_wind = wind[1] + " " + wind[2] + " " + (currentWeather.wrose||'').toUpperCase(); } else { - // on a b2 safe to just clear anyway as there is no flicker - g.clearRect(0, 30, w, h - 24); + w_temp = "Err"; + w_wind = "???"; + w_icon = errIcon; } - + + g.reset(); + g.clearRect(0, 30, w, h - 24); + // draw a grid like graph paper if (settings.grid && process.env.HWVERSION !=1) { g.setColor("#0f0"); @@ -139,6 +201,18 @@ function draw() { g.setColor(g.theme.fg); + // draw weather line + if (settings.weather) { + g.drawImage(w_icon, (w/2) - 40, 24); + g.setFontLatoSmall(); + g.setFontAlign(-1,0); // left aligned + if (drawCount % 2 == 0) + g.drawString(w_temp, (w/2) + 6, 24 + ((y - 24)/2)); + else + g.drawString( (w_wind.split(' ').slice(0, 2).join(' ')), (w/2) + 6, 24 + ((y - 24)/2)); + // display first 2 words of the wind string eg '4 mph' + } + if (settings.font == "Architect") g.setFontArchitect(); else if (settings.font == "GochiHand") @@ -161,36 +235,39 @@ function draw() { // for the colon g.setFontAlign(0,-1); // centre aligned - - if (d.getSeconds()&1) { - g.drawString(":", x,y); - } else { - // on bangle 1, we are not using clearRect(), hide : by printing over it in reverse color - if (process.env.HWVERSION == 1) { - g.setColor(g.theme.bg); - g.drawString(":", x,y); - g.setColor(g.theme.fg); - } - } - + g.drawString(":", x,y); g.setFontLatoSmall(); g.setFontAlign(0, -1); g.drawString((infoData[infoMode].calc()), w/2, h - 24 - 24); - if (drawCount % 3600 == 0) + // recalc sunrise / sunset every hour + if (drawCount % 60 == 0) updateSunRiseSunSet(new Date(), location.lat, location.lon); drawCount++; + queueDraw(); } -// Only update when display turns on -if (process.env.BOARD!="SMAQ3") // hack for Q3 which is always-on -Bangle.on('lcdPower', function(on) { - if (secondInterval) - clearInterval(secondInterval); - secondInterval = undefined; - if (on) - secondInterval = setInterval(draw, 1000); - draw(); +// timeout used to update every minute +var drawTimeout; + +// schedule a draw for the next minute +function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + nextInfo(); + draw(); + }, 60000 - (Date.now() % 60000)); +} + +// Stop updates when LCD is off, restart when on +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } }); Bangle.setUI("clockupdown", btn=> { @@ -204,8 +281,6 @@ loadFonts(); loadLocation(); g.clear(); -var secondInterval = setInterval(draw, 1000); -draw(); - Bangle.loadWidgets(); Bangle.drawWidgets(); +draw(); diff --git a/apps/pastel/pastel.settings.js b/apps/pastel/pastel.settings.js index fad36964d..bf83fa7c2 100644 --- a/apps/pastel/pastel.settings.js +++ b/apps/pastel/pastel.settings.js @@ -4,6 +4,7 @@ // initialize with default settings... let s = { 'grid': false, + 'weather': false, 'font': "Lato" } @@ -39,8 +40,16 @@ value: s.grid, format: () => (s.grid ? 'Yes' : 'No'), onchange: () => { - s.grid = !s.grid - save() + s.grid = !s.grid; + save(); + }, + }, + 'Show Weather': { + value: s.weather, + format: () => (s.weather ? 'Yes' : 'No'), + onchange: () => { + s.weather = !s.weather; + save(); }, } }) diff --git a/apps/pastel/screenshot_grid.png b/apps/pastel/screenshot_grid.png new file mode 100644 index 000000000..7b993353b Binary files /dev/null and b/apps/pastel/screenshot_grid.png differ diff --git a/apps/pastel/screenshot_pastel.png b/apps/pastel/screenshot_pastel.png index d489f1914..c792ada8d 100644 Binary files a/apps/pastel/screenshot_pastel.png and b/apps/pastel/screenshot_pastel.png differ diff --git a/apps/pastel/weather_icons.png b/apps/pastel/weather_icons.png new file mode 100644 index 000000000..59d4f007f Binary files /dev/null and b/apps/pastel/weather_icons.png differ diff --git a/apps/pebble/ChangeLog b/apps/pebble/ChangeLog index d92be5e9c..0cba5a2b2 100644 --- a/apps/pebble/ChangeLog +++ b/apps/pebble/ChangeLog @@ -4,3 +4,4 @@ 0.04: Fix widget hiding code (fix #1046) 0.05: Fix typo in settings - Purple 0.06: Added dependancy on Pedometer Widget +0.07: Fixed icon and ong file to 48x48 diff --git a/apps/pebble/pebble.icon.js b/apps/pebble/pebble.icon.js index ecd7feb7f..1c1166156 100644 --- a/apps/pebble/pebble.icon.js +++ b/apps/pebble/pebble.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("oFAwgNKiIAIFqofegIf/DAUzAAMyAwUQD60T/4ACD7Q/cPxIf/YCofcDhYiSXYYfuUZgf/D/4f/D6USkUgD/4fuogAID6vtDw/UD6vu6geF73kb6vuEAtN9wfYMIneD7JADDwIfaIAJdBD7YgBHwQfbAAgfkf6Qf/D/4feogAID6oAND/4f/iAdJD/4f/D/4fUDxYABD74iODiAftTZgfnYYczAAMyD7UT/4ACH/S+bD8DAKD9Y=")) +require("heatshrink").decompress(atob("mEw4UA///ssp4XthFCBwUBqoABqAaGBZcFBZdX1W1qgLHrwLKqv/6oLJAAILHioLJn5qBAAYLEBQoLeHQQABv4LjGAgLYq2qAAOlBbBHFBdPAKcQLdWcb7jAAoLcn4LKgEVHQVUBQsAgoLLq//6oLIr2q2oXJBZQvCqALGgILTA=")) diff --git a/apps/pebble/pebble.png b/apps/pebble/pebble.png index 10f5adb56..368e08750 100644 Binary files a/apps/pebble/pebble.png and b/apps/pebble/pebble.png differ diff --git a/apps/presentor/ChangeLog b/apps/presentor/ChangeLog new file mode 100644 index 000000000..00833cf91 --- /dev/null +++ b/apps/presentor/ChangeLog @@ -0,0 +1,8 @@ +0.1: Start of app. +0.5: BLE keyboard functionality. +1.0: BLE mouse functionality to scroll back/forward. +1.5: Added accelerator style mouse. +2.0: Added touchpad style mouse. +2.1: Initial internal git(hub) release. Added icon and such. +2.2: Begin work on presentation parts. +3.0: Presentation parts! \ No newline at end of file diff --git a/apps/presentor/README.md b/apps/presentor/README.md new file mode 100644 index 000000000..8b22eb228 --- /dev/null +++ b/apps/presentor/README.md @@ -0,0 +1,2 @@ +# Presentor +Use your Bangle to present! diff --git a/apps/presentor/app-icon.js b/apps/presentor/app-icon.js new file mode 100644 index 000000000..76939d548 --- /dev/null +++ b/apps/presentor/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4ASlgADGmIxwLV4wqGQowfWZQwjKw4wJF7ghBmVWmQlELYoweFwYABGAwrHF7IuFGAwrIF7AuHMJADGF0AwHAYovWFxaSHADQuDEgIADYZYucAQOB1fQ1eBmQwiFwlX6AAE1gqBGD6MEmQqBwIICwIGB0rDeWYksFAIuBz+fvQHC6D0dcQssEwIuB4fC4V0M4VXF7YuFDYLqBlnDF4WeHAYugL5N6L4I4BF0IvB0vQvdQGAJeBY4YucmQxEAgJgBvYOBdwYQDFzUzmZ/EllXFIKKBAYVXBwSMaller7fFAoKSBAAOBFQK7dPoJfFEoYADRjgmEeIzFFdUQAOdUIumdRLtDAAIufdRQKCr8zCQjqmE4NeF4YudWxpqCFyovBK5LqiF6DqdR6D0BdTYwJGg5sBdTQwKEAIwHdTQwMSpQueYaAufGBouiGBYukGBIumGA4uoGAouqGAYABF1QAl")) diff --git a/apps/presentor/app.js b/apps/presentor/app.js new file mode 100644 index 000000000..6b7450a0c --- /dev/null +++ b/apps/presentor/app.js @@ -0,0 +1,471 @@ +// Presentor by 7kasper (Kasper Müller) +// Version 3.0 + +const SpecialReport = new Uint8Array([ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x85, 0x01, // REPORT_ID (1) + 0x09, 0x01, // USAGE (Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x05, // USAGE_MAXIMUM (Button 5) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x03, // REPORT_SIZE (3) + 0x81, 0x03, // INPUT (Cnst,Var,Abs) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT (Data,Var,Rel) + 0x05, 0x0c, // USAGE_PAGE (Consumer Devices) + 0x0a, 0x38, 0x02, // USAGE (AC Pan) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x01, // REPORT_COUNT (1) + 0x81, 0x06, // INPUT (Data,Var,Rel) + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x85, 0x02, // REPORT_ID (2) + 0x05, 0x07, // USAGE_PAGE (Keyboard) + 0x19, 0xe0, // USAGE_MINIMUM (Keyboard LeftControl) + 0x29, 0xe7, // USAGE_MAXIMUM (Keyboard Right GUI) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x01, // REPORT_COUNT (1) + 0x81, 0x01, // INPUT (Cnst,Ary,Abs) + 0x19, 0x00, // USAGE_MINIMUM (Reserved (no event indicated)) + 0x29, 0x73, // USAGE_MAXIMUM (Keyboard F24) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x73, // LOGICAL_MAXIMUM (115) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x00, // INPUT (Data,Ary,Abs) + 0xc0 // END_COLLECTION +]); + +const MouseButton = { + NONE : 0, + LEFT : 1, + RIGHT : 2, + MIDDLE : 4, + BACK : 8, + FORWARD: 16 +}; + +const kb = require("ble_hid_keyboard"); + +const Layout = require("Layout"); +const Locale = require("locale"); +let mainLayout = new Layout({ + 'type': 'v', + filly: 1, + c: [ + { + type: 'txt', + font: '6x8', + label: 'Presentor', + valign: -1, + halign: 0, + col: g.theme.fg1, + // bgCol: g.theme.bg2, + bgCol: '#00F', + fillx: 1, + }, { + type: 'h', + fillx: 1, + c: [ + { + type: 'txt', + font: '15%', + label: '00:00', + id: 'Time', + halign: -1, + pad: 3 + }, { + fillx: 1 + }, { + type: 'txt', + font: '15%', + label: '00:00', + id: 'Timer', + halign: 1, + pad: 3 + } + ] + }, { + type: 'txt', + font: '10%', + label: '+00:00', + id: 'RestTime', + col: '#fff' + }, { + type: 'txt', + font: '10%', + label: '--------------' + }, { + type: 'txt', + font: '15%', + label: 'Presenting', + id: 'Subject' + }, { + type: 'txt', + font: '6x8', + label: 'Swipe up to start the time.', + id: 'Notes', + col: '#ff0', + fillx: 1, + filly: 1, + valign: 1 + } + ] +}, {lazy:true}); + +let settings = {pparts: [], sversion: 0}; +let HIDenabled = true; + +// Application variables +let pparti = -1; +let ppartBuzzed = false; +let restBuzzed = false; + +let lastx = 0; +let lasty = 0; + +// Mouse states +let holding = false; +let trackPadMode = false; + +// Timeout IDs. +let timeoutId = -1; +let timeoutHolding = -1; +let timeoutDraw = -1; + + +let homeRoll = 0; +let homePitch = 0; +let mCal = 0; +let mttl = 0; +let cttl = 0; + +// BT helper. +let clearToSend = true; + +// Presentation Timers +let ptimers = []; + +function delay(t, v) { + return new Promise((resolve) => { + setTimeout(resolve, t) + }); +} + +function formatTimePart(time) { + time = Math.floor(Math.abs(time)); + return time < 10 ? `0${time}` : `${time}`; +} + +function formatTime(time, doPlus) { + if (time == Infinity) return ' --:-- '; + return `${time < 0 ? '-' : (doPlus ? '+' : '')}${formatTimePart(time/60)}:${formatTimePart(time%60)}`; +} + +function loadSettings() { + settings = require("Storage").readJSON('presentor.json'); + for (let i = 0; i < settings.pparts.length; i++) { + ptimers[i] = { + active: false, + tracked: -1, + left: settings.pparts[i].minutes * 60 + settings.pparts[i].seconds + }; + } +} + +function getCurrentTimer() { + if (pparti < 0) return Infinity; + if (!settings.pparts || pparti >= settings.pparts.length) return Infinity; + if (ptimers[pparti].tracked == -1) return Infinity; + ptimers[pparti].left -= (getTime() - ptimers[pparti].tracked); + ptimers[pparti].tracked = getTime(); + // if we haven't buzzed yet and timer became negative just buzz here. + // TODO better place? + if (ptimers[pparti].left <= 0 && !ppartBuzzed) { + Bangle.buzz(400) + .then(() => delay(400)) + .then(() => Bangle.buzz(400)); + ppartBuzzed = true; + } + return ptimers[pparti].left; +} + +function getRestTime() { + let rem = 0; + // Add all remaining time from previous presentation parts. + for (let i = 0; i < pparti; i++) { + rem += ptimers[i].left; + } + if (pparti >= 0 && pparti < ptimers.length && ptimers[pparti].left < 0) { + rem += ptimers[pparti].left; + } + // if we haven't buzzed yet and timer became negative just buzz here. + // TODO better place? + if (rem < 0 && !restBuzzed) { + Bangle.buzz(200) + .then(() => delay(400)) + .then(() => Bangle.buzz(200)) + .then(() => delay(400)) + .then(() => Bangle.buzz(200)); + restBuzzed = true; + } + return rem; +} + +function drawMainFrame() { + var d = new Date(); + // update time + mainLayout.Time.label = Locale.time(d,1); + // update timer + mainLayout.Timer.label = formatTime(getCurrentTimer()); + let restTime = getRestTime(); + mainLayout.RestTime.label = formatTime(restTime, true); + mainLayout.RestTime.col = restTime < 0 ? '#f00' : (restTime > 0 ? '#0f0' : '#fff'); + mainLayout.render(); + // schedule a draw for the next minute + if (timeoutDraw != -1) clearTimeout(timeoutDraw); + timeoutDraw = setTimeout(function() { + timeoutDraw = -1; + drawMainFrame(); + }, 1000 - (Date.now() % 1000)); +} + +function drawMain() { + g.clear(); + mainLayout.forgetLazyState(); + drawMainFrame(); + // mainLayout.render(); + // E.showMessage('Presentor'); +} + +function doPPart(r) { + pparti += r; + if (pparti < 0) { + pparti = -1; + mainLayout.Subject.label = 'PAUSED'; + mainLayout.Notes.label = 'Swipe up to start again.'; + return; + } + if (!settings.pparts || pparti >= settings.pparts.length) { + pparti = settings.pparts.length; + mainLayout.Subject.label = 'FINISHED'; + mainLayout.Notes.label = 'Good Job!'; + return; + } + let ppart = settings.pparts[pparti]; + mainLayout.Subject.label = ppart.subject; + mainLayout.Notes.label = ppart.notes; + ptimers[pparti].tracked = getTime(); + // We haven't buzzed if there was time left. + ppartBuzzed = ptimers[pparti].left <= 0; + // Always reset buzzstate for the rest timer. + restBuzzed = getRestTime() < 0; + drawMainFrame(); +} + +NRF.setServices(undefined, { hid : SpecialReport }); +// TODO: figure out how to detect HID. +NRF.on('HID', function() { + HIDenabled = true; +}); + +function moveMouse(x,y,b,wheel,hwheel,callback) { + if (!HIDenabled) return; + if (!b) b = 0; + if (!wheel) wheel = 0; + if (!hwheel) hwheel = 0; + NRF.sendHIDReport([1,b,x,y,wheel,hwheel,0,0], function() { + if (callback) callback(); + }); +} + +// function getSign(x) { +// return ((x > 0) - (x < 0)) || +x; +// } + +function scroll(wheel,hwheel,callback) { + moveMouse(0,0,0,wheel,hwheel,callback); +} + +// Single click a certain button (immidiatly release). +function clickMouse(b, callback) { + if (!HIDenabled) return; + NRF.sendHIDReport([1,b,0,0,0,0,0,0], function() { + NRF.sendHIDReport([1,0,0,0,0,0,0,0], function() { + if (callback) callback(); + }); + }); +} + +function pressKey(keyCode, modifiers, callback) { + if (!HIDenabled) return; + if (!modifiers) modifiers = 0; + NRF.sendHIDReport([2, modifiers,0,keyCode,0,0,0,0], function() { + NRF.sendHIDReport([2,0,0,0,0,0,0,0], function() { + if (callback) callback(); + }); + }); +} + +function handleAcc(acc) { + let rRoll = acc.y * -50; + let rPitch = acc.x * -100; + if (mCal > 10) { + //console.log("x: " + (rRoll - homeRoll) + " y:" + (rPitch - homePitch)); + moveMouse(acc.y * -50 - homeRoll, acc.x * -100 - homePitch); + } else { + //console.log("homeroll: " +homeRoll +"homepitch: " + homePitch); + homeRoll = rRoll * 0.7 + homeRoll * 0.3; + homePitch = rPitch * 0.7 + homePitch * 0.3; + mCal = mCal + 1; + } +} +Bangle.on('lock', function(on) { + if (on && holding) { + Bangle.setLocked(false); + Bangle.setLCDPower(1); + } +}); + +function startHolding() { + pressKey(kb.KEY.F10); + holding = true; + Bangle.buzz(); + E.showMessage('Holding'); + Bangle.on('accel', handleAcc); + Bangle.setLCDPower(1); +} +function stopHolding() { + clearTimeout(timeoutId); + if (holding) { + pressKey(kb.KEY.F10); + homePitch = 0; + homeRoll = 0; + holding = false; + mCal = 0; + Bangle.removeListener('accel', handleAcc); + Bangle.buzz(); + drawMain(); + } else { + timeoutId = setTimeout(drawMain, 1000); + } + clearTimeout(timeoutHolding); + timeoutHolding = -1; +} + +Bangle.on('drag', function(e) { + if (cttl == 0) { cttl = getTime(); } + if (trackPadMode) { + if (lastx + lasty == 0) { + lastx = e.x; + lasty = e.y; + mttl = getTime(); + } + if (clearToSend) { + clearToSend = false; + let difX = e.x - lastx, difY = e.y - lasty; + let dT = getTime() - mttl; + let vX = difX / dT, vY = difY / dT; + //let qX = getSign(difX) * Math.pow(Math.abs(difX), 1.2); + //let qY = getSign(difY) * Math.pow(Math.abs(difY), 1.2); + let qX = difX + 0.02 * vX, qY = difY + 0.02 * vY; + moveMouse(qX, qY, 0, 0, 0, function() { + setTimeout(function() {clearToSend = true;}, 50); + }); + lastx = e.x; + lasty = e.y; + mttl = getTime(); + console.log("Dx: " + (qX) + " Dy: " + (qY)); + } + if (!e.b) { + // short press + if (getTime() - cttl < 0.2) { + clickMouse(MouseButton.LEFT); + console.log("click left"); + } + // longer press in center + else if (getTime() - cttl < 0.6 && e.x > g.getWidth()/4 && e.x < 3 * g.getWidth()/4 && e.y > g.getHeight() / 4 && e.y < 3 * g.getHeight() / 4) { + clickMouse(MouseButton.RIGHT); + console.log("click right"); + } + cttl = 0; + lastx = 0; + lasty = 0; + } + } else { + if(!e.b){ + Bangle.buzz(100); + if(lasty > 40){ + doPPart(-1); + // E.showMessage('down'); + } else if(lasty < -40){ + doPPart(1); + // E.showMessage('up'); + } else if(lastx > 40){ + // E.showMessage('right'); + //kb.tap(kb.KEY.RIGHT, 0); + scroll(-1); + } else if(lastx < -40){ + // E.showMessage('left'); + //kb.tap(kb.KEY.LEFT, 0); + scroll(1); + } else if(lastx==0 && lasty==0 && holding == false){ + // E.showMessage('press'); + clickMouse(MouseButton.LEFT); + } + stopHolding(); + lastx = 0; + lasty = 0; + } else{ + lastx = lastx + e.dx; + lasty = lasty + e.dy; + if (timeoutHolding == -1) { + timeoutHolding = setTimeout(startHolding, 500); + } + } + } +}); + + +function onBtn() { + if (trackPadMode) { + trackPadMode = false; + stopHolding(); + drawMain(); + } else { + clearToSend = true; + trackPadMode = true; + E.showMessage('Mouse'); + } + Bangle.buzz(); +} +setWatch(onBtn, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat: true}); + +loadSettings(); +drawMain(); \ No newline at end of file diff --git a/apps/presentor/app.png b/apps/presentor/app.png new file mode 100644 index 000000000..d29cf7be6 Binary files /dev/null and b/apps/presentor/app.png differ diff --git a/apps/presentor/interface.html b/apps/presentor/interface.html new file mode 100644 index 000000000..e772d24da --- /dev/null +++ b/apps/presentor/interface.html @@ -0,0 +1,285 @@ + + + + + + + + + + + + +
+

Presentor

+
+
+
+
+
+
#
+
Subject
+
Time
+
Notes
+
+
Loading...
+
+
+
+
+ + +
+ + + + + + diff --git a/apps/presentor/settings.json b/apps/presentor/settings.json new file mode 100644 index 000000000..398bf1332 --- /dev/null +++ b/apps/presentor/settings.json @@ -0,0 +1 @@ +{"pparts":[{"subject":"#1","minutes":10,"seconds":0,"notes":"This is a note."},{"subject":"#2","minutes":2,"seconds":50,"notes":"Change in the app!"}],"sversion":2.2} \ No newline at end of file diff --git a/apps/puzzle15/ChangeLog b/apps/puzzle15/ChangeLog new file mode 100644 index 000000000..0950b7ae0 --- /dev/null +++ b/apps/puzzle15/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version, UI mechanics ready, no real game play so far +0.02: Lots of enhancements, menu system not yet functional, but packaging should be now... +0.03: Menu logic now generally functioning, splash screen added. The first really playable version! +0.04: Settings dialog, about screen +0.05: Central game end function diff --git a/apps/puzzle15/README.md b/apps/puzzle15/README.md new file mode 100644 index 000000000..16c0c4593 --- /dev/null +++ b/apps/puzzle15/README.md @@ -0,0 +1,57 @@ +# Puzzle15 - A 15-puzzle for the Bangle.js 2 + +This is a Bangle.js 2 adoption of the famous 15 puzzle. + +## The game + +A board of _n_ by _n_ fields is filled with _n^2-1_ numbered stones. So, one field, the "gap", is left free. + +Bring them in the correct order so that the gap is finally at the bottom right of the playing field. +The less moves you need, the better you are. + +If _n_ is 4, the number of stones is _16-1=15_. Hence the name of the game. + +## How to play + +If you start the game, it shows a splash screen and then generates a shuffled 4x4 board with a 15 puzzle. +Move the stones with drag gestures on the screen. +If you want to move the stone below the gap upward, drag from the bottom of the screen upward. +The drag gestures can be performed anywhere on the screen, there is no need to start or end them on the stone to be moved. + +If you managed to order the stones correctly, a success message appears. +You can continue with another game, go to the game's main menu, or quit the game entirely. + +There is a grey menu button right of the board containing the well-known three-bar menu symbol ("Hamburger menu"). +It opens the game's main menu directly from within the game. + +## The main menu + +Puzzle15 has a main menu which can be reached from the in-game menu button or the end-of-game message window. +It features the following options: + +* **Continue** - Continue the currently running game. _This option is only shown if the main menu is opened during an open game._ +* **Start 3x3**, **Start 4x4**, **Start 5x5** - Start a new game on a board with the respective dimension. Any currently open game is dropped. +* **About** Show a small "About" info box. +* **Exit** Exit Puzzle15 and return to the default watch face. + +## Game settings + +The game has some global settings which can be accessed on the usual way through the Bangle.js' app settings user interface. +Currently it has the following options: + +* **Splash** - Define whether the game should open with a splash screen. **long** shows the splash screen for five seconds, **short** shows it for two seconds. **off** starts the app _without_ a splash screen, it directly comes up with whatever the "Start with" option says. +* **Start with** - What should happen after the splash screen (or, if it is disabled, directly at app start): **3x3**, **4x4** and **5x5** start the game with a board of the respective dimension, **menu** shows the main menu which allows to select the board size. + +## Implementation notes + +The game engine always generates puzzles which can be solved. + +Solvability is detected by counting inversions, +i.e. pairs of stones where the stone at the earlier field (row-wise, left to right, top to bottom) has a number _greater than_ the stone on the later field, with all pairs of stones compared. +The algorithm is described at https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ . + +## The splash screen + +The Splash screen shows a part of the illustration "The 14-15-puzzle in puzzleland" from Sam Loyd. Other than Puzzle15, it depicts a 15 puzzle with the stones "14" and "15" swapped. This puzzle is indeed *not* solvable. + +Have fun! diff --git a/apps/puzzle15/introscreen.png b/apps/puzzle15/introscreen.png new file mode 100644 index 000000000..766cd61fa Binary files /dev/null and b/apps/puzzle15/introscreen.png differ diff --git a/apps/puzzle15/puzzle15.app-icon.js b/apps/puzzle15/puzzle15.app-icon.js new file mode 100644 index 000000000..04fb4a665 --- /dev/null +++ b/apps/puzzle15/puzzle15.app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgn/AC3+7oAD7e7AAW8BQndBQe79/9DomgHocH74KD/RJE34Xax4XDtvoC4fJ54XDluAC4f2z4XDzm/C4ett4XD34OBF4e/I4m+C4f8r4XChHuC5U98oXEF4cP7/AC5O9mYXC/2/F4cGtwvE/SsBC4Ws7gvD7YCBL4ULO4i/u1QAD7QED1e6AoetCAnf/YeE1wpD/lgBQcKIAgXG14LD/twC5kL3Z+BC4P+LgIXBg272wXD7wXEh7eCC4PWzIXChHtOoIXB/WX54XDh3KmAXC1oLBI4UD+AXC+/rdIIvD5wvD3O4C4cJ4AXC/dUI4kJhgMBC4Ov+AXDh9QC4X2/gvEhvvoAXC81dC4duR4f8wSncC6v8u4AD3ndAAXcy4KDtYKD7vf/oGE2wRDvPNBQfLFAnP/o2EVIIACg7yBAATZBAAe/C7P9g4XCx+wn/6C4Op//AC4MK+cI/+QC4X2/fPC4PM2HKh8H7vpewIXBhvThV5+AXC+/5C4UL2HHC4Pf/P/AIJHB6cAj2wC4X+3AXPhADBF4fX94XB1va1vOC4PXAIX6hfrxvb0CPD7p3C1e6hW2C4LOBAIIXB3eJ3YXEX78GM4IAC9QXG1QAD7QEDJYIFD14oE//7DwgME/twBQcPC70G6EG5dQ1/8VYPtC4ObgfM5IXHr/whvO4Gvy6LBtX9vfugnr3AXHkXggGOC4P97/43X9ukOgnv6BfIC4Oe2AXC6+nI4MOgfI9QXJhssF4f91AXCgnA9IXHr3u1HusGv3Ob//s/t693l3xHJX9v+3YAD7oAE5YKD34XFAC4=")) diff --git a/apps/puzzle15/puzzle15.app.js b/apps/puzzle15/puzzle15.app.js new file mode 100644 index 000000000..eec21933c --- /dev/null +++ b/apps/puzzle15/puzzle15.app.js @@ -0,0 +1,771 @@ +// A 15-puzzle game for the Bangle.js 2 clock +// (C) Dirk Hillbrecht 2022 +// Released unter the terms of the MIT license + +// The intro screen as large base-64-encoded binary data +const introscreen = E.toArrayBuffer(atob("sLABwAAAAA5QAAACAAAAHAAMEDgA/F/nvoAAAAA+3AAAAAAAAB4AQBIkAPwv//4AAAAAP/wQAAAAAAAAAA3wBAD8P//+AAAAAD7tGQAAAAAAAIADAAMF2C///wAAACA4oRmAHx/wQAAAAAABOfgP//8AAAAAAGEYgA+/AMAAQAAAAAP8B///AAAGAABxMKgD/AAABGAAAAAD/Af//wAAYQAr6TOoAfwAIAWTgAAAA/4D//8SAeCA9W0/mAH8ABAEBAAAAAP+A///YAMRh/5of/ADwMAYOAAAAAAD/gH/f4AMYd/3dv+AAwUABSAAAAAAA/8A/XIAMDrPn///gAMQAADAABgAAAH3APBAAMAdX3/fvjACEAQAAA/mAAAB/wcAAAcAz1/+3/34AgAAgADuF+AAAP8YAA8MEJ+Bt29/GAIAACAADAgwAAD/4AACMAyv/+f/6HwGAABAAE2IoAAAfgAAB8ADf/////mOADh5gABNCCAAAHwB/x4AAX//5/L9D4AAAAYBCQjgAA3QAgBgAAE//7P//jzAAADPAGkAoADggABwAwAC3//F9/5n4AAB/wFpACAHAADAAAEAA///c/v/4jABwf+A8Xwh+AD0BAAAAIfvPm/G/+IYA8P3wPh+P4AHAAAB+ACH//+Hy/9iCAfD/8CYX/QAL9AAAAcAme//z/PrR6gB4P/guPgAAODAGBwAAI8+7gf0P6/oAeD/4bjh//wxA4AAAw2f//8f+f+PiAHwf/+53AA/+AAAAA4Gv/P/9/A/nwgB8D6AuWD8EAAAYAD4A+7/8X/ADr8IAfAwgZlO/AAAHwAH4L2/////mDz9CAD6MB65YAAAAf+A/4D23/n/v4j4fcgA/wDguWAAAB8AD7ngg1////+EAD/Pg8AGDZlmgAP8e4zCX4aP///9AZA/D/4AAAGYf//////4ZwG2j/8H/gwLPggAAAAfWH/////8AQRCpr/+E/9IBT4IAA/hj1h///wFgIqBBO9//33/m6i8CAB8A//YH/gDMfGO/WH/0/5//4gEfAgP4Af+AB/gAHFEBoaTH+/+w/8fQDgI+aP//iifYABRCJ9a9w///+P+H/M4Gw8//34o3yBKkMUW0e3Pt//i6AHxeDg///gouP/poNPAnjzp/3v+2PgDfngz/v3rvjhO6bTTqDWsY//v/nz4LD/4N6bff3YYHu7UU6yZcm////5+3Kgf/CtMP/b+GJ7hr9O41tNB///+XNhAb/hv3kv/7hg7MQn3o+4N5Y9//gx8ME+59P47//gYK6Pnbvc5bu6///48MFl+d93/yv++GAVi+X51IyKqA//+eC4AzP0P3y4//hgb4/83QXYfjAP//1MYbwn9D/77/14YCnf/gIHLX96D//5WGA8Z7Bnf7b/8GgKG94GXuU9y4P/+DwwCeeafvfm93hubX8dvF3E6Ar/N/54OATj2Du/Pv/qY10/zTBB1h+CAff/8AwH4dmb6t7/2mN+IuRyI4CBngD///AMx6Xsnve+09pkNvgsQwAAWAHwo/+hBAeh5Czt5/HcZxSQ6AceCAAiAK/9ggQHBNBuZ5eQvGOducgFgA1BxADx5MIEX/Jo2p8YR/xjt4wINYAfYAf/K4TDAV8Ye48DLBDXY46ADQmBIe+HoSOMgwj5hFqH+CAB78O6AAX4gACAAgMPjYEK4f//7p/2f/NjOuAH8MAACACb5YmCCsDSPnbADz7w49+lI8BAAAf8aICRgAjACB/O4Ag1VcMuD/eAQD/oHISSsYEMzhg/qtABft//7LACAG/gwAMg2MGBCJ4cPD25gBN95Ovn5UA4DgHRncFAxQijj//5KA/jvcL6gAJBEAAf7fBB4MUI+fv/szeEAaqjO/gObYAAAAz57GDpAO5x19BIA/9uYw7gEyMQHwAD/mAg8KBjwv9Giv/vrvKQHBNAEsAUAP9kuP+gZAff+dcEX/ZljjebYDAAAAnf8Pj/YKsH/2E1HEAUQGTB21gwAAAGS5NYv0C6jP1mJY5MEqV0f9fcAfPoHM7qWL/EDg39PGcex7iu+4Bz7B/AD/63qRg/kwML/+DJR9PvOUTAK64RwYAcjek5f/4Yv/aT20L7v0m6gCNvNiYgDI15iE//jH/+2zij+9HsMY8qrA98f+CL9SiP/8//8/gkJ/Ht99M/OogD+MAOifQJj//nH//Y5KPw7//FLBZACAH+Arn8GV//////zMVT36V5sdAecAgCkHqP5H3///4v/+cbUdvIc7wADDAId4AegXuX///////iaVfkvNu/+wYA5Aj/AJl6j////v3/uyOW91wcvqImAZIcQeCBeg/////f/v8MqV/7gZ2cDwICMYAcgXpf////v+/JmJv5/7+B9A+HAiKYA5Bcb//////nxMOI8f92d+APDgZF34SbeU7///72/naafn22WAD/DwwCigR+kHlO///9/9fmWFt/+GMiT8uMARV/F5B/XP///5BTmsGTV/3EADfxlAMsfqjZ/XT///X/l/9FJs//CADDuQgEUnw08vnW/3/38tvx7G3v/BHeOfm4DNT4zpJ7uj///5NI//Cef/gvBlf5uBig8Og/aN////YVe9iWuvnwAD7g/JoxYeF0k/tf///s0U7c8ozbz4BwEHztJoPyrIdrZ//////qz5DpZvcDgBw/7MVHxc+mc+/8P/jL//+PV358GAACPtyKD4SYt/PZ+B////8/+dk/gDgB+G5tNA8fvir/HfNfAG4f/+9q8AAAAABgayg+E0Ot3/flDwADAP/4W/AHAAAAUfJoHizE7/xzww5AMPAAAXoQGADAAH9M/wZF2Sb/b4B3KAQPiLSOMAAAOAA4if/wmz3m+/eAOBwAA/aljmEAAAwAALNz/7Z8v/7/BIgbLwA3jBjNAIAHwAGnD39kTKe//3gBxeAAH0oreAP/+Pok7YH9HgCv7/+gABPAAADcxoADff4h3olwHy/Cvet+YAeAACgO/b+AR//g7geWjwP+/Kf/+IDgAeQXeEt3A+53AH4dKMDH//wnu/BIwDAYIbi/VQa/2bol+knwPg99r7/+MIAAAgDQ33L2P+/rLeyTfiaA/Cf/9kgAADYAIN+7f/ef+dj5pzj54Bynf8kiAABM8Dh+/r//+/anc0/A+7wH/9+On8AAHhgsf/1+CAD31mbA4bcDgI//DyQAAwAxrP/f4AgAKfzNlMNlkGib7gABdAYYBrD/3wLwAD//m3DGSPwYn/wA8BZgBg5S//i5AAAD/xR5x5uOHpP4AAQewAAPVn/nwAAAAP4o89mvDjSXsYAfH5AIB9///4Pwfx7+0debX4R09+pgeQHA8Bn/78f/nwAH/5DfLvzgfp3gAAkI0AJxL//+fjAAAP8k/ly4YOgF4no4Z3AAYH/xAL/gAAB7/DyJcGPz9cSA2XgaAPB+2AH/4AAAZ/+BonFj8B3AYAfAtiLg//A5QAQAAOQ/5WZg5qSVwYAAMAxK4P//jcAZyAGbD/sIQctMlYsAD9wclSf//gEAAB/B84H9UEPdm5cgAAAwOKMP//8eAGs/9i/gfaBnmziXYIABwXsRN7f//Cb+D/7fmB1wfzoqvktAAATGQD/3/vwdgAG918YP3h5+Q/0fmAAJMRZb///ZEQAAcT5xV/+A7MCdgIMAABBqt///+WEAADp+MNo/4XkCj4AMAAbILUf///vEhAgkvDH6D/vzA/mAgQIAPBrDpf/7D7+eSThxt4P/pgbnhkYCAQYDs//v/3/7P5Nw8+3g/+gPvgDhDABhgZP3//v3/j828efJsB/YDP8BjhAPM8Hf/9f/9/7eaDPPkwYH/g9BxIAQBAhA9Pa3/v/+3Fi/yz8Fgv+ASUAQYAJuOKH/9///+7yrj7p/MH//4/5CM8AwEw4S+7df/+uxx59ejvh2D//0YA+sAADhDq+d9v+b++M+zR3IfYf9ibgASEcAgCX1br+8i+fz+Nk7memA/J5IADiYYC2XwbbGK0/f/OHydxpz4DxiJgAAICAAY8e/3vd/mP8C5G4y93gP8hOAHhOeAA86rOvEfzQ/hMjudE4GAwlLwAgUcI4wtr77vf9uH+rR3OZI5YCMRvgAQAABDhR73nv8jwf7o5nnmfBx/yV+AIAAP+IY3/lX/ZvB90sxwzTkHiMZn4AAGQAxhXtX33E+8HoPI+ZhzwSAgXfwARwQPlaSv4dyfhg9w2cMw4+PjCYr+AEewAkhdlfvRP5qH/B+/YeODnIiHe4OAYAHCeld7/m88f/4ONkOHh2hjM//gAAAA4AK/n8TOLFp/gIzHjwsyYBXf8AAAACBA153p3ByYH8GZRw4ZMYwpM3wAwAAwA7E3k7j5Ng/zEg4cO0xOKyz3AAgABALQf6M44++D/sYcOD7iMRZHP8AAAAAGOFpkcObPwf7kOHhO3SkGQYbwANgBCDfUyOjun/B+yPjwzPzHJYlf2AOMAAQEjLHd1T+cHxDw4dj+YvjKX/wOBgBwAr/4udN/Zg/4McJxv9xMMLD/GAcACk+o/hOur3Cv/ifD4ZfsMizgO/BHgAEQfC8/bZz8zruHh+KR//FIYA/n2fAAif4ZjrmcnLR/w4jEMH8kQkAAwAJmJEB/j8GSOcFKH/AZjCAz6pCAAAAAHwIho8bjZnNjegf4GxgwD/GUgAGAAB+BIqDh/k3gZKeD/jYRMAB4ZwABwAAIg55/8P3ZwMrM4P/KISAAHimAAOAACIHD//59s8nJDzB/zFMAAAMLwAAi4B7Aen/8P3+7lJpcH9gHAAABzgAACNs+gBs/fx5Pd3ExxgfxngAAAOcCAAMBf4AEB/+P42diY8MD8w4AAAB/wfAOAB4AD4H3w/DcFMOQwP/DAAAAH+B+4AAIAAD8v/X4femHBzl/4AAAAAHwAQAACAAAJ39x/Ct5Dh8Lf/gAAAAAP4AAEh4AADe+/D8HUgxvGf/8AAAAAAOwABf4AAAMT/8fxOQd3i9//8AAAAAAzAAAYAAAD5d/P+5IMxxH//8wAAAAAHYBgCCMAAOPP4fxkHw4Vv/3DAAAAAA5AGAhRwAA/D/D/bD4cMx//wOAAAAADAAf8BAAABwH4f9xzvE/f/4A4AAAAAYAB/gpgAAaf/v+QcPim3//AB4AAAABwAP8AAAABg/8fYeDxmT//wABwAAAAHwD6AADgAHP/h/Dg+1Zf/8=")); + +// *** Global constants from which several other settings are derived + +// 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; + +// Number of steps in stone move animation +const animationSteps = 6; + +// Milliseconds to wait between move animation steps +const animationWaitMillis = 30; + +// Total width of the playing field (full screen width) +const fieldw = g.getWidth(); + +// Total height of the playing field (screen height minus widget zones) +const fieldh = g.getHeight() - 48; + + +// *** Global game characteristics + +// Size of the playing field +var stonesPerLine; + +// Size of one field +var stonesize; + +// Actual left start of the playing field (so that it is centered) +var leftstart; + +// Actual top start of the playing field (so that it is centered) +var topstart; + +// Number of stones on the board (needed at several occasions) +var stonesPerBoard; + +// Set the stones per line globally and all derived values, too +function setStonesPreLine(bPL) { + stonesPerLine = bPL; + stonesize = Math.floor(Math.min(fieldw / (stonesPerLine + 1), fieldh / stonesPerLine)) - 2; + leftstart = (fieldw - ((stonesPerLine + 1) * stonesize + 8)) / 2; + topstart = 24 + ((fieldh - (stonesPerLine * stonesize + 6)) / 2); + stonesPerBoard = (stonesPerLine * stonesPerLine); +} + + +// *** Global app settings + +var SETTINGSFILE = "puzzle15.json"; + +// variables defined from settings +var splashMode; +var startWith; + +/* For development purposes +require('Storage').writeJSON(SETTINGSFILE, { + splashMode: "off", + startWith: "5x5", +}); +/* */ + +/* OR (also for development purposes) +require('Storage').erase(SETTINGSFILE); +/* */ + +// Helper method for loading the settings +function def(value, def) { + return (value !== undefined ? value : def); +} + +// Load settings +function loadSettings() { + var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; + splashMode = def(settings.splashMode, "long"); + startWith = def(settings.startWith, "4x4"); +} + + +// *** Low level helper classes + +// One node of a first-in-first-out storage +class FifoNode { + constructor(payload) { + this.payload = payload; + this.next = null; + } +} + +// Simple first-in-first-out (fifo) storage +// Needed to keep the stone movements in order +class Fifo { + + // Initialize an empty Fifo + constructor() { + this.first = null; + this.last = null; + } + + // Add an element to the end of the internal fifo queue + add(payload) { + if (this.last === null) { // queue is empty + this.first = new FifoNode(payload); + this.last = this.first; + } else { + let newlast = new FifoNode(payload); + this.last.next = newlast; + this.last = newlast; + } + } + + // Returns the first element in the queue, null if it is empty + remove() { + if (this.first === null) + return null; + let oldfirst = this.first; + this.first = this.first.next; + if (this.first === null) + this.last = null; + return oldfirst.payload; + } + + // Returns if the fifo is empty, i.e. it does not hold any elements + isEmpty() { + return (this.first === null); + } + +} + +// Helper class to keep track of tasks +// Executes tasks given by addTask. +// Tasks must call Worker.endTask() when they are finished, for this they get the worker passed as parameter. +// If a task is given with addTask() while another task is still running, +// it is queued and executed once the currently running task and all +// previously scheduled tasks have finished. +// Tasks must be functions with the Worker as first and only parameter. +class Worker { + + // Create an empty worker + constructor() { + this.tasks = new Fifo(); + this.busy = false; + } + + // Add a task to the worker + addTask(task) { + if (this.busy) // other task is running: Queue this task + this.tasks.add(task); + else { // No other task is running: Execute directly + this.busy = true; + task(this); + } + } + + // Called by the task once it finished + endTask() { + if (this.tasks.isEmpty()) // No more tasks queued: Become idle + this.busy = false; + else // Call the next task immediately + this.tasks.remove()(this); + } + +} + +// 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)); + } +} + +// *** Mid-level game mechanics + +// Representation of a position where a stone is set. +// Stones can be moved from field to field. +// The playing field consists of a fixed set of fields forming a square. +// During an animation, a series of interim field instances is generated +// which represents the locations of a stone during the animation. +class Field { + + // Generate a field with a left and a top coordinate. + // Note that these coordinates are "cooked", i.e. they contain all offsets + // needed place the elements globally correct on the screen + constructor(left, top) { + this.left = left; + this.top = top; + this.centerx = (left + stonesize / 2) + 1; + this.centery = (top + stonesize / 2) + 2; + } + + // Returns whether this field contains the given coordinate + contains(x, y) { + return (this.left < x && this.left + stonesize > x && + this.top < y && this.top + stonesize > y); + } + + // Generate a field for the given playing field index. + // Playing field indexes start at top left with "0" + // and go from left to right line by line from top to bottom. + static forIndex(index) { + return new Field(leftstart + (index % stonesPerLine) * (stonesize + 2), + topstart + (Math.floor(index / stonesPerLine)) * (stonesize + 2)); + + } + // Special field for the result "stone" + static forResult() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart + ((stonesPerLine - 1) * (stonesize + 2))); + } + + // Special field for the menu + static forMenu() { + return new Field(leftstart + (stonesPerLine * (stonesize + 2)), + topstart); + } + +} + +// Representation of a moveable stone of the game. +// Stones are moved from field to field to solve the puzzle +// Stones are numbered from 0 to the maximum number ot stones. +// Stone "0" represents the gap on the playing field. +// The main knowledge of a Stone instance is how to draw itself. +class Stone { + + // Create stone with the given number + // The constructor creates the "draw()" function which is used to draw the stone + constructor(number, targetindex) { + this.number = number; + this.targetindex = targetindex; + // gap: Does not draw anything + if (number === 0) + this.draw = function(field) {}; + else if ((number + (stonesPerLine % 2 == 0 ? (Math.floor((number - 1) / stonesPerLine)) : 0)) % 2 == 0) { + // Black stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(1, 1, 1).drawString(number, field.centerx, field.centery); + }; + } else { + // White stone + this.draw = function(field) { + g.setFont("Vector", (stonesPerLine === 5 ? 16 : 20)).setFontAlign(0, 0).setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.drawString(number, field.centerx, field.centery); + }; + } + } + + // Returns whether this stone is on its target index + isOnTarget(index) { + return index === this.targetindex; + } +} + +// Helper class which knows how to clear the rectangle opened up by the two given fields +class Clearer { + + // Create a clearer for the area between the two given fields + constructor(startfield, endfield) { + this.minleft = Math.min(startfield.left, endfield.left); + this.mintop = Math.min(startfield.top, endfield.top); + this.maxleft = Math.max(startfield.left, endfield.left); + this.maxtop = Math.max(startfield.top, endfield.top); + } + + // Clear the area defined by this clearer + clearArea() { + g.setColor(1, 1, 1); + g.fillRect(this.minleft, this.mintop, + this.maxleft + stonesize, this.maxtop + stonesize); + } + +} + +// Helper class which moves a stone between two fields +class Mover extends Clearer { + + // Create a mover which moves the given stone from startfield to endfield + // and animate the move in the given number of steps + constructor(stone, startfield, endfield, steps) { + super(startfield, endfield); + this.stone = stone; + this.startfield = startfield; + this.endfield = endfield; + this.steps = steps; + } + + // Create the coordinate between start and end for the given step + // Computation uses sinus for a smooth movement + stepCoo(start, end, step) { + return start + ((end - start) * ((1 + Math.sin((step / this.steps) * Math.PI - (Math.PI / 2))) / 2)); + } + + // Compute the interim field for the stone to place during the animation + stepField(step) { + return new Field( + (this.minleft === this.maxleft ? this.minleft : + this.stepCoo(this.startfield.left, this.endfield.left, step)), + (this.mintop === this.maxtop ? this.mintop : + this.stepCoo(this.startfield.top, this.endfield.top, step))); + } + + // Perform one animation step + animateStep(step, worker) { + this.clearArea(); + this.stone.draw(this.stepField(step)); + if (step < this.steps) // still steps left: Issue next step + setTimeout(function(t) { + t.animateStep(step + 1, worker); + }, animationWaitMillis, this); + else // all steps done: Inform the worker + worker.endTask(); + } + + // Start the animation, this method is called by the worker + animate(worker) { + this.animateStep(1, worker); + } + +} + +// Representation of the playing field +// Knows to draw the field and to move a stone into a gap +class Board { + + // Generates the actual playing field with all fields and stones + constructor() { + this.fields = []; + this.resultField = Field.forResult(); + this.menuField = Field.forMenu(); + for (i = 0; i < stonesPerBoard; i++) + this.fields[i] = Field.forIndex(i); + this.setShuffled(); + //this.setAlmostSolved(); // to test the game end + } + + /* Set the board into the "solved" position. Useful for showcasing and development + setSolved() { + this.stones = []; + for (i = 0; i < stonesPerBoard; i++) + this.stones[i] = new Stone((i + 1) % stonesPerBoard, i); + this.moveCount = 0; + } + /* */ + + /* Initialize an almost solved playing field. Useful for tests and development + setAlmostSolved() { + this.setSolved(); + b = this.stones[this.stones.length - 1]; + this.stones[this.stones.length - 1] = this.stones[this.stones.length - 2]; + this.stones[this.stones.length - 2] = b; + } + /* */ + + // Initialize a shuffled field. The fields are always solvable. + setShuffled() { + let nrs = []; // numbers of the stones + for (i = 0; i < stonesPerBoard; i++) + nrs[i] = i; + this.stones = []; + let count = stonesPerBoard; + for (i = 0; i < stonesPerBoard; i++) { + // Take a random number of the (remaining) numbers + let curridx = Math.floor(Math.random() * count); + let currnr = nrs[curridx]; + // Initialize the next stone with that random number + this.stones[i] = new Stone(currnr, (currnr + (stonesPerBoard - 1)) % stonesPerBoard); + // Remove the number just taken from the list of numbers + for (j = curridx + 1; j < count; j++) + nrs[j - 1] = nrs[j]; + count -= 1; + } + // not solvable: Swap the first and second stone which are not the gap. + // This will always result in a solvable board. + if (!this.isSolvable()) { + let a = (this.stones[0].number === 0 ? 2 : 0); + let b = (this.stones[1].number === 0 ? 2 : 1); + let bx = this.stones[a]; + this.stones[a] = this.stones[b]; + this.stones[b] = bx; + } + this.moveCount = 0; + } + + // Draws the complete playing field + draw() { + new Clearer(this.fields[0], this.fields[this.fields.length - 1]).clearArea(); + for (i = 0; i < this.fields.length; i++) + this.stones[i].draw(this.fields[i]); + this.drawResult(null); + this.drawMenu(); + } + + // returns the index of the field left of the field with the given index, + // -1 if there is none (index indicates already a leftmost field on the board) + leftOf(index) { + return (index % stonesPerLine === 0 ? -1 : index - 1); + } + + // returns the index of the field right of the field with the given index, + // -1 if there is none (index indicates already a rightmost field on the board) + rightOf(index) { + return (index % stonesPerLine === (stonesPerLine - 1) ? -1 : index + 1); + } + + // returns the index of the field top of the field with the given index, + // -1 if there is none (index indicates already a topmost field on the board) + topOf(index) { + return (index >= stonesPerLine ? index - stonesPerLine : -1); + } + + // returns the index of the field bottom of the field with the given index, + // -1 if there is none (index indicates already a bottommost field on the board) + bottomOf(index) { + return (index < (stonesPerLine - 1) * stonesPerLine ? index + stonesPerLine : -1); + } + + // Return the index of the gap in the field, -1 if there is none (should never happel) + indexOf0() { + for (i = 0; i < this.stones.length; i++) + if (this.stones[i].number === 0) + return i; + return -1; + } + + // Returns the row in which the gap is, 0 is upmost + rowOf0() { + let idx = this.indexOf0(); + if (idx < 0) + return -1; + return Math.floor(idx / stonesPerLine); + } + + // Searches the gap on the field and then moves one of the adjacent stones into it. + // The stone is selected by the given startfunc which returns the index + // of the selected adjacent field. + // Startfunc is one of (left|right|top|bottom)Of. + moveTo0(startfunc, worker) { + let endidx = this.indexOf0(); // Target field (the gap) + if (endidx === -1) { + worker.endTask(); + return; + } + let startidx = startfunc(endidx); // Start field (relative to the gap) + if (startidx === -1) { + worker.endTask(); + return; + } + // Replace in the internal representation + let moved = this.stones[startidx]; + this.stones[startidx] = this.stones[endidx]; + this.stones[endidx] = moved; + this.moveCount += 1; + // Move on screen using an animation effect. + new Mover(moved, this.fields[startidx], this.fields[endidx], animationSteps).animate(worker); + } + + // Move the stone right from the gap into the gap + moveRight(worker) { + this.moveTo0(this.leftOf, worker); + } + + // Move the stone left from the gap into the gap + moveLeft(worker) { + this.moveTo0(this.rightOf, worker); + } + + // Move the stone above the gap into the gap + moveUp(worker) { + this.moveTo0(this.bottomOf, worker); + } + + // Move the stone below the gap into the gap + moveDown(worker) { + this.moveTo0(this.topOf, worker); + } + + // Check if the board is solved (all stones at the right position) + isSolved() { + for (i = 0; i < this.stones.length; i++) + if (!this.stones[i].isOnTarget(i)) + return false; + return true; + } + + // counts the inversions on the board + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + getInversionCount() { + let inversions = 0; + for (outer = 0; outer < stonesPerBoard - 1; outer++) { + let outernr = this.stones[outer].number; + if (outernr === 0) + continue; + for (inner = outer + 1; inner < stonesPerBoard; inner++) { + let innernr = this.stones[inner].number; + if (innernr > 0 && outernr > innernr) + inversions++; + } + } + return inversions; + } + + // return whether the puzzle is solvable + // see https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/ + isSolvable() { + let invs = this.getInversionCount(); + if (stonesPerLine % 2 !== 0) // odd number of rows/columns + return (invs % 2 === 0); + else { + return ((invs + this.rowOf0()) % 2 !== 0); + } + } + + // draw the result field, pass null as argument if not called from worker + drawResult(worker) { + let field = this.resultField; + let solved = this.isSolved(); + if (solved) + g.setColor(0, 1, 0); + else + g.setColor(1, 0, 0); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setFont("Vector", 14).setFontAlign(0, 0).drawString(this.moveCount, field.centerx, field.centery); + if (worker !== null) + worker.endTask(); + if (solved) + setTimeout(() => { + gameEnd(this.moveCount); + }, 500); + } + + // draws the menu button + drawMenu() { + let field = this.menuField; + g.setColor(0.5, 0.5, 0.5); + g.fillRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + g.setColor(0, 0, 0); + g.drawRect(field.left, field.top, field.left + stonesize, field.top + stonesize); + let l = field.left + 8; + let r = field.left + stonesize - 8; + let t = field.top + 5; + for (i = 0; i < 3; i++) + g.fillRect(l, t + (i * 6), r, t + (i * 6) + 2); + } + +} + + +// *** Global helper methods + +// draw some text with some surrounding to increase contrast +// text is drawn at given (x,y) position with textcol. +// frame is drawn 2 pixels around (x,y) in each direction in framecol. +function framedText(text, x, y, textcol, framecol) { + g.setColor(framecol); + for (i = -2; i < 3; i++) + for (j = -2; j < 3; j++) { + if (i === 0 && j === 0) + continue; + g.drawString(text, x + i, y + j); + } + g.setColor(textcol).drawString(text, x, y); +} + +// Show the splash screen at program start, call afterSplash afterwards. +// If spash mode is "off", call afterSplash directly. +function showSplash(afterSplash) { + if (splashMode === "off") + afterSplash(); + else { + g.reset(); + g.drawImage(introscreen, 0, 0); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("15", g.getWidth() / 2, g.getHeight() / 2 - g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(() => { + g.setFont("Vector", 40).setFontAlign(0, 0); + framedText("Puzzle", g.getWidth() / 2, g.getHeight() / 2 + g.getFontHeight() * 0.66, "#f00", "#fff"); + setTimeout(afterSplash, (splashMode === "long" ? 2000 : 1000)); + }, (splashMode === "long" ? 1000 : 1)); + }, (splashMode === "long" ? 2000 : 1000)); + } +} + + +// *** Global flow control + +// Initialize the game with an explicit number of stones per line +function initGame(bpl) { + setStonesPreLine(bpl); + newGame(); +} + +// Start a new game with the same number of stones per line as before +function newGame() { + board = new Board(); + continueGame(); +} + +// Continue the currently running game +function continueGame() { + E.showMenu(); + board.draw(); + dragger.setEnabled(true); +} + +// Show message on game end, allows to restart new game +function gameEnd(moveCount) { + dragger.setEnabled(false); + E.showPrompt("You solved the\n" + stonesPerLine + "x" + stonesPerLine + " puzzle in\n" + moveCount + " move" + (moveCount === 1 ? "" : "s") + ".", { + title: "Puzzle solved", + buttons: { + "Again": newGame, + "Menu": () => showMenu(false), + "Exit": exitGame + } + }).then(v => { + E.showPrompt(); + setTimeout(v, 10); + }); +} + +// A tiny about screen +function showAbout(doContinue) { + E.showAlert("Author: Dirk Hillbrecht\nLicense: MIT", "Puzzle15").then(() => { + if (doContinue) + continueGame(); + else + showMenu(false); + }); +} + +// Show the in-game menu allowing to start a new game +function showMenu(withContinue) { + var mainmenu = { + "": { + "title": "15 Puzzle" + } + }; + if (withContinue) + mainmenu.Continue = continueGame; + mainmenu["Start 3x3"] = () => initGame(3); + mainmenu["Start 4x4"] = () => initGame(4); + mainmenu["Start 5x5"] = () => initGame(5); + mainmenu.About = () => showAbout(withContinue); + mainmenu.Exit = exitGame; + dragger.setEnabled(false); + g.clear(true); + E.showMenu(mainmenu); +} + +// Handle a "click" event (only needed for menu button) +function handleclick(e) { + if (board.menuField.contains(e.x, e.y)) + setTimeout(() => showMenu(true), 10); +} + +// Handle a drag event (moving the stones around) +function handledrag(e) { + worker.addTask(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => board.moveRight(e) : e => board.moveLeft(e)) : + (e.dy > 0 ? e => board.moveDown(e) : e => board.moveUp(e))); + worker.addTask(e => board.drawResult(e)); +} + +// exit the game, clear screen first to prevent ghost images +function exitGame() { + g.clear(true); + setTimeout(load, 300); +} + + +// *** Main program + +g.clear(true); + +// Load global app settings +loadSettings(); + +// We need a worker... +var worker = new Worker(); + +// Board will be initialized after the splash screen has been shown +var board; + +// 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(); + +// Start the game by handling the splash screen sequence +showSplash(() => { + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); + if (startWith === "3x3") + initGame(3); + else if (startWith === "4x4") + initGame(4); + else if (startWith === "5x5") + initGame(5); + else + showMenu(false); +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/puzzle15.app.png b/apps/puzzle15/puzzle15.app.png new file mode 100644 index 000000000..f95366a46 Binary files /dev/null and b/apps/puzzle15/puzzle15.app.png differ diff --git a/apps/puzzle15/puzzle15.settings.js b/apps/puzzle15/puzzle15.settings.js new file mode 100644 index 000000000..352ec4315 --- /dev/null +++ b/apps/puzzle15/puzzle15.settings.js @@ -0,0 +1,50 @@ +// Settings menu for the Puzzle15 app + +(function(back) { + var FILE = "puzzle15.json"; + // Load settings + var settings = Object.assign({ + splashMode: "long", + startWith: "4x4" + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Helper method which uses int-based menu item for set of string values + function stringItems(startvalue, writer, values) { + return { + value: (startvalue === undefined ? 0 : values.indexOf(startvalue)), + format: v => values[v], + min: 0, + max: values.length - 1, + wrap: true, + step: 1, + onchange: v => { + writer(values[v]); + writeSettings(); + } + }; + } + + // Helper method which breaks string set settings down to local settings object + function stringInSettings(name, values) { + return stringItems(settings[name], v => settings[name] = v, values); + } + + var mainmenu = { + "": { + "title": "15 Puzzle" + }, + "< Back": () => back(), + "Splash": stringInSettings("splashMode", ["long", "short", "off"]), + "Start with": stringInSettings("startWith", ["3x3", "4x4", "5x5", "menu"]) + }; + + // Actually display the menu + E.showMenu(mainmenu); + +}); + +// end of file \ No newline at end of file diff --git a/apps/puzzle15/screenshot.png b/apps/puzzle15/screenshot.png new file mode 100644 index 000000000..3a79a3ae6 Binary files /dev/null and b/apps/puzzle15/screenshot.png differ diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog index 16e65d4f9..b9c26b4e3 100644 --- a/apps/rebble/ChangeLog +++ b/apps/rebble/ChangeLog @@ -1,3 +1,4 @@ 0.01: First release 0.02: Fix typo to Purple 0.03: Added dependancy on Pedometer Widget +0.04: Fixed icon and png to 48x48 pixels diff --git a/apps/rebble/README.md b/apps/rebble/README.md index 712fa4e9b..0ecb51d7a 100644 --- a/apps/rebble/README.md +++ b/apps/rebble/README.md @@ -11,6 +11,7 @@ * Uses pedometer widget to get latest step count * Dependant apps are installed when Rebble installs * Uses the whole screen, widgets are made invisible but still run in the background +* The icon is James Dean - 'Rebel Without a Cause' ![](screenshot_rebble.png) ![](screenshot_rebble2.png) diff --git a/apps/rebble/rebble.icon.js b/apps/rebble/rebble.icon.js index 4c898974e..3fc45b820 100644 --- a/apps/rebble/rebble.icon.js +++ b/apps/rebble/rebble.icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("oFA4X/AAIHBw3Aiv3HmE/HQQAF/gPEnWqAAOpy2VqoFB3gPIBoIABtQPJ1PVqv1q3qB5OlrNVEIQPK2tlBwOptQPIyvdH4VtrQPI3tbqtdB4OaB5FVH4NV0pgBB5F13//MIIPJ1O2TgWV/o/I1fbB4WpqoPI1NvB4REBJ5APD/wPBD5JOBB4WVqwPH0oPE0oPJ/NX//6AoNVF5HZq3pq2qSYIPI6tX+pNBB5Ol6v6B4IABH5P7//b1oPBN5GlLwPr9IPK1IPC/SvK1QPCOAIPL6te//5B5lW/5ABL5APB/wPB3IPJ1Y/C/yuBF5APC9X+yo/K34LB3QPBtQPJ//23SPB1QPI3eVs2qJwIPJ1flqyeBtQPJtZPBLwIPKzf/1ROCB5OWAQJOBB5QsBAAQGBf5FlB5tVvoPMNQO9B4daB5O+B4aPIqtX35tBB5M1qtbB4i/HB4WvOAjvGB4IpBIQIADB46aBB4t8B49VB54AFB6zrB1Wm1RTBywPI0oPCeQOaB4+ltOlq2V02VqwPOrQPIF5w/PFQIvPB71pH4uqX8g")) +require("heatshrink").decompress(atob("mEw4X/AoOG4EV+/I+dVAAVUCgcFBIYABpIJBgcFoIKEqkQgEH6EH0ILEqAhCgkBqEVBYdAhUBBoU9GAlAlw5CgERgILDIocEgEGoALDlEHwEAlkUg8EBYfAFwVA+BgEqmQjWrBgMQhgvDqmA9Wq1WsNoMALweDBQIAB4E8BYdTpwLD/kA4AXDjwKC1f/IAILDnQLC1//4ALEHQQLCKgILDFwYLB6EATgVABYe///MNgdA3kQEoILGqCNBlfQh//4NAPAVQ+YLBQYM/ocABYfAiEqgE0g6DBF4eAlFrYQZHDoOu1Xo8lgBYtCKIOo9aOBAAJrCBYWv9X/+gXEqSZC/f//4LHz/6DQIjEBYOhgG6BY1a1WggDCB3ojErYTBoEOa4QLF1X9jWrXwILGKYOvBYtfKYX+17iBHYdX1WQgf/34LBUwQLB1cLWIJqCBYdV9W+1+//oLBWQVVqnuD4M/KQoAB/+kBYJGBCwYLCI4P/DQILFnwLCEQ1Vp+q/46CBYtDXgJ1FAAVwfI4ABqAUCBY8A9gLIqEA9ALEKYYLB9YLERwQ=")) diff --git a/apps/rebble/rebble.png b/apps/rebble/rebble.png index 69653015c..acfd37400 100644 Binary files a/apps/rebble/rebble.png and b/apps/rebble/rebble.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index a2245a02d..4d9881613 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -42,3 +42,4 @@ 0.37: Going into passkey menu now saves settings with passkey 0.38: Restructed menus as per forum discussion 0.39: Fix misbehaving debug info option +0.40: Moved off into Utils, put System after Apps diff --git a/apps/setting/settings.js b/apps/setting/settings.js index 1208018ed..4bdf7f304 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -66,11 +66,10 @@ function showMainMenu() { '': { 'title': 'Settings' }, '< Back': ()=>load(), /*LANG*/'Apps': ()=>showAppSettingsMenu(), - /*LANG*/'Bluetooth': ()=>showBLEMenu(), /*LANG*/'System': ()=>showSystemMenu(), + /*LANG*/'Bluetooth': ()=>showBLEMenu(), /*LANG*/'Alerts': ()=>showAlertsMenu(), - /*LANG*/'Utils': ()=>showUtilMenu(), - /*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() } + /*LANG*/'Utils': ()=>showUtilMenu() }; return E.showMenu(mainmenu); @@ -537,7 +536,8 @@ function showUtilMenu() { setTimeout(showMainMenu, 50); } else showUtilMenu(); }); - } + }, + /*LANG*/'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() } }; if (Bangle.factoryReset) { menu['Factory Reset'] = ()=>{ diff --git a/apps/sonicclk/Changelog b/apps/sonicclk/Changelog new file mode 100644 index 000000000..7c83f6988 --- /dev/null +++ b/apps/sonicclk/Changelog @@ -0,0 +1,2 @@ +1.00 Added sonic clock app +1.01 Fixed text alignment issue; Increased acceleration required to activate twist; \ No newline at end of file diff --git a/apps/sonicclk/README.md b/apps/sonicclk/README.md new file mode 100644 index 000000000..a381e0a07 --- /dev/null +++ b/apps/sonicclk/README.md @@ -0,0 +1,13 @@ +# Sonic Clock + +A classic sonic clock featuring run, stop and wait animations. + +![Sonic Clock screenshot](screenshot.png) + +## Usage + +- Sonic will run when the screen is unlocked +- Sonic will stop when the screen is locked +- Sonic will wait when looking at your watch face (when `Bangle.on("twist", fn)` is fired). + +### Made with love by [Joseph](https://github.com/Johoseph) 🤗 diff --git a/apps/sonicclk/app-icon.js b/apps/sonicclk/app-icon.js new file mode 100644 index 000000000..33e22971b --- /dev/null +++ b/apps/sonicclk/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AE0ZzIACBIgFFC7oTCylEzOIDYeZogX6LwWd7oYCAAOJC82UpoXFAAKnMC6x2CpIVFC/gSCxOYAYP///4C4X/DBIXWIIwWBDAWPAYIXedQJwG/8AgEP//wgAX2CwIADRoQXmSIoXtJAeEoi+C+lEoAcBogX7zOUondolEpvdAAQXgYIgXCAAwXlAAIYC6ENLx4XtAYMZDAvd6gWJC7IKJABgX/C74A/ADY")) diff --git a/apps/sonicclk/app.js b/apps/sonicclk/app.js new file mode 100644 index 000000000..296677281 --- /dev/null +++ b/apps/sonicclk/app.js @@ -0,0 +1,284 @@ +const vw = g.getWidth(); +const bgWidth = 384; + +const sonic = { + bpp: 4, + transparent: 1, +}; + +const defaultSonic = { + width: 36, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECKYOComZxAVDogRXBQfdCQQABCLITCogRFCQIRYLAgAFCQwRQBoQCC///AYYRWGAoMBAAgRYCYf/gEAB4ICBCKrDDCL4UECMA3DolEB4IDBAAQRb7oMB7vdHIIRWCAYRCAAoRWCoozDCBQRRBQI3FERIRPPIbGBPIQiICKAQCdQRZDAYIRXCQIJB/4RDEhQRQGYP//7FEGpARQCAYREgEzERQRNAAIRCLIKkBCLg4CYoIABB5IRWAQQRcBoIODCRYRQB4INECLkRgH/ZAMACRgRSmbGBCIIFBmYWCCLAAC7oCBdgNEAgIRWBQY2B7oTBAwMzCYoRUBIIQBCQQGBHIYRUABwRRA=")), +}; + +const startingBuffers = [ + { + width: 49, + height: 63, + buffer: require("heatshrink").decompress(atob("iIAFzIAEyMRjICBABkZDAogCCxgRBxAACzOUpIzDFxmYC4WJzNEGYgyQzOd6lIDoJiNGQYwB7tEDoQxQI4NN7BPDGJ54BondQAgvPGCjDDGCQVCCAVESAOIx/4GJYuCSIQwBFwX/GJbyEAAmDn///AwJjLyDAAsz/4xBGBAvJGAfwhIwHF5QwDgAwHXwYADGB6+DAAY2EGBYzHGA8P+EAgLyKGBIBBgD3ObwVPGAMEoAwIe4+ZolEGAVEGBRfDAYOUpvd6gaBpvQF5IuBFoOJDANE7vdC4IEBF5h8DGAQABGQNBF5YuCGAgyDYZwxE6lDmcxR5gAFGINEmczF6IxCob1B+YxIF5AxCeoQxJzLNBAAQFBhOUoEDGAIxJjOQC4cJAoIvBgAwCGJIwWiEAgcwglEpOZGwgvCGBANBmcAaIJ2BC4cPC4QwIga3BmEIeoIwBDoJhDF4j5DBgIABgGIGAROBMIYvEilBGAgvDAARIBGQL0FA4IwFF4YABJQIyBoczF4lEGA0JCwWUEoPd7qBCzIwDgAwGF4YXBpvd6iyBzORGAVAgLEEF4YwCF4IwEMYMzGAjECC4YwBF4IwEycxic0GAsN7vRiMZDIQvDBwItBS4IqBGAgQBAgIXCzovCLwItBS4MwgAwCF4XTn/xGIiODF4QwCF40z/4xERwYvCGAQvF7vf+CUEEobvCAAJgEF4Xd/6UEXgTvDAANAMAwwFCwgwESBAwFC4owCF43f/vUoaSCABAvCMAv/aQKSDAA8UF4JgG7/0oIWJGAhgG/9EC5YwCSA5eKFQiQIC5sQF4QwD6hfMGpAvBL5g1IhovVGAQvUGAMEF6owBF6owB")), + }, + { + width: 42, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIAEA4QNEAAwUSBwOIAAWZogXECjgWFzPdogECCj5pDondIggUeSAYUjCISRDCQX/CpIUTXQgAEmf/CoIUaCZIUDgAUZRoYADCkIXHCg4BBgAUZX4y+BogUCogUccwgTB7vdCwIEBFA4UTHQYACCgQACogoJCiYAFCoYTHCigTHCoQnICioaJCRwUUHYbhBHpwUTXoQTCmczCpgUTcwYUB/8zAIIUfgDeBCoQUC/4UeRYQPBCoITCHxQUXCYMAgAVBmboKCjCUCAAQPFToIUaMgOIAAIUGBoIUYCoS9DNYIVECYwUWmYVBcIYVBokRBQIUbBoLhB7pjFOYQTGCixpCAwidCCY4UVCoYGFCYIUeBwLaGCZYUVDQ4TRChA")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5CbobeBAAPdCCgPECAYADoggHCCIRFB4YQQXYwSDOgwQPAAoQDBwMzmYRICB5SCGIYQZB4h0CEBoQKmf/oj/CCQQgHCB4PBAAJ1CxAWBEBAQRmYPBAgJCKCCIyCfoYQXCIKQBB4ndGoIQWiL4BgDaBS4IPBGw4QPBwQSCAgMzCoScECCg4FAAQQVB4IREABAQSAAQtGCCoRDOQYiMCBpxGCCoA==")), + }, + { + width: 44, + height: 62, + buffer: require("heatshrink").decompress(atob("iIAEzIADBAgPFCrIQBxAABAYIZDxAVfC4QSBogaFCsQAB7oWFCsAUCogVmSAQVCCQP/CxgVUYggACmf/CwQVdCYwVEgGIgAVbSoQADCsgYGCowACCroWFolECoVECr7wCAALyB7oXBAYJCJCqgUECoYADokzmYWFCrYWFNoIVceQwXDS4QUBCrYACRwQVDAoIXBCrznCMwRAFAAYpFCqf/mf/CgKZFohQGCrIACS4hFCCQ3dIgIVXIQIUDxAVIBAIVXCYJAFxGITIIVfCwMziOZAIIXEAAIVeAAQFBgBFEDITADCrUAAAoeDCwP/IAX/CrAPBog/CJQoWBHwJsFCrAWBCYpqCCgQNECqwUBCpJrCBooVUIBgABEQwVWokzChRPDCrA/NKIgxJCp4FEAB4VVA")), + }, + { + width: 38, + height: 62, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECSgNBxAABAYIWDxASZCgQPBogXFCTgAB7oTFCTQRCogSgPYQSCB4P/CZISRVIgACmf/CYQSXCAwSEgGIgASVdgbvDCToVGCQwACCS4TFolECQVECTLPCe4b4BAAPdCTARECQYADogkJCSYTFCIoSSaIwUDEYwSTAATKCCQa4GCSwRC/8zog3CehASYaIkAmYTGCSYTBCIISEBIITGCSStEZ4YSceo7PBQpASSBAQVFaBISRgChBAIIRCxAkKCSAPBeQI5CK4ITG7oJBCSgRCPoYSFCIISVTwaiEzIREEAISTABH/AAIDBG4YSce4IAB/43MCSQRD7q1CEhwSQRwS1CCTqzDWwgSTA==")), + }, + { + width: 40, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIAEA4QNEAAoTRBoOIAAWZogWECbQVFzPdogECCbpkDondIIgTcRIYThCASKDCIX/ChATSWIgAEmf/CgITYCRITDgATXQwYADCb4WHCY4BBgATXWwy1BogTCogTabYgSB7vdCoIDBEwwTSG4YACB4QACogmICaQAFCgYSGCaYSHCgQlHCagZIWIISNCaQ5DmYTB/8zCbq0DogThbYgTDExwTQWIszmazKCagWFbQNEgATcHwpoBomZgAULCaYUFgA8SCZlE7q5DHIITdBQPda4ITDRpQTXMQoUJCbLhCCgUzCgwTWCgIJFMxATVCgQABBYoACCa4IEBQwAICZwA=")), + }, + { + width: 36, + height: 60, + buffer: require("heatshrink").decompress(atob("iIANzIPOCI+ZABWICKwTDxGZogDBAAIRaHAfdCQoRXKQdECLhWCBIQRBAgP/CQwRQB4YAFmf/CQQRUK4YRIgGIgARUdIoReCQgRGAAQRVCQtEogRCogRXBoLqEdYIAB7oRWCAczmYRCAAdEEQ4RNOIR4CCQoQECKJxBogQCCIJcCPYPdCKo1CCIwIBG4QRUSYZ8BGgszUoQRSFIQSBdQbGDQQIVBCKIQCGILJCCAYRBRIKuCCKf/xARIiIRYK4ZYExBYDCKjDBxGICQdEAYQQCCKjcBBQKjFGgayDCKYAGT4gRSCQYRICAgRVCRYRYcIIRdCQSZCG44YFCKArCEiARQAAYTCChwRKA==")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5C7oADCQLABCCgPBBwkACgogECBwJFCAoPDCCAPFJgJ5BKYJjECCBqBBwf/mYQBAoMzmYRCCCIpCBAIPCA4gQSB4RrBboY6CoggGCCC+DB4YQBOogQRXwYQGU4YQUSYRTEdQgQTWYQzDB4gQVKooABBwYQSRAIQECYgQCHoQQWHQIQDXYLdDCB4RELYQPDCCyOGXYIPKCCAPGCAIgOCBpBLCCKyDBxYQMA==")), + }, + { + width: 36, + height: 60, + buffer: require("heatshrink").decompress(atob("iIANzIPOCI+ZABWICKwTDxGZogDBAAIRaHAfdCQoRXKQdECLhWCBIQRBAgP/CQwRQB4YAFmf/CQQRUK4YRIgGIgARUdIoReCQgRGAAQRVCQtEogRCogRXBoLqEdYIAB7oRWCAczmYRCAAdEEQ4RNOIR4CCQoQECKJxBogQCCIJcCPYwRRGoQRGBAITCCKoACPgI0FUYQSGCJpNDdQbGDQQQRTJooQECIIMBCMMRCKhwDPIMALAoZBCLQADxAABCAQRXogOBUYgUBCLTIDAAQSB/4RYB4oAB/4QDWQYROYowADgDHCCLASGCIYJDCKI2MDQwRQCQgcGQwwRRBAIAGB44RPA==")), + }, + { + width: 34, + height: 60, + buffer: require("heatshrink").decompress(atob("iIADzIADBAgOECCALBxAABAYIUDxAQVCQQNBogVFCDAAB7oRFCCwPCogQcNoQQCBoP/CI4QPS4gACmf/CIQQTBwwQEgGIfoQQPfIg1ECC6XFCA4ACCCYRFolECAVECCq5CbobeBAAPdCCgPDmczCAQADoggGCBkAA4RPBCIoPDCCAPDCAZXCOgoQQJYgQEBwJbBCIQQUBAIxFCCgPDboi7DEA4QPXwYPDCAJ1GCDQJCA4YQPB4hTEdQYQXgCXDB4YQVawQDBxAFBBwYQVAAYQBGAoABCCCqBBAcAAAIQGXYQQPCIcAYApACCCgPFAAX/XoQ5FCCwPBGQIPFCCIREmYOCB44QRTAabHCCAA==")), + }, +]; + +const stoppingBuffers = [ + { + width: 44, + height: 60, + buffer: require("heatshrink").decompress(atob("iIAEzIACA4gOFAAwVTCQVEzOIDIdECsAWC7oWCAAIVkogVFCwIVgNggAECsARCAQX//4VC/4WJCqg8FCgIWCAQIVdM43/gEACYICBCriVBAAQVmIg4VmIQdETAVEogZBogVhzNE7oTB7oACCoYbBCrCbECQgVFIoIVeCwcACg4VbN4xrGCrgCBBwMAC4QNBBwYVeM4IABIYYABBgIUGCqv/iK1CCQOINwdEAwIVDC4QVVmf/G4QNBCooFCCoRRBCqv//4WBCpJsECrb0DC4QVDYYgHBCq8zCohEDNAIGBCrgTBCob4DZAQGCCgYZBCqoICAAqUBdwQUCKYLaCCsACBCwP/IAP/IBQVVCgQECCgRrBmYVfAAhACe4QUOCqzwCCoKCLCrYWDCiIVWeQYVY")), + }, + { + width: 48, + height: 60, + buffer: require("heatshrink").decompress(atob("iIAEzIAFBIYQFAAwXXDAVEzOIxAaCogXlDAPdDAYZBC8wPBC4oYBC8h2FC84VBB4QCB///C4X/DBQXWIIwWBDAQCBC8AWGCQMAgAVBAQIXgOwR4DC9JLGC83/JIuIoimColEDYNEC7wYCHYIACondCoPdAAQXDDoIXYCoT7CC4gAFC4RLBC7IWHDAkACw4XZToY9CJI51EC7ZICAoIJBCAMADIQJCC8IADCAQXDbIJHBDgIWJC7AABCgOIPAdEAwIXDDJIXWCwQXFAoQXCcIIXnOwgXjDIQXDaogHBC8JKDOQIGBC8zaGAwQWDDYIXeDYkADAIPBUoX/IxAXeAQIYB/5GB/5GMC64WCAgQWCOoMzC8YAEIwVEapIXhDANEC4KONC7oYDCyYXYDAIABC7QA==")), + }, +]; + +const waitingBuffers = [ + { + width: 36, + height: 62, + offset: -1, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECKYOComZxAVDogRXBQfdCQQABCLITCogRFCQIRYLAgAFCQwRQBoR7C/5aECKwwF/4ABBAQDBCK4TD/8AgH/HQYRUYYYRGzICCCKgUECIoDBCLA3DolEB4IDBAAQRb7oMB7vdHIIRWCAYRCAAoRWCoozDCBQRRBQI3FERIRPPIbGBPIQiICKAQCdQRZDAYIRXCQIJB/4RDEhQRQGYP//7FEGpARQCAYREgEzERQRNAAIRCLIKkBCLg4CYoIABB5IRWAQQRcBoIODCRYRQB4INECLkRgH/ZAMACRgRSmbGBCIIFBmYWCCLAAC7oCBdgNEAgIRWBQY2B7oTBAwMzCYoRUBIIQBCQQGBHIYRUABwRRA")), + }, + { + width: 38, + height: 62, + offset: 1, + buffer: require("heatshrink").decompress(atob("iIADzIACA4gNECSoPComZxAWDogSZBYfdCYQABCTYUCogSFCYISaLogAFCY4SRBwSCC/5fECTAzF/4ABBAQDBCTIUD/8AgH/HoYSWZwYSGzICCCSwVECQoDBCTQ6DolECAIDBAAQSd7oNB7vdHgISYCIYSCAAoSYCwo2DCJYSSBYI6FEhQSQQAbRBQAQkJCSIRCe4ReDAYISZCYIKB/4SDExYSRGwP//7QEHBISRCIYSEgEzEhYSOAAISCLwKtBCTw7CaAIABCBQSXAQQSeBwIPDCZgSRCAIOECT0RgH/aYMACYUzAgQSambRBBoIFBmdEgAAECSwAC7oCBfINECgIABCSwLDHIPdCgIRDEYgSWBYIRBCYSFBQwwSTABKpICR4A=")), + }, +]; + +const bg = { + width: 384, + height: 153, + bpp: 8, + transparent: 254, + buffer: require("heatshrink").decompress(atob("i4ASj0evF4pFIDKYA/AEp//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5/bx+PB5vX64RFC54v3P/4v/P/4v/P/4v/P/4v/P/4v7CY5/VHIKxZACYvvP/4v/P/4v/P4/GAAR/BAAJP/P/5P/P/5P/F8sjkYnBO4oAHP64vHGKJfdF7p//P/5//P/4dTP4dIpB//P/5//P/5//F+p/TIIpDXMIohLF95//P/5//P/4APj0eP/5//P/5//P/4vxB4Z/XAH4AfP/4A/P/4A/P/4A/P/4A/P/4A/AB5/JTIaXJWZobFDpovvP/5//P/5//P/5//P/5//P/5//AAIPFCpp/JEpoABIIZHBL7IvvR45//P/5//P/5//P/5//P/5//P/IbTP6IAbJIpLXF+Z//F/5//F/5//F/5//F/5//F/5/tAH5//AH5//AH5//AH5//AH5//AH5//AH5//AH5//AH4AP4wACP4IABI/5//I/5//I/5//I/5//I/5//I/6B7PoNIpBF/P/5F/P/5F/P/5F/P/5F/P/5F/AHMejx//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//P/5//AHfGAAR/BAAJH/P/5H/P/5H/P/5H/P/5H/P/5H/QPZ9BpFIIv5//Iv4Aj6XSP/4A/P/4A/P/4A96XSP/4A/ACcejx//P/5//AEplB6XSAYJ//AH5//AH5//AHJlB6XSAYJ//AH5//AH5//AGPSAARhBA4oABA4ILBA4p//AH5//AH5//AE/SAARhBA4oABA4ILBA4p//AH5//AH5//AE5dB6QACAofX64HFAoZ//AH5//AH5//AFJdB6QACAofX64HFAoZ//AH5//AH5//AFJdB6XS6/XAYIFLCYJ//AH5//AH5//AFJdB6XS6/XAYIFLCYJ//AH5//AH5//AFZfB6XS6/XhgAEA4ILBB4J//AH5//AH5//AFpfB6XS6/XhgAEA4ILBB4J//AH5//AH5//AE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")), +}; + +const topSpeed = 15; +const timeout = 200; +let currentSpeed = 0; +let currentSonic = -1; + +let drawTimeout, drawInterval, waitTimeout; +let bgScroll = [0, null]; + +const start = () => { + if (drawTimeout) clearTimeout(drawTimeout); + if (waitTimeout) clearTimeout(waitTimeout); + if (drawInterval) clearInterval(drawInterval); + + drawInterval = setInterval(() => { + draw("start"); + bgScroll[0] += currentSpeed; + if (bgScroll[1]) bgScroll[1] += currentSpeed; + if (currentSpeed < topSpeed) currentSpeed++; + }, timeout); +}; + +const stop = () => { + if (drawTimeout) clearTimeout(drawTimeout); + if (drawInterval) clearInterval(drawInterval); + + drawInterval = setInterval(() => { + if (currentSpeed <= 0) { + clearInterval(drawInterval); + draw("reset"); + } else { + draw("stop"); + bgScroll[0] += currentSpeed; + if (bgScroll[1]) bgScroll[1] += currentSpeed; + currentSpeed--; + } + }, timeout); +}; + +const wait = () => { + currentSonic = -1; + currentSpeed = 0; + if (drawTimeout) clearTimeout(drawTimeout); + if (drawInterval) clearInterval(drawInterval); + Bangle.setLCDPower(1); + + drawInterval = setInterval(() => draw("wait"), timeout); + + waitTimeout = setTimeout(() => { + clearInterval(drawInterval); + currentSonic = -1; + draw("reset"); + }, 7500); +}; + +const queueDraw = () => { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function () { + drawTimeout = undefined; + draw("reset"); + }, 60000 - (Date.now() % 60000)); +}; + +const drawSonic = (action) => { + let target; + + if (action === "reset" || currentSonic === -1) { + target = defaultSonic; + } else if (action === "start") { + target = startingBuffers[currentSonic]; + } else if (action === "stop") { + if (currentSonic > 1) currentSonic = 0; + target = stoppingBuffers[currentSonic]; + } else { + target = waitingBuffers[currentSonic]; + } + + sonic.width = target.width; + sonic.height = target.height; + sonic.buffer = target.buffer; + sonic.offset = target.offset; + + g.drawImage( + sonic, + vw / 2 - 30 + (50 - sonic.width) + (sonic.offset || 0), + 86 + (65 - sonic.height) + ); + + if (action === "start") { + if (currentSonic === startingBuffers.length - 1) { + currentSonic = 6; + } else { + currentSonic++; + } + } else if (action === "stop") { + if (currentSpeed <= 2) { + currentSonic = -1; + } else if (currentSpeed <= 14) { + currentSonic = 1; + } else { + currentSonic = 0; + } + } else { + if (currentSonic === waitingBuffers.length - 1) { + currentSonic = 0; + } else { + currentSonic++; + } + } +}; + +const drawTime = () => { + const x = vw / 2; + const y = 24 + 25; + + const date = new Date(); + const timeStr = require("locale").time(date, 1).trim(); + const dateStr = require("locale").date(date).toUpperCase(); + + g.setColor("#000"); + g.setFontAlign(0, 0).setFont("6x8", 5); + g.drawString(timeStr, x + 3, y + 2); + + g.setFont("6x8", 1.5); + g.drawString(dateStr, x + 1, y + 29); + + g.setColor("#fff"); + g.setFontAlign(0, 0).setFont("6x8", 5); + g.drawString(timeStr, x, y); + + g.setFont("6x8", 1.5); + g.drawString(dateStr, x, y + 28); +}; + +const draw = (action) => { + if (bgWidth - bgScroll[0] < 0) { + bgScroll[0] = bgScroll[1]; + bgScroll[1] = null; + } + + g.drawImage(bg, -bgScroll[0], 24); + + if (bgWidth - bgScroll[0] < vw) { + bgScroll[1] = bgScroll[0] - bgWidth; + g.drawImage(bg, -bgScroll[1], 24); + } + + drawSonic(action); + drawTime(); + if (action === "reset") queueDraw(); +}; + +g.setTheme({ bg: "#0099ff", fg: "#fff", dark: true }).clear(); + +Bangle.on("lock", (locked) => { + if (locked) { + stop(); + } else { + start(); + } +}); + +Bangle.on("twist", () => wait()); + +Bangle.setOptions({ + lockTimeout: 10000, + backlightTimeout: 12000, + twistThreshold: 1600, +}); + +Bangle.setUI("clock"); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +draw("reset"); + +if (Bangle.isLocked()) { + stop(); +} else { + start(); +} diff --git a/apps/sonicclk/app.png b/apps/sonicclk/app.png new file mode 100644 index 000000000..db359d1e5 Binary files /dev/null and b/apps/sonicclk/app.png differ diff --git a/apps/sonicclk/screenshot.png b/apps/sonicclk/screenshot.png new file mode 100644 index 000000000..63173989a Binary files /dev/null and b/apps/sonicclk/screenshot.png differ diff --git a/apps/touchmenu/ChangeLog b/apps/touchmenu/ChangeLog new file mode 100644 index 000000000..c5277e465 --- /dev/null +++ b/apps/touchmenu/ChangeLog @@ -0,0 +1 @@ +0.01: App launched diff --git a/apps/touchmenu/README.md b/apps/touchmenu/README.md new file mode 100644 index 000000000..0e81f3755 --- /dev/null +++ b/apps/touchmenu/README.md @@ -0,0 +1,40 @@ +# TouchMenu + +A redesign of the built-in `E.showMenu()` to take advantage of the full touch screen on the Bangle.js 2. + +![screenshot](touchmenu.gif) + +## Features + +- All of the features of the built-in `E.showMenu()` +- Icon support for menu items: + ```javascript + menu.items[0].icon = Graphics.createImage(...); + ``` +- Custom accent colors: + ```javascript + E.showMenu({ + "": { + cAB: g.theme.bg2, // Accent background + cAF: g.theme.fg2 // Accent foreground + } + }) + ``` +- Automatic back button detection - name a button `< Back` and it will be given a special position and icon + +## Controls + +- Scroll through the options +- Tap on an option to select it +- Tap on a button again to use it +- Tap on a selected Boolean to toggle it +- Tap on a selected number to change - tap the right side of the screen to decrease, left side to increase +- If detected, tap on the back button in the upper left to go back + +## Requests + +Contact information is on my website: [kyleplo](https://kyleplo.com) + +## Creator + +[kyleplo](https://kyleplo.com) diff --git a/apps/touchmenu/touchmenu.boot.js b/apps/touchmenu/touchmenu.boot.js new file mode 100644 index 000000000..93a0ba1c8 --- /dev/null +++ b/apps/touchmenu/touchmenu.boot.js @@ -0,0 +1,197 @@ +E.showMenu = function(items) { + const gw = g.getWidth(); + const gh = g.getHeight(); + Bangle.removeAllListeners("drag"); + if(!items){ + delete m; + g.clearRect(0, 30, gw, gh - 30); + return false; + } + var loc = require("locale"); + var m = { + info: { + title: "Menu", + cB: g.theme.bg, + cF: g.theme.fg, + cHB: g.theme.bgH, + cHF: g.theme.fgH, + cAB: g.theme.bg2, + cAF: g.theme.fg2, + predraw : () => {}, + preflip : () => {} + }, + scroll: 0, + items: [], + selected: -1, + draw: () => { + g.reset().setFont('12x20'); + m.info.predraw(g); + g.setColor(m.info.cB).fillRect(0, 50, gw, gh - 30).setColor(m.info.cF); + m.items.forEach((e, i) => { + const s = (i * 48) - m.scroll + 50; + if(s < 30 || s > gh - 74){ + return false; + } + if(i == m.selected){ + g.setColor(m.info.cHB).fillRect(0, s, gw, Math.min(s + 48, gh - 30)).setColor(m.info.cHF); + }else{ + g.setColor(m.info.cF); + } + g.drawString(e.title, (e.icon ? 30 : 10), s + 5); + if(e.icon){ + g.drawImage(e.icon, 5, s + 5); + } + if(e.type && s < gh - 72){ + if(e.format){ + g.setFontAlign(1, -1, 0).drawString(e.format(e.value), gw - 10, s + 25).setFontAlign(-1, -1, 0); + }else{ + g.setFontAlign(1, -1, 0).drawString(e.value, gw - 10, s + 25).setFontAlign(-1, -1, 0); + } + } + }); + g.setColor(m.info.cAB).fillRect(0, 30, gw, 50); + g.setColor(m.info.cAF).drawString(m.info.title, (m.back ? 30 : 10), 32); + if(m.back){ + g.drawLine(5, 40, 20, 40); + g.drawLine(5, 40, 15, 33); + g.drawLine(5, 40, 15, 47); + } + m.info.preflip(g, m.scroll > 0, m.scroll < (m.items.length - 1) * 48); + }, + select: (x, y) => { + if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0)){ + if(y){ + if(y < 50 || y > gh - 30){ + return false; + }else{ + m.selected = Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0); + } + }else{ + m.selected = Math.floor(m.scroll / 48); + } + m.draw(); + }else{ + if(m.items[m.selected].type && m.items[m.selected].type === "boolean"){ + m.items[m.selected].value = !m.items[m.selected].value; + m.items[m.selected].onchange(m.items[m.selected].value); + m.draw(); + }else if(m.items[m.selected].type && m.items[m.selected].type === "number"){ + if(x && x < (gw / 2)){ + m.items[m.selected].value = m.items[m.selected].value - (m.items[m.selected].step ? m.items[m.selected].step : 1); + }else{ + m.items[m.selected].value = m.items[m.selected].value + (m.items[m.selected].step ? m.items[m.selected].step : 1); + } + if(m.items[m.selected].value > (m.items[m.selected].max ? m.items[m.selected].max : Infinity)){ + m.items[m.selected].value = m.items[m.selected].min ? m.items[m.selected].min : 0; + } + if(m.items[m.selected].value < (m.items[m.selected].min ? m.items[m.selected].min : 0)){ + m.items[m.selected].value = m.items[m.selected].max ? m.items[m.selected].max : 10; + } + m.items[m.selected].onchange(m.items[m.selected].value); + m.draw(); + }else{ + if(m.items[m.selected]){ + m.items[m.selected](); + } + } + } + }, + move: d => { + m.scroll += (d * 48); + m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48); + m.selected = Math.max(Math.min(Math.floor((m.scroll - 50) / 48), m.items.length - 1), 0); + m.draw(); + }, + }; + Object.keys(items).forEach(i => { + if(i == ""){ + m.info = Object.assign(m.info, items[i]); + }else if(i === "< Back" && items[i]){ + m.back = items[i]; + }else if(items[i]){ + m.items.push(items[i]); + m.items[m.items.length - 1].title = loc.translate(i); + if(items[i].hasOwnProperty("value")){ + if(typeof items[i].value === "boolean"){ + m.items[m.items.length - 1].type = "boolean"; + }else{ + m.items[m.items.length - 1].type = "number"; + } + } + } + }); + m.info.title = loc.translate(m.info.title); + m.draw(); + Bangle.on("drag", d => { + if(!d.b){ + return false; + } + if(d.dx == 0 && d.dy == 0){ + if(d.x < 30 && d.y < 50){ + m.back(); + return false; + } + m.select(d.x, d.y); + }else{ + m.selected = -1; + m.scroll -= d.dy; + m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48); + m.draw(); + } + }); + return m; +}; + +E.showAlert = function (e, t){ + if(!e){ + E.showMenu(); + return false; + } + return new Promise(r => { + const menu = { + "": { + "title": (t ? t : "Alert") + }, + Ok: () => { + E.showMenu(); + r(); + } + }; + menu[e] = () => {}; + E.showMenu(menu); + }); +}; +E.showMessage = E.showAlert; + +E.showPrompt = function (e, t){ + if(!e){ + E.showMenu(); + return false; + } + return new Promise(r => { + const menu = { + "": { + "title": (t && t.title ? t.title : "Choose") + } + }; + menu[e] = () => {}; + if(t && t.buttons){ + Object.keys(t.buttons).forEach(b => { + menu[b] = () => { + E.showMenu(); + r(t.buttons[b]); + }; + }); + }else{ + menu.Yes = () => { + E.showMenu(); + r(true); + }; + menu.No = () => { + E.showMenu(); + r(false); + }; + } + E.showMenu(menu); + }); +}; diff --git a/apps/touchmenu/touchmenu.gif b/apps/touchmenu/touchmenu.gif new file mode 100644 index 000000000..3df4b3462 Binary files /dev/null and b/apps/touchmenu/touchmenu.gif differ diff --git a/apps/touchmenu/touchmenu.png b/apps/touchmenu/touchmenu.png new file mode 100644 index 000000000..58733cbc7 Binary files /dev/null and b/apps/touchmenu/touchmenu.png differ diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 910cd4658..101da48e1 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -11,3 +11,4 @@ 0.12: Allow hiding the widget 0.13: Tweak Bangle.js 2 light theme colors 0.14: Use weather condition code for icon selection +0.15: Fix widget icon diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 8afdfe6df..1d48116e1 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -53,6 +53,16 @@ exports.get = function() { scheduleExpiry(storage.readJSON('weather.json')||{}); +/** + * + * @param cond Weather condition, as one of: + * {number} code: (Preferred form) https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 + * {string} weather description (in English: breaks for other languages!) + * {object} use cond.code if present, or fall back to cond.txt + * @param x Left + * @param y Top + * @param r Icon Size + */ exports.drawIcon = function(cond, x, y, r) { var palette; @@ -249,32 +259,35 @@ exports.drawIcon = function(cond, x, y, r) { g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); } - function chooseIcon(condition) { - if (!condition) return () => {}; - condition = condition.toLowerCase(); - if (condition.includes("thunderstorm")) return drawThunderstorm; - if (condition.includes("freezing")||condition.includes("snow")|| - condition.includes("sleet")) { + /* + * Choose weather icon to display based on weather description + */ + function chooseIconByTxt(txt) { + if (!txt) return () => {}; + txt = txt.toLowerCase(); + if (txt.includes("thunderstorm")) return drawThunderstorm; + if (txt.includes("freezing")||txt.includes("snow")|| + txt.includes("sleet")) { return drawSnow; } - if (condition.includes("drizzle")|| - condition.includes("shower")) { + if (txt.includes("drizzle")|| + txt.includes("shower")) { return drawRain; } - if (condition.includes("rain")) return drawShowerRain; - if (condition.includes("clear")) return drawSun; - if (condition.includes("few clouds")) return drawFewClouds; - if (condition.includes("scattered clouds")) return drawCloud; - if (condition.includes("clouds")) return drawBrokenClouds; - if (condition.includes("mist") || - condition.includes("smoke") || - condition.includes("haze") || - condition.includes("sand") || - condition.includes("dust") || - condition.includes("fog") || - condition.includes("ash") || - condition.includes("squalls") || - condition.includes("tornado")) { + if (txt.includes("rain")) return drawShowerRain; + if (txt.includes("clear")) return drawSun; + if (txt.includes("few clouds")) return drawFewClouds; + if (txt.includes("scattered clouds")) return drawCloud; + if (txt.includes("clouds")) return drawBrokenClouds; + if (txt.includes("mist") || + txt.includes("smoke") || + txt.includes("haze") || + txt.includes("sand") || + txt.includes("dust") || + txt.includes("fog") || + txt.includes("ash") || + txt.includes("squalls") || + txt.includes("tornado")) { return drawMist; } return drawUnknown; @@ -298,7 +311,6 @@ exports.drawIcon = function(cond, x, y, r) { case 531: return drawShowerRain; default: return drawRain; } - break; case 6: return drawSnow; case 7: return drawMist; case 8: @@ -308,16 +320,21 @@ exports.drawIcon = function(cond, x, y, r) { case 802: return drawCloud; default: return drawBrokenClouds; } - break; default: return drawUnknown; } } - if (cond.code && cond.code > 0) { - chooseIconByCode(cond.code)(x, y, r); - } else { - chooseIcon(cond.txt)(x, y, r); + function chooseIcon(cond) { + if (typeof (cond)==="object") { + if ("code" in cond) return chooseIconByCode(cond.code); + if ("txt" in cond) return chooseIconByTxt(cond.txt); + } else if (typeof (cond)==="number") { + return chooseIconByCode(cond.code); + } else if (typeof (cond)==="string") { + return chooseIconByTxt(cond.txt); + } + return drawUnknown; } - + chooseIcon(cond)(x, y, r); }; diff --git a/apps/weather/widget.js b/apps/weather/widget.js index f2ddf0b5b..2905d776b 100644 --- a/apps/weather/widget.js +++ b/apps/weather/widget.js @@ -52,8 +52,8 @@ if (!w) return; g.reset(); g.clearRect(this.x, this.y, this.x+this.width-1, this.y+23); - if (w.txt) { - weather.drawIcon(w.txt, this.x+10, this.y+8, 7.5); + if (w.code||w.txt) { + weather.drawIcon(w, this.x+10, this.y+8, 7.5); } if (w.temp) { let t = require('locale').temp(w.temp-273.15); // applies conversion diff --git a/apps/widbatpc/ChangeLog b/apps/widbatpc/ChangeLog index 99822b5a9..273e611a4 100644 --- a/apps/widbatpc/ChangeLog +++ b/apps/widbatpc/ChangeLog @@ -11,3 +11,4 @@ 0.12: Fixed for Bangle 2 0.13: Fillbar setting added, see README 0.14: Fix drawing the bar when charging +0.15: Added option to always display the icon when charging (useful if 'hide if charge greater than' is enabled) diff --git a/apps/widbatpc/settings.js b/apps/widbatpc/settings.js index b7a5db9e6..b45fc6749 100644 --- a/apps/widbatpc/settings.js +++ b/apps/widbatpc/settings.js @@ -13,6 +13,7 @@ 'fillbar': false, 'charger': true, 'hideifmorethan': 100, + 'alwaysoncharge': false, } // ...and overwrite them with any saved values // This way saved values are preserved if a new version adds more settings @@ -68,6 +69,11 @@ format: x => x+"%", onchange: save('hideifmorethan'), }, + 'Show on charge': { // Not sure if this is readable enough in the 'big' menu + value: s.alwaysoncharge, + format: onOffFormat, + onchange: save('alwaysoncharge'), + }, } E.showMenu(menu) }) diff --git a/apps/widbatpc/widget.js b/apps/widbatpc/widget.js index 3e5ff47b4..5386ffe22 100644 --- a/apps/widbatpc/widget.js +++ b/apps/widbatpc/widget.js @@ -29,6 +29,7 @@ 'percentage': true, 'charger': true, 'hideifmorethan': 100, + 'alwaysoncharge': false, }; Object.keys(DEFAULTS).forEach(k=>{ if (settings[k]===undefined) settings[k]=DEFAULTS[k] @@ -67,8 +68,11 @@ var w = 40; if (Bangle.isCharging() && setting('charger')) w += 16; - if (E.getBattery() > setting('hideifmorethan')) + if (E.getBattery() > setting('hideifmorethan')) { w = 0; + if( Bangle.isCharging() && setting('alwaysoncharge') === true) + w = 56; + } var changed = WIDGETS["batpc"].width != w; WIDGETS["batpc"].width = w; return changed; diff --git a/core b/core index 5a5957714..b05af96b2 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 5a5957714d4aa04413329f57c03e6de0cfb74caf +Subproject commit b05af96b2522a7a7225a56d804faf9383f8a8f97 diff --git a/loader.js b/loader.js index 768f5f38f..a0c280634 100644 --- a/loader.js +++ b/loader.js @@ -40,7 +40,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) { if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { - showToast(`You're using an old Bangle.js firmware (${deviceVersion}). You can update with the instructions here` ,"warning", 20000); + showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update with the instructions here` ,"warning", 20000); } diff --git a/modules/Layout.js b/modules/Layout.js index 6dc4b6368..65e9a8dc8 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -42,8 +42,8 @@ layoutObject has: and `fillx`/`filly` to be set. Not compatible with text rotation. * A `col` field, eg `#f00` for red * A `bgCol` field for background color (will automatically fill on render) -* A `halign` field to set horizontal alignment. `-1`=left, `1`=right, `0`=center -* A `valign` field to set vertical alignment. `-1`=top, `1`=bottom, `0`=center +* A `halign` field to set horizontal alignment WITHIN a `v` container. `-1`=left, `1`=right, `0`=center +* A `valign` field to set vertical alignment WITHIN a `h` container. `-1`=top, `1`=bottom, `0`=center * A `pad` integer field to set pixels padding * A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space * A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space