diff --git a/.eslintignore b/.eslintignore index 57fedb0da..e657b6260 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ apps/animclk/V29.LBM.js apps/banglerun/rollup.config.js +apps/schoolCalendar/fullcalendar/main.js +apps/authentiwatch/qr_packed.js diff --git a/README.md b/README.md index 20ae8afb2..8e186cf79 100644 --- a/README.md +++ b/README.md @@ -384,14 +384,18 @@ Example `settings.js` ```js // make sure to enclose the function in parentheses (function(back) { - function get(key, def) { return require('Settings').get('myappid', key, def); } - function set(key, value) { require('Settings').set('myappid', key, value); } + let settings = require('Storage').readJSON('myappid.json',1)||{}; + if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value + function save(key, value) { + settings[key] = value; + require('Storage').write('myappid.json', settings); + } const appMenu = { '': {'title': 'App Settings'}, '< Back': back, 'Monkeys': { - value: get('monkeys', 12), - onchange: (m) => set('monkeys', m) + value: settings.monkeys, + onchange: (m) => {save('monkeys', m)} } }; E.showMenu(appMenu) diff --git a/apps.json b/apps.json index ea42a1c68..a312b90a3 100644 --- a/apps.json +++ b/apps.json @@ -16,7 +16,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.36", + "version": "0.37", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", @@ -32,7 +32,7 @@ { "id": "messages", "name": "Messages", - "version": "0.03", + "version": "0.07", "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", "type": "app", @@ -41,16 +41,19 @@ "readme": "README.md", "storage": [ {"name":"messages.app.js","url":"app.js"}, + {"name":"messages.settings.js","url":"settings.js"}, {"name":"messages.img","url":"app-icon.js","evaluate":true}, {"name":"messages.wid.js","url":"widget.js"}, {"name":"messages","url":"lib.js"} ], + "data": [{"name":"messages.json"},{"name":"messages.settings.json"}], "sortorder": -9 }, { "id": "android", "name": "Android Integration", - "version": "0.01", + "shortName": "Android", + "version": "0.04", "description": "(BETA) App to display notifications from Gadgetbridge on Android. This will eventually replace the Gadgetbridge widget.", "icon": "app.png", "tags": "tool,system,messages,notifications", @@ -58,15 +61,16 @@ "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"android.app.js","url":"app.js"}, + {"name":"android.settings.js","url":"settings.js"}, {"name":"android.img","url":"app-icon.js","evaluate":true}, {"name":"android.boot.js","url":"boot.js"} ], - "sortorder": -9 + "sortorder": -8 }, { "id": "ios", "name": "iOS Integration", - "version": "0.01", + "version": "0.03", "description": "(BETA) App to display notifications from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", @@ -77,13 +81,13 @@ {"name":"ios.img","url":"app-icon.js","evaluate":true}, {"name":"ios.boot.js","url":"boot.js"} ], - "sortorder": -9 + "sortorder": -8 }, { "id": "health", "name": "Health Tracking", "version": "0.08", - "description": "Logs health data and provides an app to view it (BETA - requires firmware 2v11)", + "description": "Logs health data and provides an app to view it (requires firmware 2v10.100 or later)", "icon": "app.png", "tags": "tool,system,health", "supports": ["BANGLEJS","BANGLEJS2"], @@ -108,19 +112,14 @@ "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"launch.app.js","url":"app-bangle1.js","supports":["BANGLEJS"]}, - {"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]}, - {"name":"launch.settings.js","url":"settings.js","supports":["BANGLEJS2"]} + {"name":"launch.app.js","url":"app-bangle2.js","supports":["BANGLEJS2"]} ], - "data": [ - {"name":"launch.json"} - ] - , "sortorder": -10 }, { "id": "setting", "name": "Settings", - "version": "0.33", + "version": "0.34", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", @@ -136,11 +135,12 @@ { "id": "about", "name": "About", - "version": "0.11", + "version": "0.12", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "icon": "app.png", "tags": "tool,system", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"bangle1-about-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"about.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]}, @@ -170,7 +170,7 @@ { "id": "locale", "name": "Languages", - "version": "0.09", + "version": "0.10", "description": "Translations for different countries", "icon": "locale.png", "type": "locale", @@ -216,7 +216,7 @@ "id": "welcome", "name": "Welcome", "shortName": "Welcome", - "version": "0.13", + "version": "0.14", "description": "Appears at first boot and explains how to use Bangle.js", "icon": "app.png", "screenshots": [{"url":"screenshot_welcome.png"}], @@ -242,6 +242,7 @@ "tags": "start,welcome", "supports": ["BANGLEJS"], "custom": "custom.html", + "screenshots": [{"url":"bangle1-customized-welcome-screenshot.png"}], "storage": [ {"name":"mywelcome.boot.js","url":"boot.js"}, {"name":"mywelcome.app.js","url":"app.js"}, @@ -268,6 +269,20 @@ ], "data": [{"name":"gbridge.json"}] }, + { "id": "gbdebug", + "name": "Gadgetbridge Debug", + "shortName":"GB Debug", + "version":"0.01", + "description": "Debug info for Gadgetbridge. Run this app and when Gadgetbridge messages arrive they are displayed on-screen.", + "icon": "app.png", + "tags": "", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"gbdebug.app.js","url":"app.js"}, + {"name":"gbdebug.img","url":"app-icon.js","evaluate":true} + ] + }, { "id": "mclock", "name": "Morphing Clock", @@ -278,6 +293,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-morphing-clock-screenshot.png"}], "storage": [ {"name":"mclock.app.js","url":"clock-morphing.js"}, {"name":"mclock.img","url":"clock-morphing-icon.js","evaluate":true} @@ -292,6 +308,7 @@ "icon": "app.png", "tags": "", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-moon-phase-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"moonphase.app.js","url":"app.js"}, @@ -422,6 +439,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-sweep-clock-screenshot.png"}], "storage": [ {"name":"sweepclock.app.js","url":"sweepclock.js"}, {"name":"sweepclock.img","url":"sweepclock-icon.js","evaluate":true} @@ -444,6 +462,27 @@ {"name":"matrixclock.img","url":"matrixclock-icon.js","evaluate":true} ] }, + { + "id": "mandlebrotclock", + "name": "Mandlebrot Clock", + "version": "0.01", + "description": "A mandlebrot set themed clock cool", + "icon": "mandlebrotclock.png", + "screenshots": [{ "url": "screenshot_mandlebrotclock.png" }], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + { "name": "mandlebrotclock.app.js", "url": "mandlebrotclock.js" }, + { + "name": "mandlebrotclock.img", + "url": "mandlebrotclock-icon.js", + "evaluate": true + } + ] + }, { "id": "imgclock", "name": "Image background clock", @@ -472,6 +511,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-impercise-word-clock-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"impwclock.app.js","url":"clock-impword.js"}, @@ -548,13 +588,14 @@ { "id": "cubescramble", "name": "Cube Scramble", - "version":"0.03", - "description": "A random scramble generator for the 3x3 Rubik's cube", + "version":"0.04", + "description": "A random scramble generator for the 3x3 Rubik's cube with a basic timer", "icon": "cube-scramble.png", "tags": "", "supports" : ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle2-cube-scramble-screenshot.png"},{"url":"bangle1-cube-scramble-screenshot.png"}], "storage": [ {"name":"cubescramble.app.js","url":"cube-scramble.js"}, {"name":"cubescramble.img","url":"cube-scramble-icon.js","evaluate":true} @@ -658,7 +699,7 @@ { "id": "gpsrec", "name": "GPS Recorder", - "version": "0.24", + "version": "0.26", "description": "Application that allows you to record a GPS track. Can run in background", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -677,7 +718,7 @@ "id": "recorder", "name": "Recorder (BETA)", "shortName": "Recorder", - "version": "0.03", + "version": "0.04", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", @@ -728,11 +769,11 @@ { "id": "slevel", "name": "Spirit Level", - "version": "0.01", + "version": "0.02", "description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat", "icon": "spiritlevel.png", "tags": "tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"slevel.app.js","url":"spiritlevel.js"}, {"name":"slevel.img","url":"spiritlevel-icon.js","evaluate":true} @@ -745,7 +786,7 @@ "description": "Show currently installed apps, free space, and allow their deletion from the watch", "icon": "files.png", "tags": "tool,system,files", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"files.app.js","url":"files.js"}, {"name":"files.img","url":"files-icon.js","evaluate":true} @@ -779,6 +820,7 @@ "tags": "battery", "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle2-charge-animation-screenshot.png"},{"url":"bangle-charge-animation-screenshot.png"}], "storage": [ {"name":"chargeanim.app.js","url":"app.js"}, {"name":"chargeanim.boot.js","url":"boot.js"}, @@ -844,7 +886,7 @@ "id": "widbatpc", "name": "Battery Level Widget (with percentage)", "shortName": "Battery Widget", - "version": "0.13", + "version": "0.14", "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage", "icon": "widget.png", "type": "widget", @@ -984,6 +1026,7 @@ "readme": "README.md", "interface": "interface.html", "allow_emulator": true, + "screenshots": [{"url":"bangle1-stopwatch-screenshot.png"}], "storage": [ {"name":"swatch.app.js","url":"stopwatch.js"}, {"name":"swatch.img","url":"stopwatch-icon.js","evaluate":true} @@ -1185,6 +1228,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-vibrate-clock-screenshot.png"}], "storage": [ {"name":"vibrclock.app.js","url":"app.js"}, {"name":"vibrclock.img","url":"app-icon.js","evaluate":true} @@ -1200,6 +1244,7 @@ "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle2-simple-v-clock-screenshot.png"}], "storage": [ {"name":"svclock.app.js","url":"vclock-simple.js"}, {"name":"svclock.img","url":"vclock-simple-icon.js","evaluate":true} @@ -1215,6 +1260,7 @@ "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle2-dev-clock-screenshot.png"},{"url":"bangle1-dev-clock-screenshot.png"}], "storage": [ {"name":"dclock.app.js","url":"clock-dev.js"}, {"name":"dclock.img","url":"clock-dev-icon.js","evaluate":true} @@ -1246,6 +1292,7 @@ "tags": "party,parrot,lol", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-party-parrot-screenshot.png"}], "storage": [ {"name":"pparrot.app.js","url":"party-parrot.js"}, {"name":"pparrot.img","url":"party-parrot-icon.js","evaluate":true} @@ -1261,6 +1308,7 @@ "tags": "rings,hypnosis,psychadelic", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-hypno-rings-screenshot.png"}], "storage": [ {"name":"hrings.app.js","url":"hypno-rings.js"}, {"name":"hrings.img","url":"hypno-rings-icon.js","evaluate":true} @@ -1328,6 +1376,7 @@ "icon": "show-color.png", "type": "app", "tags": "tool", + "screenshots": [{"url":"bangle1-view-color-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -1343,6 +1392,7 @@ "icon": "clock-mixed.png", "type": "clock", "tags": "clock", + "screenshots": [{"url":"bangle1-mixed-clock-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -1360,6 +1410,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-binary-clock-screenshot.png"}], "storage": [ {"name":"bclock.app.js","url":"clock-binary.js"}, {"name":"bclock.img","url":"clock-binary-icon.js","evaluate":true} @@ -1373,6 +1424,7 @@ "icon": "clock-tris.png", "tags": "game", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-clock-tris-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"clotris.app.js","url":"clock-tris.js"}, @@ -1432,6 +1484,7 @@ "tags": "pomodoro,cooking,tools", "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle2-pomodoro-screenshot.png"}], "storage": [ {"name":"pomodo.app.js","url":"pomodoro.js"}, {"name":"pomodo.img","url":"pomodoro-icon.js","evaluate":true} @@ -1448,6 +1501,7 @@ "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle2-large-digit-blob-clock-screenshot.png"},{"url":"bangle1-large-digit-blob-clock-screenshot.png"}], "storage": [ {"name":"blobclk.app.js","url":"clock-blob.js"}, {"name":"blobclk.img","url":"clock-blob-icon.js","evaluate":true} @@ -1507,6 +1561,7 @@ "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"berlin-clock-screenshot.png"}], "storage": [ {"name":"berlinc.app.js","url":"berlin-clock.js"}, {"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true} @@ -1521,6 +1576,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-center-clock-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"ctrclk.app.js","url":"app.js"}, @@ -1535,6 +1591,7 @@ "icon": "app.png", "type": "app", "tags": "", + "screenshots": [{"url":"bangle1-demo-loop-screenshot1.png"},{"url":"bangle1-demo-loop-screenshot2.png"},{"url":"bangle1-demo-loop-screenshot3.png"},{"url":"bangle1-demo-loop-screenshot4.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -1567,6 +1624,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-pipboy-themed-clock-screenshot.png"}], "storage": [ {"name":"pipboy.app.js","url":"app.js"}, {"name":"pipboy.img","url":"app-icon.js","evaluate":true} @@ -1613,6 +1671,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-workout-HRM-screenshot.png"}], "storage": [ {"name":"wohrm.app.js","url":"app.js"}, {"name":"wohrm.img","url":"app-icon.js","evaluate":true} @@ -1657,6 +1716,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": false, + "screenshots": [{"url":"bangle1-mario-clock-screenshot.png"}], "storage": [ {"name":"marioclock.app.js","url":"marioclock-app.js"}, {"name":"marioclock.img","url":"marioclock-icon.js","evaluate":true} @@ -1695,13 +1755,13 @@ { "id": "barclock", "name": "Bar Clock", - "version": "0.08", + "version": "0.09", "description": "A simple digital clock showing seconds as a bar", "icon": "clock-bar.png", "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], "type": "clock", "tags": "clock", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "allow_emulator": true, "storage": [ @@ -1719,6 +1779,7 @@ "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle2-dot-clcok-screenshot.png"},{"url":"bangle1-dot-clock-screenshot.png"}], "storage": [ {"name":"dotclock.app.js","url":"clock-dot.js"}, {"name":"dotclock.img","url":"clock-dot-icon.js","evaluate":true} @@ -1828,6 +1889,7 @@ "tags": "game,fun", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-rpg-dice-screenshot.png"}], "storage": [ {"name":"rpgdice.app.js","url":"app.js"}, {"name":"rpgdice.img","url":"app-icon.js","evaluate":true} @@ -1856,6 +1918,7 @@ "tags": "clock,minion", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-minion-clock-screenshot.png"}], "storage": [ {"name":"minionclk.app.js","url":"app.js"}, {"name":"minionclk.img","url":"app-icon.js","evaluate":true} @@ -1865,7 +1928,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.09", + "version": "0.10", "description": "[BETA] Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are", "icon": "app.png", "tags": "outdoors,gps", @@ -1933,7 +1996,7 @@ "icon": "custom.png", "type": "bootloader", "tags": "tool,system", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS","BANGLEJS2"], "custom": "custom.html", "storage": [ {"name":"custom"} @@ -1948,6 +2011,7 @@ "icon": "app.png", "tags": "stopwatch,chrono,timer,chronometer", "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url":"bangle1-dev-stopwatch-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"devstopwatch.app.js","url":"app.js"}, @@ -1981,6 +2045,7 @@ "tags": "app,learn,visual", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-NATO-alphabet-screenshot.png"},{"url":"bangle1-NATO-alphabet-screenshot2.png"}], "storage": [ {"name":"nato.app.js","url":"nato.js"}, {"name":"nato.img","url":"nato-icon.js","evaluate":true} @@ -1997,6 +2062,7 @@ "tags": "numerals,clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-numerals-screenshot.png"}], "storage": [ {"name":"numerals.app.js","url":"numerals.app.js"}, {"name":"numerals.img","url":"numerals-icon.js","evaluate":true}, @@ -2123,13 +2189,14 @@ { "id": "metronome", "name": "Metronome", - "version": "0.06", + "version": "0.07", + "readme": "README.md", "description": "Makes the watch blinking and vibrating with a given rate", "icon": "metronome_icon.png", "tags": "tool", - "supports": ["BANGLEJS"], - "readme": "README.md", + "supports": ["BANGLEJS","BANGLEJS2"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-metronome-screenshot.png"}], "storage": [ {"name":"metronome.app.js","url":"metronome.js"}, {"name":"metronome.img","url":"metronome-icon.js","evaluate":true}, @@ -2145,6 +2212,7 @@ "icon": "blackjack.png", "tags": "game", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-black-jack-game-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"blackjack.app.js","url":"blackjack.app.js"}, @@ -2178,6 +2246,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-SWL-clock-screenshot.png"}], "storage": [ {"name":"swlclk.app.js","url":"app.js"}, {"name":"swlclk.img","url":"app-icon.js","evaluate":true} @@ -2255,6 +2324,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-pong-screenshot.png"}], "storage": [ {"name":"pong.app.js","url":"app.js"}, {"name":"pong.img","url":"app-icon.js","evaluate":true} @@ -2317,6 +2387,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-large-clock-screenshot.png"}], "storage": [ {"name":"largeclock.app.js","url":"largeclock.js"}, {"name":"largeclock.img","url":"largeclock-icon.js","evaluate":true}, @@ -2368,6 +2439,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-timer-screenshot.png"}], "storage": [ {"name":"simpletimer.app.js","url":"app.js"}, {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true}, @@ -2384,6 +2456,7 @@ "icon": "beebclock.png", "type": "clock", "tags": "clock", + "screenshots": [{"url":"bangle1-beeb-clock-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -2417,6 +2490,7 @@ "tags": "tools,health", "supports": ["BANGLEJS"], "readme": "README.md", + "screenshots": [{"url":"bangle1-get-up-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"getup.app.js","url":"app.js"}, @@ -2495,6 +2569,7 @@ "version": "0.01", "description": "La palla predice il futuro", "icon": "app.png", + "screenshots": [{"url":"bangle1-magic-8-ball-italiano-screenshot.png"}], "tags": "game", "supports": ["BANGLEJS"], "allow_emulator": true, @@ -2608,6 +2683,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-vertical-watch-face-screenshot.png"}], "storage": [ {"name":"verticalface.app.js","url":"app.js"}, {"name":"verticalface.img","url":"app-icon.js","evaluate":true} @@ -2635,6 +2711,7 @@ "icon": "life.png", "tags": "game", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-game-of-life-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"life.app.js","url":"life.min.js"}, @@ -2682,6 +2759,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-mixed-clock-2-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"miclock2.app.js","url":"clock-mixed.js"}, @@ -2817,6 +2895,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-CPR-assist-screenshot.png"}], "storage": [ {"name":"cprassist.app.js","url":"cprassist.js"}, {"name":"cprassist.img","url":"cprassist-icon.js","evaluate":true}, @@ -2860,6 +2939,7 @@ "icon": "counter_icon.png", "tags": "tool", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-counter-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"counter.app.js","url":"counter.js"}, @@ -2903,7 +2983,7 @@ "id": "cscsensor", "name": "Cycling speed sensor", "shortName": "CSCSensor", - "version": "0.05", + "version": "0.06", "description": "Read BLE enabled cycling speed and cadence sensor and display readings on watch", "icon": "icons8-cycling-48.png", "tags": "outdoors,exercise,ble,bluetooth", @@ -3239,6 +3319,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "readme": "README.md", + "screenshots": [{"url":"bangle1-lazy-clock-screenshot.png"}], "allow_emulator": true, "storage": [ {"name":"lazyclock.app.js","url":"lazyclock-app.js"}, @@ -3340,6 +3421,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-slow-mo-clock-screenshot.png"}], "storage": [ {"name":"slomoclock.app.js","url":"app.js"}, {"name":"slomoclock.img","url":"app-icon.js","evaluate":true}, @@ -3654,7 +3736,7 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.06", + "version": "0.07", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", "screenshots": [{"url":"screenshot_v1.png"},{"url":"screenshot_v2.png"}], @@ -3679,6 +3761,7 @@ "icon": "battleship-icon.png", "tags": "game", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-battle-ship-screenshot.png"}], "readme": "README.md", "allow_emulator": true, "storage": [ @@ -3728,10 +3811,11 @@ "id": "qmsched", "name": "Quiet Mode Schedule and Widget", "shortName": "Quiet Mode", - "version": "0.03", - "description": "Automatically turn Quiet Mode on or off at set times", + "version": "0.04", + "description": "Automatically turn Quiet Mode on or off at set times, and change LCD options while Quiet Mode is active.", "icon": "app.png", - "screenshots": [{"url":"screenshot_edit.png"},{"url":"screenshot_main.png"},{"url":"screenshot_widget_alarms.png"},{"url":"screenshot_widget_silent.png"}], + "screenshots": [{"url":"screenshot_b1_main.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_lcd.png"}, + {"url":"screenshot_b2_main.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_lcd.png"}], "tags": "tool,widget", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", @@ -3845,8 +3929,8 @@ { "id": "thermom", "name": "Thermometer", - "version": "0.02", - "description": "Displays the current temperature, updated every 20 seconds", + "version": "0.03", + "description": "Displays the current temperature in degree Celsius, updated every 20 seconds", "icon": "app.png", "tags": "tool", "supports": ["BANGLEJS"], @@ -3882,6 +3966,7 @@ "type": "clock", "tags": "clock", "supports": ["BANGLEJS"], + "screenshots": [{"url":"bangle1-mystic-clock-screenshot.png"}], "readme": "README.md", "allow_emulator": true, "storage": [ @@ -3898,6 +3983,7 @@ "icon": "hcclock-icon.png", "type": "clock", "tags": "clock", + "screenshots": [{"url":"bangle1-high-contrast-clock-screenshot.png"}], "supports": ["BANGLEJS"], "allow_emulator": true, "storage": [ @@ -3979,6 +4065,7 @@ "tags": "clock", "supports": ["BANGLEJS"], "allow_emulator": true, + "screenshots": [{"url":"bangle1-vector-clock-screenshot.png"}], "storage": [ {"name":"vectorclock.app.js","url":"app.js"}, {"name":"vectorclock.img","url":"app-icon.js","evaluate":true} @@ -3988,10 +4075,11 @@ "id": "fd6fdetect", "name": "fd6fdetect", "shortName": "fd6fdetect", - "version": "0.1", + "version": "0.2", "description": "Allows you to see 0xFD6F beacons near you.", "icon": "app.png", "tags": "tool", + "readme": "README.md", "supports": ["BANGLEJS"], "storage": [ {"name":"fd6fdetect.app.js","url":"app.js"}, @@ -4008,6 +4096,7 @@ "supports": ["BANGLEJS"], "readme": "README.md", "allow_emulator": true, + "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], "storage": [ {"name":"choozi.app.js","url":"app.js"}, {"name":"choozi.img","url":"app-icon.js","evaluate":true} @@ -4032,15 +4121,24 @@ "id": "pastel", "name": "Pastel Clock", "shortName": "Pastel", - "version": "0.05", - "description": "A Configurable clock with custom fonts and background", + "version": "0.08", + "description": "A Configurable clock with custom fonts and background. Has a cyclic information line that includes, day, date, battery, sunrise and sunset times", "icon": "pastel.png", + "dependencies": {"mylocation":"app"}, "screenshots": [{"url":"screenshot_pastel.png"}], "type": "clock", "tags": "clock", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ + {"name":"f_architect","url":"f_architect.js"}, + {"name":"f_gochihand","url":"f_gochihand.js"}, + {"name":"f_cabin","url":"f_cabin.js"}, + {"name":"f_orbitron","url":"f_orbitron.js"}, + {"name":"f_monoton","url":"f_monoton.js"}, + {"name":"f_elite","url":"f_elite.js"}, + {"name":"f_lato","url":"f_lato.js"}, + {"name":"f_latosmall","url":"f_latosmall.js"}, {"name":"pastel.app.js","url":"pastel.app.js"}, {"name":"pastel.img","url":"pastel.icon.js","evaluate":true}, {"name":"pastel.settings.js","url":"pastel.settings.js"} @@ -4067,7 +4165,7 @@ "id": "waveclk", "name": "Wave Clock", "version": "0.02", - "description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: This requires firmware 2v11 or later Bangle.js 1**", + "description": "A clock using a wave image by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: Works on any Bangle.js 2, but requires firmware 2v11 or later on Bangle.js 1**", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", @@ -4083,7 +4181,7 @@ "id": "floralclk", "name": "Floral Clock", "version": "0.01", - "description": "A clock with a flower background by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: This requires firmware 2v11 or later Bangle.js 1**", + "description": "A clock with a flower background by [Lillith May](https://www.instagram.com/_lilustrations_/). **Note: Works on any Bangle.js 2 but requires firmware 2v11 or later on Bangle.js 1**", "icon": "app.png", "screenshots": [{"url":"screenshot_floral.png"}], "type": "clock", @@ -4227,7 +4325,7 @@ "name": "Q Alarm and Timer", "shortName": "Q Alarm", "icon": "app.png", - "version": "0.02", + "version": "0.03", "description": "Alarm and timer app with days of week and 'hard' option.", "tags": "tool,alarm,widget", "supports": ["BANGLEJS", "BANGLEJS2"], @@ -4245,11 +4343,18 @@ "id": "emojuino", "name": "Emojuino", "shortName": "Emojuino", - "version": "0.01", + "version": "0.02", "description": "Emojis & Espruino: broadcast Unicode emojis via Bluetooth Low Energy.", "icon": "emojuino.png", + "screenshots": [ + { "url": "screenshot-tx.png" }, + { "url": "screenshot-swipe.png" }, + { "url": "screenshot-welcome.png" } + ], + "type": "app", "tags": "emoji", "supports" : [ "BANGLEJS2" ], + "allow_emulator": true, "readme": "README.md", "storage": [ { "name": "emojuino.app.js", "url": "emojuino.js" }, @@ -4260,8 +4365,8 @@ "id": "cliclockJS2Enhanced", "name": "Commandline-Clock JS2 Enhanced", "shortName": "CLI-Clock JS2", - "version": "0.1", - "description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design", + "version": "0.02", + "description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design. Also added HID media controlls, just swipe on the clock face to controll the media! Gadgetbride support coming soon(hopefully) Thanks to t0m1o1 for media controls!", "icon": "app.png", "screenshots": [{"url":"screengrab.png"}], "type": "clock", @@ -4293,7 +4398,8 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.02", + "version":"0.06", + "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", "type": "clock", @@ -4309,21 +4415,21 @@ "shortName":"BinWatch", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version":"0.03", + "version":"0.04", "supports": ["BANGLEJS2"], "readme": "README.md", "allow_emulator":true, "description": "Famous binary watch", "tags": "clock", "type": "clock", - "storage": [ + "storage": [ {"name":"binwatch.app.js","url":"app.js"}, + {"name":"binwatch.bg176.img","url":"Background176_center.img"}, + {"name":"binwatch.bg240.img","url":"Background240_center.img"}, {"name":"binwatch.img","url":"app-icon.js","evaluate":true} ] }, - { - "id": "hidmsicswipe", "name": "Bluetooth Music Swipe Controls", "shortName": "Swipe Control", @@ -4336,7 +4442,6 @@ {"name":"hidmsicswipe.app.js","url":"hidmsicswipe.js"}, {"name":"hidmsicswipe.img","url":"hidmsicswipe-icon.js","evaluate":true} ] - }, { "id": "authentiwatch", @@ -4344,7 +4449,7 @@ "shortName": "AuthWatch", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], - "version": "0.01", + "version": "0.03", "description": "Google Authenticator compatible tool.", "tags": "tool", "interface": "interface.html", @@ -4356,7 +4461,219 @@ {"name":"authentiwatch.img","url":"app-icon.js","evaluate":true} ], "data": [{"name":"authentiwatch.json"}] - - + }, + { "id": "schoolCalendar", + "name": "School Calendar", + "shortName":"SCalendar", + "icon": "CalenderLogo.png", + "version": "0.01", + "description": "A simple calendar that you can see your upcoming events that you create in the customizer. Keep in note that your events reapeat weekly.(Beta)", + "tags": "tool", + "readme":"README.md", + "custom":"custom.html", + "supports": ["BANGLEJS"], + "screenshots": [{"url":"screenshot_basic.png"},{"url":"screenshot_info.png"}], + "storage": [ + {"name":"schoolCalendar.app.js"}, + {"name":"schoolCalendar.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"app.json"} + ] + }, + { "id": "timecal", + "name": "TimeCal", + "shortName":"TimeCal", + "icon": "icon.png", + "version":"0.01", + "description": "TimeCal shows the Time along with a 3 week calendar", + "tags": "clock", + "type": "clock", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"timecal.app.js","url":"timecal.app.js"} + ] + }, + { + "id": "a_clock_timer", + "name": "A Clock with Timer", + "version": "0.01", + "description": "A Clock with Timer, Map and Time Zones", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + {"name":"a_clock_timer.app.js","url":"app.js"}, + {"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true} + ] + }, + { + "id":"intervalTimer", + "name":"Interval Timer", + "shortName":"Interval Timer", + "icon": "app.png", + "version":"0.01", + "description": "Interval Timer for workouts, HIIT, or whatever else.", + "tags": "timer, interval, hiit, workout", + "readme":"README.md", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"intervalTimer.app.js","url":"app.js"}, + {"name":"intervalTimer.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "93dub", + "name": "93 Dub", + "shortName":"93 Dub", + "icon": "93dub.png", + "screenshots": [{"url":"screenshot.png"}], + "version":"0.03", + "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", + "supports":["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"93dub.app.js","url":"app.js"}, + {"name":"93dub.img","url":"app-icon.js","evaluate":true} + ] + }, + { "id": "poweroff", + "name": "Poweroff", + "shortName":"Poweroff", + "version":"0.01", + "description": "Simple app to power off your Bangle.js", + "icon": "app.png", + "tags": "poweroff, shutdown", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"poweroff.app.js","url":"app.js"}, + {"name":"poweroff.img","url":"app-icon.js","evaluate":true} + ] +}, +{ + "id": "sensible", + "name": "SensiBLE", + "shortName": "SensiBLE", + "version": "0.02", + "description": "Collect, display and advertise real-time sensor data.", + "icon": "sensible.png", + "type": "app", + "tags": "tool,sensors", + "supports" : [ "BANGLEJS2" ], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + { "name": "sensible.app.js", "url": "sensible.js" }, + { "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true } + ] +}, + { + "id": "widbars", + "name": "Bars Widget", + "version": "0.01", + "description": "Display several measurements as vertical bars.", + "icon": "icon.png", + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbars.wid.js","url":"widget.js"} + ] +}, +{ + "id":"a_speech_timer", + "name":"A Speech Timer", + "icon": "app.png", + "version":"1.00", + "description": "A timer designed to help keeping your speeches and presentations to time.", + "tags": "tool,timer", + "readme":"README.md", + "supports":["BANGLEJS2"], + "storage": [ + {"name":"a_speech_timer.app.js","url":"app.js"}, + {"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true} + ] +}, + { + "id": "sensible", + "name": "SensiBLE", + "shortName": "SensiBLE", + "version": "0.02", + "description": "Collect, display and advertise real-time sensor data.", + "icon": "sensible.png", + "type": "app", + "tags": "tool,sensors", + "supports" : [ "BANGLEJS2" ], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + { "name": "sensible.app.js", "url": "sensible.js" }, + { "name": "sensible.img", "url": "sensible-icon.js", "evaluate": true } + ] + }, + { "id": "mylocation", + "name": "My Location", + "shortName":"My Location", + "icon": "mylocation.png", + "type": "app", + "screenshots": [{"url":"screenshot_1.png"}], + "version":"0.01", + "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", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"mylocation.app.js","url":"mylocation.app.js"}, + {"name":"mylocation.img","url":"mylocation.icon.js","evaluate": true } + ], + "data": [ + {"name":"mylocation.json"} + ] + }, + { + "id": "pebble", + "name": "Pebble Clock", + "shortName": "Pebble", + "version": "0.03", + "description": "A pebble style clock to keep the rebellion going", + "readme": "README.md", + "icon": "pebble.png", + "screenshots": [{"url":"pebble_screenshot.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"pebble.app.js","url":"pebble.app.js"}, + {"name":"pebble.settings.js","url":"pebble.settings.js"}, + {"name":"pebble.img","url":"pebble.icon.js","evaluate":true} + ] + }, + { "id": "pooqroman", + "name": "pooq Roman watch face", + "shortName":"pooq Roman", + "version":"0.0.0", + "description": "A classic watch face with a certain dynamicity. Most amusing in 24h mode. Slide up to show more hands, down for less(!). By design does not support standard widgets, sorry!", + "icon": "app.png", + "type": "clock", + "tags": "clock", + "supports" : ["BANGLEJS2"], + "allow_emulator":true, + "readme": "README.md", + "storage": [ + {"name":"pooqroman.app.js","url":"app.js"}, + {"name":"pooqroman.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"pooqroman.json"} + ] } ] diff --git a/apps/93dub/93dub.png b/apps/93dub/93dub.png new file mode 100644 index 000000000..59950c895 Binary files /dev/null and b/apps/93dub/93dub.png differ diff --git a/apps/93dub/ChangeLog b/apps/93dub/ChangeLog new file mode 100644 index 000000000..5fbfe4fa3 --- /dev/null +++ b/apps/93dub/ChangeLog @@ -0,0 +1,3 @@ +0.01: Initial version for upload +0.02: DiscoMinotaur's adjustments (removed battery and adjusted spacing) +0.03: Code style cleanup diff --git a/apps/93dub/README.md b/apps/93dub/README.md new file mode 100644 index 000000000..fd24d54d8 --- /dev/null +++ b/apps/93dub/README.md @@ -0,0 +1,11 @@ +# 93 Dub + + + +Uses many portions from Espruino documentation, example watchfaces, and the waveclk app. It also sourced from Jon Barlow's 91 Dub v2.0 source code and resources and adapted for Bangle.js 2's screen. Time, date and the battery display works. It is not pixel perfect to the original. + +Contributors: +Leer10 +Orviwan (original watchface and assets) +Gordon Williams (Bangle.js, watchapps for reference code and documentation) +DiscoMinotaur (adjustments) diff --git a/apps/93dub/app-icon.js b/apps/93dub/app-icon.js new file mode 100644 index 000000000..39d11fd6a --- /dev/null +++ b/apps/93dub/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBG2XwAgcPC6P/h//AAIDBA4Pwh/w+AGBAgIDBC4oVDAAITBCAIIBAYIBBAgIvHh4YCFgQPBAoIvCCwoAWIQYAQGLgAWI6bQVdQiiDOyAX/C/7+IAIYvSh4RBAYIXLAwJAHC6ZFCF5yn/C7wDBBAJ3EVAKBDC5QLBYAoLFC5nwCgoXlL44vSL653sL4QXBL6DvXC9YCBACIXCZ4YAQFaYAgPAhqCa4SDFLoZpICYIXDQKLyCDIQXVAAKI0AAYA==")) diff --git a/apps/93dub/app.js b/apps/93dub/app.js new file mode 100644 index 000000000..92544304c --- /dev/null +++ b/apps/93dub/app.js @@ -0,0 +1,137 @@ +// get 12 hour status, code from barclock +const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"]; + +// define background +var imgBg = require("heatshrink").decompress(atob("2GwgJC/AH4A/AH4A/AH4A/AH4A/ACcGAhAV/Cp3gvdug+Gj0AgeABYMBAQMIggVEg/w/9/h/Gn8As3ACpk559zznmseAs0B13nq/Rie+uodCIIUZw9hzFmv+AgcCmco7MRilow1ACpN8gFhwMilFRCoMowgVEIIVhIINhwFg4GiCpfw/dhx/mn4uBCoXRhWktAVFTIVhw9mj8YseDkUnqPEoeuugVEAAlgSgICBACAVC8AUQCQQVSAEsD/4ASeYgA/ACkHNiK5Cj4VR/AVBng+RCQVwCqMOAQPhIKOHgEB44VR8YVBx4VR+eAgOfCqPxwEDCqX5CoKvS/PAgc/YqQVU/gV/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/Cv4V/CsMfCqP4CoOfCqP54EBx4VR+OAgPPCqPzwEA44VR4cAgHhCqMHCoNwAQIAPjwCBngVRvgCBV6XwCoMHCqPAHyIA/AEigEf4IAOkAEDoAPJWAtA+PHv+Al6uPCofAGAgALoHz51/8AVT+IVS+4VPpMR73woH27n/8Eh8+ZmadIqsoyGICofAkMUktJFZAVBzgVBv34YgMhi8RkIVJnGQIIN8/H34FB8kJiIVIkVEyGQkF8/Pj4GBkhBKCoOexEQvHx8fBgMXzMxTJkICoXCVx8AggDGABsD/4AB/AVQAH4APA")); + +// define fonts +// reg number first char 48 28 by 41 +var fontNum = atob("AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//w//j4//A/+P4/8A/4/4AAAAD/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/4AAAAP/wAAAAf/gAAAA//AAAAB/+AAAAD/8AAAAH/wAAAAH/H/gH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wB/4AP/4H/4A//4f/4D//5//4P//h//4//+B//4AAAAAAAAAAAAAAAAAf/+AAAB//4gAAD//jgAAD/+PgABj/4/gAHj/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f88AAfx/8wAAfH/8AAAcf/8AAAR//4AAAH//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAA4AAAAAD4AAYAAP4AD8AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAHgAH/H/GH/H8f/gf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAP//AAAAP//AAAAP//AAAAP/8AAAAP/2AAAAP/eAAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAB/7x/4AH/7H/4Af/4f/4B//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wAAAD//wAAAj//gAADj/+AAAPj/5gAA/j/ngAD/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8AA8f8fwAAx/8fAAAH/8cAAAf/8QAAA//8AAAA//8AAAAAAAAAAAAAA//8D//g//8P/+I//8//44//0//j4//Y/+P4/94/4/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/AAPH/H8AAMf/HwAAB//HAAAH//EAAAH//AAAAH//AAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAGAAAAAAOAAAAAAeAAAAAA+AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB+AAAAAD8AAAAAH4AAAAAPwAAAAAfgAAAAA/AAAAAB8AAAAADx/4B/4HH/4H/4Mf/4f/4R//5//4H//h//4f/+B//4AAAAAAAAAAAAAD//wP/+D//w//4j//z//jj//T/+Pj/9j/4/j/3j/j/gAfgAP/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/AA/AAf8f+8f8fx/+x/8fH/+H/8cf/+f/8R//4f/8H//gf/8AAAAAAAAAAAAAA//8AAAA//8AAAI//8AAA4//0AAD4//YAAP4/94AA/4AH4AD/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/4APwAP/wAfgAf/gA/AA//AB+AB/+AD8AD/8AH4AH/wAPwAH/H/vH/H8f/sf/Hx//h//HH//n//Ef/+H//B//4H//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); +// tiny font for percentage first char 48 6 by 8 +var fontTiny = atob("AH6BgYF+ACFB/wEBAGGDhYlxAEKBkZFuAAx0hP8EAPqRkZGOAH6RkZFOAICHmKDAAG6RkZFuAHKJiYl+AAAAAAAAAAAAAAAA"); +// date font first char 48 12 by 15 +var fontDate = atob("AAAAAfv149wAeADwAeADwAeADvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAPHn9/AAAAAAP0A9wweGDwweGDwweGDvAL8AAAAAAAAAAAgwOGDwweGDwweGDvHp98AAAAA/gB6AAwAGAAwAGAAwAGAPHj9/AAAAAfgF6BwweGDwweGDwweGDgHoB+AAAAAfv169wweGDwweGDwweGDgHoB+AAAAAAAAAAgAGAAwAGAAwAGAAvHh9/AAAAAfv169wweGDwweGDwweGDvHr9+AAAAAfgF6BwweGDwweGDwweGDvHr9+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + +// define days of the week images +var imgMon = E.toArrayBuffer(atob("Ig8BgHwfD5AvB8HD8z8wMPzPzMQzM/M/DMz8z8c7f7f7z////3Oz+3+PzPzPw/M/M/D8z8z8PzPzPw/vB8/n/8H3/A==")); +var imgTue = E.toArrayBuffer(atob("Ig8BwDv9wDAOfmgf/5+Z///n5n/5+fmf/n5+Z//fv9oH////Af37/b/+fn5n/5+fmf/n5+Z/+fn5n/5/g+gfn+D8AA==")); +var imgWed = E.toArrayBuffer(atob("Ig8Bf7gHgM/NA9Az8z/z8PzP/Pw/M/8/D8z/z8c7QPf7z+A//3O3/3+MzP/PwzM/8/D8z/z8PzP/PxAtA9A4B4B4DA==")); +var imgThu = E.toArrayBuffer(atob("Ig8BgHf7f6Ac/M/P/z8z8//PzPzz8/M/PPz8z8+/QLf7/+A///v3+3+8/PzPzz8/M/PPz8z88/PzPzz8/vB/P3/8HA==")); +var imgFri = E.toArrayBuffer(atob("Ig8B/wDwP7+geg/P5/5+c/n/n5z+f+fnP5/5+c/oHoF7/AfAf/7/7/+/n/k/z+f+R/P5/5j8/n/nHz+/++PP7//8+A==")); +var imgSat = E.toArrayBuffer(atob("Ig8B4DwDwDgOgXAJ/5+f/n/n5/+f+fn55/5+fnoHoF/fAfAf//+b/f3/5n5+f/mfn5/+Z+fn//n5+eAef358B7//nA==")); +var imgSun = E.toArrayBuffer(atob("Ig8BwHf7D7Ac/MHD/z8wMP/PzMQ/8/M/D/z8z8QPf7f6A/////83+3+/zPzPz/M/M/P8z8z8//PzPwA/B8/oD8H3/A==")); + + + +// define icons +var imgSep = E.toArrayBuffer(atob("BhsBAAAAAA///////////////AAAAAAA")); +var imgPercent = E.toArrayBuffer(atob("BwcBuq7ffbqugA==")); +var img24hr = E.toArrayBuffer(atob("EwgBj7vO53na73tcDtu9uDev7vA93g==")); +var imgPM = E.toArrayBuffer(atob("EwgB+HOfdnPu1X3ar4dV9+q+/bfftg==")); + +//vars +var separator = true; +var is24hr = !is12Hour; +var leadingZero = true; + +//the following 2 sections are used from waveclk to schedule minutely updates +// 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; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function drawBackground() { + g.setBgColor(0,0,0); + g.setColor(1,1,1); + g.clear(); + g.drawImage(imgBg,0,0); + g.reset(); +} + +function draw(){ + drawBackground(); + var date = new Date(); + var h = date.getHours(), m = date.getMinutes(); + var d = date.getDate(), w = date.getDay(); + g.reset(); + g.setBgColor(0,0,0); + g.setColor(1,1,1); + + //draw 24 hr indicator and 12 hr specific behavior + if (is24hr){ + g.drawImage(img24hr,32, 65); + if (leadingZero){ + h = ("0"+h).substr(-2); + } + } else if (h > 12) { + g.drawImage(imgPM,40, 70); + h = h - 12; + if (leadingZero){ + h = ("0"+h).substr(-2); + } else { + h = " " + h; + } + } + + //draw separator + if (separator){ + g.drawImage(imgSep, 85,98);} + + //draw day of week + var imgW = null; + if (w == 0) {imgW = imgSun;} + if (w == 1) {imgW = imgMon;} + if (w == 2) {imgW = imgTue;} + if (w == 3) {imgW = imgWed;} + if (w == 4) {imgW = imgThr;} + if (w == 5) {imgW = imgFri;} + if (w == 6) {imgW = imgSat;} + g.drawImage(imgW, 85, 63); + + + // draw nums + // draw time + g.setColor(0,0,0); + g.setBgColor(1,1,1); + g.setFontCustom(fontNum, 48, 28, 41); + if (h<10) { + if (leadingZero) { + h = ("0"+h).substr(-2); + } else { + h = " " + h; + } + } + g.drawString(h, 25, 90, true); + g.drawString(("0"+m).substr(-2), 92, 90, true); + // draw date + g.setFontCustom(fontDate, 48, 12, 15); + g.drawString(("0"+d).substr(-2), 123,63, true); + + // widget redraw + Bangle.drawWidgets(); + queueDraw(); +} + + +draw(); + +//the following section is also from waveclk +Bangle.on('lcdPower',on=>{ + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}); + +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); diff --git a/apps/93dub/screenshot.png b/apps/93dub/screenshot.png new file mode 100644 index 000000000..197c52c01 Binary files /dev/null and b/apps/93dub/screenshot.png differ diff --git a/apps/a_clock_timer/ChangeLog b/apps/a_clock_timer/ChangeLog new file mode 100644 index 000000000..c01ad2077 --- /dev/null +++ b/apps/a_clock_timer/ChangeLog @@ -0,0 +1 @@ +0.01: Beta version for Bangle 2 (2021/11/28) diff --git a/apps/a_clock_timer/README.md b/apps/a_clock_timer/README.md new file mode 100644 index 000000000..e8e2647a9 --- /dev/null +++ b/apps/a_clock_timer/README.md @@ -0,0 +1,15 @@ +# A Clock with Timer, Map and Time Zones + +* Works with Bangle 2 +* Timer + * Right tap: start/increase by 10 minutes; Left tap: decrease by 5 minutes + * Short buzz at T-30, T-20, T-10 ; Double buzz at T +* Other time zones + * Currently hardcoded to Paris and Tokyo (this will be customizable in a future version) +* World Map + * The yellow line shows the position of the sun + + + +## Creator +[@alainsaas](https://github.com/alainsaas) diff --git a/apps/a_clock_timer/app-icon.js b/apps/a_clock_timer/app-icon.js new file mode 100644 index 000000000..86e58b698 --- /dev/null +++ b/apps/a_clock_timer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AAnAnEH4Ef+eAiEDAoPDz+T/ff/+T3+T/VAj8z/0f4VP51zDoX/5Hzz/z//f5EBAoP+r4FBFIgPBAAP4v5AFABPvrwSB0YFBrtX/+nCI3u/+vhFhh/q/f/9Fhu4NB187v3n/fvCIf/CIIAFRIUB8EAg3QgJmB4H/iAEB//+/lggqUC//wi4FB8AHBj4FB+H/wEzBgPg/0AkE3BIP8gE8n4VBGIN/IAPAsEA//8v6OBAoUjgEIAoPwkMATIN//BQBgfgg/wAoMH/EHEwILB/gNBgFgAocByEB/ED9AoCAoPAgE4gHwgeAgOYgAVBAoMYAoKECAoIVBAoIfBoCRCAAw=")) diff --git a/apps/a_clock_timer/app.js b/apps/a_clock_timer/app.js new file mode 100644 index 000000000..5f9a3a468 --- /dev/null +++ b/apps/a_clock_timer/app.js @@ -0,0 +1,129 @@ +// assets +function getImg() { + return require("heatshrink").decompress(atob("2FRgP/ABnxBRP5BJH+gEfBZHghnAv4JFmA+Bj0PBIn3//4h3An4oDAQJWEEIf8AwMEuFOCofAh/QjAWEg4VEwEAnw2DDoKEHEYPwAoUBmgrDhgUHS4XgAwUD/gVC/g+FAAZgEwEf4YqC/EQFQ4NDFgV/4Z3C/EcCo1974VCLAV/V4d7Co9/Co0PCoX+vk4Ko/HCosCRYX5nwTFkEAr/nCokICoL+B/aCGCoMHCoq3EdoraGCosPz4HBcILEJCocBwEHOwQrIgQrHgoHCFYMEgwVJYoMBsEnCofAnkMNQJXH4D4EbQMPkF/xwrEj+/HIkAoAVDj8QueHCoorDCoUDLwd96J0BKwgrHh4VDv+9CosDx6QCCo4HB//8VwvvXgQVDJIYSBCo/sBwaZBgF/NoYVHgH8V4qYDAwUYlAVFEYbFDDgwAGConogf9Zg8DCpP4cIh0Dg0BGAgVE+gVIgUA+AVI+wVE/xAEh5HDEgn+CpEAbgJCCHQoVBn4VJ/ED4ANDAAQVJ4EPPQPAt4VF4BeDColgj/8h/gFYwJBCpF//k//ANDCAYVIcgP+CpH/54VHCAIVB/4VIwYECCocIAwIVBx4VG9+AMITbCYAYJB34VG/UAj4VI7/9Cgw9CJYXAmBtDMAQsIfYhvCCofyvywGB4QFFgYGC/d+agYVLSgf8+ArG/APBD4QVBgh0CAwNwv/fCo4PCCo94s7VDCohnDAoI7Enlv8BZECoRCDAggAB3/3/gzDMAIVFY4IVE4IPBOoZ9DCpXwCoMvCqKxB//3bYywD4BtFAAPfDooVFFYIVGw4VFB4KZFngNE/uPCovgFYgEBuK+Fg4zFCoIrFCovwgQVF+AVFgPxEYzFEbgQVD4EDCoozBYogVCgYVE8bpGCo4HDCoPzBgoVIL4fAg4MGgAIHCofgCszND8BOHK4x2BCofwXgv4h6vGCps/Co6uDAA/7RgIjDDwTaDABPA//9FaAtDCop0FC5YVDLwoAH8//94GD/wVNCYKNECpwPBQggVPNggVBNp4VFFZwAGCquHCqnzCB4")); +} +var IMAGEWIDTH = 176; +var IMAGEHEIGHT = 81; + +Graphics.prototype.setFontMichroma36 = function() { +g.setFontCustom(atob("AAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAAAAAAAAAAAAAAAAAAAGAAAAA+AAAAD+AAAAP+AAAA/8AAAD/wAAAf/AAAB/4AAAH/gAAAf+AAAB/4AAAH/gAAAf+AAAAfwAAAAfAAAAAcAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AP///8APwAD+APAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAeAPAAAeAPwAD+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAEAAAAAOAAAAAfAAAAA+AAAAB8AAAAD8AAAAH4AAAAPwAAAAPgAAAAfAAAAAf///+Af///+Af///+Af///+AAAAAAAAAAAAAAAAAAAAAAAAAA/Af+AD/A/+AH/B/+AP/D/+APwD4eAPADweAfADweAeADweAeADweAeADweAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAPgeAeAPAeAeAPAeAeAPAeAeAPAeAfAPAeAPw/AeAP/+AeAH/+AeAD/8AeAB/wAOAAAAAAAAAAAAAAAAAAAAAAAAAB8APgAD8AP4AH8AP8AP8AP8APgAB+AfAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAeAfAeAeAPx/h+AP///+AH///8AD///4AB/h/gAAAAAAAAAAAAAAAAAAAAAAeAAAAA/AAAAA/AAAAB/AAAAD/AAAAH/AAAAPvAAAAPPAAAAfPAAAA+PAAAB8PAAAD4PAAADwPAAAHwPAAAPgPAAAfAPAAA+APAAA8APAAB8APAAD4APAAHwAPAAPgAPAAPAAPAAfAAPAAf///+Af///+Af///+Af///+AAAAPAAAAAPAAAAAPAAAAAPAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAf/8PgAf/8P4Af/8P8Af/8P8AeB4A+AeB4AeAeDwAeAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAfAeDwAeAeD4A+AeD+D+AeB//8AeB//4AeA//4AAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AH///8AP4fB+APAeAeAfA8AeAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAfA8APAPA+AeAPgeAeAP8fh+AH8f/8AD8P/8AA8H/4AAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAACAeAAAGAeAAAOAeAAAeAeAAA+AeAAD+AeAAH8AeAAP4AeAAfwAeAA/gAeAB/AAeAD+AAeAP4AAeAfwAAeA/gAAeB/AAAeD+AAAeH8AAAefwAAAe/gAAAf/AAAAf+AAAAf8AAAAf4AAAAfgAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAMAAB+B/wAD/j/4AH/3/8AP///+AP//A+AfB+AeAeA+AeAeA+APAeA+APAeA+APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA+APAeA+APAeA+APAeA+AOAeA+AeAPh/A+AP///+AP/3/8AH/3/8AB/D/wAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAD/4HAAH/8HwAP/+H4AP5/H8AfAfA8AeAPAeAeAPAeAeAPAeAeAHgfAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHAPAeAPAOAeAPAeAPAPAeAPwfB+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAAAAAAAB8DwAAB8HwAAB8HwAAB8DwAAAAAAAAAAAAA"), 46, atob("CBIkESMjJCMjIyMjCA=="), 36+(1<<8)+(1<<16)); +}; + +Graphics.prototype.setFontMichroma16 = function(scale) { +g.setFontCustom(atob("AAAAGAAYAAAAGAB4A/APwD4AeADgAAAAAAA/8H/4YBjAGMAcwBzAHMAcwBzAHMAYYBh/+D/wAAAAABgAOABwAGAA//h/+AAAAAA4+Hn4YZjhmMOYw5jDmMMYwxjDGOMYYxh/GD4YAAAAADBwcHhgGOAYwBzHHMccxxzHHMcc5xhnGH/4PfAAAAAAAOAB4APgB2AGYAxgHGA4YDBgYGD/+P/4AOAAYAAAAAD+cP547BjsGOwc7BzsHOwc7BzsHOwY7zjv+APgAAAAAD/wf/hmGOYYxhzGHMYcxhzGHOYYZhh3uDP4AeAAAEAA4ADgAOAI4DjgeODw4eDjgOcA7gD8APgA8AAAAAAAAAA58H/4bxjmGMYcxhzGHMYcxhzGHOYYbxh/+DnwAAAAADxgfnBnOOMYwxjDHMMcwxzDHMMY4xhjOH/4P/AAAAAABnAGcAAA"), 46, atob("BAgQCBAQEBAQEBAQBA=="), 16+(scale<<8)+(1<<16)); +}; + +// timer +var timervalue = 0; +var istimeron = false; +var timertick; + +Bangle.on('touch',t=>{ + if (t == 1) { + Bangle.buzz(30); + if (timervalue < 5*60) { timervalue = 1 ; } + else { timervalue -= 5*60; } + } + else if (t == 2) { + Bangle.buzz(30); + if (!istimeron) { + istimeron = true; + timertick = setInterval(countDown, 1000); + } + timervalue += 60*10; + } +}); + +function timeToString(duration) { + var hrs = ~~(duration / 3600); + var mins = ~~((duration % 3600) / 60); + var secs = ~~duration % 60; + var ret = ""; + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); + } + ret += "" + mins + ":" + (secs < 10 ? "0" : ""); + ret += "" + secs; + return ret; +} + +function countDown() { + timervalue--; + + g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); + + g.setFontAlign(0, -1, 0); + g.setFont("6x8").drawString("Timer", 44, g.getHeight()/2-20); + g.setFont("Michroma16").drawString(timeToString(timervalue), 44, g.getHeight()/2-10); + + if (timervalue <= 0) { + istimeron = false; + clearInterval(timertick); + + Bangle.buzz().then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 500)); + }).then(()=>{ + return Bangle.buzz(1000); + }); + } + else + if ((timervalue <= 30) && (timervalue % 10 == 0)) { Bangle.buzz(); } +} + +function showWelcomeMessage() { + g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); + g.setFontAlign(0, 0).setFont("6x8"); + g.drawString("Touch right to", 44, 80); + g.drawString("start timer", 44, 88); + setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000); +} + +// time +var drawTimeout; + +function getGmt() { + var d = new Date(); + var gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000); + return gmt; +} + +function getTimeFromTimezone(offset) { + return new Date(getGmt().getTime() + offset * 60 * 60 * 1000); +} + +function queueNextDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +} + +function draw() { + g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT); + g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT); + + var x_sun = 176 - (getGmt().getHours() / 24 * 176 + 4); + g.setColor('#ff0').drawLine(x_sun, g.getHeight()-IMAGEHEIGHT, x_sun, g.getHeight()); + g.reset(); + + var locale = require("locale"); + + var date = new Date(); + g.setFontAlign(0,0); + g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46); + g.setFont("6x8"); + g.drawString(locale.date(new Date(),1), 125, 68); + g.drawString("PAR "+locale.time(getTimeFromTimezone(1),1), 125, 80); + g.drawString("TYO "+locale.time(getTimeFromTimezone(9),1), 125, 88); + + queueNextDraw(); +} + +// init +g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); +draw(); +Bangle.setUI("clock"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +showWelcomeMessage(); diff --git a/apps/a_clock_timer/app.png b/apps/a_clock_timer/app.png new file mode 100644 index 000000000..b91bc3f18 Binary files /dev/null and b/apps/a_clock_timer/app.png differ diff --git a/apps/a_clock_timer/screenshot.png b/apps/a_clock_timer/screenshot.png new file mode 100644 index 000000000..4fb3dd9f2 Binary files /dev/null and b/apps/a_clock_timer/screenshot.png differ diff --git a/apps/a_speech_timer/ChangeLog b/apps/a_speech_timer/ChangeLog new file mode 100644 index 000000000..4a8e3fbe7 --- /dev/null +++ b/apps/a_speech_timer/ChangeLog @@ -0,0 +1 @@ +1.00: Release (2021/12/01) diff --git a/apps/a_speech_timer/README.md b/apps/a_speech_timer/README.md new file mode 100644 index 000000000..098c352f3 --- /dev/null +++ b/apps/a_speech_timer/README.md @@ -0,0 +1,16 @@ +# A Speech Timer + +* A timer designed to help keeping your speeches and presentations to time +* Vibrates 1-2-3 times and changes screen color within the target time range. + * Example for a 5 to 7 minutes speech: vibrates once at 5:00 (green), twice at 6:00 (yellow), thrice at 7:00 (red). +* Use the buttons to start a timer +* Swipe left or right to choose different target times +* Touching the timer on the upper part of the screen locks (or unlocks) the buttons to prevent accidental changes + + + + + + +## Creator +[@alainsaas](https://github.com/alainsaas) diff --git a/apps/a_speech_timer/app-icon.js b/apps/a_speech_timer/app-icon.js new file mode 100644 index 000000000..1fdb2c509 --- /dev/null +++ b/apps/a_speech_timer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP//kAj//AAP5/+PApH7//PAonvAoXzAonj//nApHggEHAoWAgA5BAAJCCAoU/IYIFCv///w0CAonrv/HAoXLv+DAogLFgPeAoV+nlOAoV4/8+AoV79+eFIVzAof7u/v5xBCs4FL84FE//O74FBu4FB64FD73TAoNz/+eAoV5IIIFCvl8vwFCv8A/wFDO4IFFFIQFCGoSVFUIqtDh65D/1vYof+Y4LLDw7dD/0ndIYRCeoQFC/P/z/+i///oFBGoX8gEfAgI=")) diff --git a/apps/a_speech_timer/app.js b/apps/a_speech_timer/app.js new file mode 100644 index 000000000..dae2545b2 --- /dev/null +++ b/apps/a_speech_timer/app.js @@ -0,0 +1,173 @@ +Graphics.prototype.setFontMichroma36 = function() { +g.setFontCustom(atob("AAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAAAAAAAAAAAAAAAAAAAGAAAAA+AAAAD+AAAAP+AAAA/8AAAD/wAAAf/AAAB/4AAAH/gAAAf+AAAB/4AAAH/gAAAf+AAAAfwAAAAfAAAAAcAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AP///8APwAD+APAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAPAeAAAeAPAAAeAPwAD+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAEAAAAAOAAAAAfAAAAA+AAAAB8AAAAD8AAAAH4AAAAPwAAAAPgAAAAfAAAAAf///+Af///+Af///+Af///+AAAAAAAAAAAAAAAAAAAAAAAAAA/Af+AD/A/+AH/B/+AP/D/+APwD4eAPADweAfADweAeADweAeADweAeADweAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAHgeAeAPgeAeAPAeAeAPAeAeAPAeAeAPAeAfAPAeAPw/AeAP/+AeAH/+AeAD/8AeAB/wAOAAAAAAAAAAAAAAAAAAAAAAAAAB8APgAD8AP4AH8AP8AP8AP8APgAB+AfAAAeAeAAAeAeAAAPAeAAAPAeAAAPAeAAAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAPAeAeAeAfAeAeAPx/h+AP///+AH///8AD///4AB/h/gAAAAAAAAAAAAAAAAAAAAAAeAAAAA/AAAAA/AAAAB/AAAAD/AAAAH/AAAAPvAAAAPPAAAAfPAAAA+PAAAB8PAAAD4PAAADwPAAAHwPAAAPgPAAAfAPAAA+APAAA8APAAB8APAAD4APAAHwAPAAPgAPAAPAAPAAfAAPAAf///+Af///+Af///+Af///+AAAAPAAAAAPAAAAAPAAAAAPAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAf/8PgAf/8P4Af/8P8Af/8P8AeB4A+AeB4AeAeDwAeAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAPAeDwAfAeDwAeAeD4A+AeD+D+AeB//8AeB//4AeA//4AAAP/AAAAAAAAAAAAAAAAAAAAAAAAAAA///AAD///wAH///4AH///8AP4fB+APAeAeAfA8AeAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAfA8APAPA+AeAPgeAeAP8fh+AH8f/8AD8P/8AA8H/4AAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAAAAeAAACAeAAAGAeAAAOAeAAAeAeAAA+AeAAD+AeAAH8AeAAP4AeAAfwAeAA/gAeAB/AAeAD+AAeAP4AAeAfwAAeA/gAAeB/AAAeD+AAAeH8AAAefwAAAe/gAAAf/AAAAf+AAAAf8AAAAf4AAAAfgAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAMAAB+B/wAD/j/4AH/3/8AP///+AP//A+AfB+AeAeA+AeAeA+APAeA+APAeA+APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA8APAeA+APAeA+APAeA+APAeA+AOAeA+AeAPh/A+AP///+AP/3/8AH/3/8AB/D/wAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAD/4HAAH/8HwAP/+H4AP5/H8AfAfA8AeAPAeAeAPAeAeAPAeAeAHgfAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHgPAeAHAPAeAPAOAeAPAeAPAPAeAPwfB+AP///8AH///4AD///wAA///AAAAAAAAAAAAAAAAAAAAAAAAAAAB8DwAAB8HwAAB8HwAAB8DwAAAAAAAAAAAAA"), 46, atob("CBIkESMjJCMjIyMjCA=="), 36+(1<<8)+(1<<16)); +}; + +Graphics.prototype.setFontMichroma16 = function(scale) { +g.setFontCustom(atob("AAAAGAAYAAAAGAB4A/APwD4AeADgAAAAAAA/8H/4YBjAGMAcwBzAHMAcwBzAHMAYYBh/+D/wAAAAABgAOABwAGAA//h/+AAAAAA4+Hn4YZjhmMOYw5jDmMMYwxjDGOMYYxh/GD4YAAAAADBwcHhgGOAYwBzHHMccxxzHHMcc5xhnGH/4PfAAAAAAAOAB4APgB2AGYAxgHGA4YDBgYGD/+P/4AOAAYAAAAAD+cP547BjsGOwc7BzsHOwc7BzsHOwY7zjv+APgAAAAAD/wf/hmGOYYxhzGHMYcxhzGHOYYZhh3uDP4AeAAAEAA4ADgAOAI4DjgeODw4eDjgOcA7gD8APgA8AAAAAAAAAA58H/4bxjmGMYcxhzGHMYcxhzGHOYYbxh/+DnwAAAAADxgfnBnOOMYwxjDHMMcwxzDHMMY4xhjOH/4P/AAAAAABnAGcAAA"), 46, atob("BAgQCBAQEBAQEBAQBA=="), 16+(scale<<8)+(1<<16)); +}; + +function timeToString(duration) { + var hrs = ~~(duration / 3600); + var mins = ~~((duration % 3600) / 60); + var secs = ~~duration % 60; + var ret = ""; + if (hrs > 0) { + ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); + } + ret += "" + mins + ":" + (secs < 10 ? "0" : ""); + ret += "" + secs; + return ret; +} + +var newtimer_left_from = 60; +var newtimer_left_to = 2*60; + +var newtimer_right_from = 5*60; +var newtimer_right_to = 7*60; + +var current_from = 5*60; +var current_mid = 6*60; +var current_to = 7*60; +var current_value = 0; + +var timerinterval; +var istimeron = false; + +var islocked = false; + +function countDown() { + current_value++; + draw(); + + if (current_value == current_from) { + Bangle.buzz(500); + } else if (current_value == current_mid) { + Bangle.buzz(400).then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 800)); + }).then(()=>{ + return Bangle.buzz(500); + }); + } else if (current_value == current_to) { + Bangle.buzz(300).then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 600)); + }).then(()=>{ + Bangle.buzz(300).then(()=>{ + return new Promise(resolve=>setTimeout(resolve, 600)); + }).then(()=>{ + return Bangle.buzz(500); + }); + }); + } + +} + +Bangle.on('touch',(touchside, touchdata)=>{ + if (!islocked && istimeron && touchdata.y > (100+10)) { + Bangle.buzz(40); + istimeron = false; + clearInterval(timerinterval); + } else if (touchdata.y > 24 && touchdata.y < (100-10)) { + Bangle.buzz(40); + islocked = !islocked; + } else if (!islocked && touchdata.y > (100+10) && touchdata.x > 88 + 10) { + Bangle.buzz(40); + current_from = newtimer_right_from; + current_to = newtimer_right_to; + current_mid = (current_from + current_to) / 2; + current_value = 0; + if (timerinterval) clearInterval(timerinterval); + timerinterval = setInterval(countDown, 1000); + istimeron = true; + } else if (!islocked && touchdata.y > (100+10) && touchdata.x < 88 - 10) { + Bangle.buzz(40); + current_from = newtimer_left_from; + current_to = newtimer_left_to; + current_mid = (current_from + current_to) / 2; + current_value = 0; + if (timerinterval) clearInterval(timerinterval); + timerinterval = setInterval(countDown, 1000); + istimeron = true; + } + showInstructions = false; + draw(); +}); + +Bangle.on('swipe',(swiperight, swipedown)=>{ + console.log(swiperight); + console.log(swipedown); + + if (swiperight == -1) { + if (newtimer_left_from >= 60) { + newtimer_left_from += 60; + newtimer_left_to += 60; + } else { // special case for 0:30 to 1:00 + newtimer_left_from = 60; + newtimer_left_to = 120; + } + newtimer_right_from += 60; + newtimer_right_to += 60; + draw(); + } else if (swiperight == 1) { + if (newtimer_left_from > 60) { + newtimer_left_from -= 60; + newtimer_left_to -= 60; + } else { // special case for 0:30 to 1:00 + newtimer_left_from = 30; + newtimer_left_to = 60; + } + + if (newtimer_right_from > 120) { + newtimer_right_from -= 60; + newtimer_right_to -= 60; + } + draw(); + } +}); + +var drawTimeout; +var showInstructions = true; + +function draw() { + g.reset(); + if (current_value >= current_to) { g.setBgColor("#F00"); } + else if (current_value >= current_mid) { g.setBgColor("#FF0"); } + else if (current_value >= current_from) { g.setBgColor("#8F8"); } + g.clearRect(0,24,176,176); + + g.reset(); + g.setFontAlign(0, 0); + + g.setFont("Michroma36").drawString(timeToString(current_value), 88, 62); + + g.setFont("HaxorNarrow7x17"); + g.drawString(timeToString(current_from), 44, 62+26); + g.drawString(timeToString(current_mid), 88, 62+26); + g.drawString(timeToString(current_to), 132, 62+26); + + if (current_value >= current_from) { g.drawRect(44-1,62+26+9,44+1,62+26+9+1); } + if (current_value >= current_mid) { g.drawRect(88-1,62+26+9,88+1,62+26+9+1); } + if (current_value >= current_to) { g.drawRect(132-1,62+26+9,132+1,62+26+9+1); } + + if (showInstructions) { + g.setFont("6x8").drawString("Tapping timer locks buttons", 88, 100+5); + g.setFont("6x8").drawString("<= Swipe to change time =>", 88, 168); + } + + g.setColor(islocked ? "#444" : "#000"); + g.setFont("Michroma16"); + g.drawString(timeToString(newtimer_left_from), 44, 138-9); + g.drawString(timeToString(newtimer_left_to), 44, 138+9); + g.drawString(timeToString(newtimer_right_from), 132, 138-9); + g.drawString(timeToString(newtimer_right_to), 132, 138+9); + + g.drawRect(0+8,138-24, 88-9+1, 138+22+1); + g.drawRect(0+8,138-24, 88-9, 138+22); + g.drawRect(88+8,138-24, 176-10+1, 138+22+1); + g.drawRect(88+8,138-24, 176-10, 138+22); +} + +require("FontHaxorNarrow7x17").add(Graphics); +g.clear(); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +draw(); diff --git a/apps/a_speech_timer/app.png b/apps/a_speech_timer/app.png new file mode 100644 index 000000000..1eb777fa7 Binary files /dev/null and b/apps/a_speech_timer/app.png differ diff --git a/apps/a_speech_timer/screenshot0.png b/apps/a_speech_timer/screenshot0.png new file mode 100644 index 000000000..ee3ababc1 Binary files /dev/null and b/apps/a_speech_timer/screenshot0.png differ diff --git a/apps/a_speech_timer/screenshot1.png b/apps/a_speech_timer/screenshot1.png new file mode 100644 index 000000000..69ea91e95 Binary files /dev/null and b/apps/a_speech_timer/screenshot1.png differ diff --git a/apps/a_speech_timer/screenshot2.png b/apps/a_speech_timer/screenshot2.png new file mode 100644 index 000000000..fd511e0f6 Binary files /dev/null and b/apps/a_speech_timer/screenshot2.png differ diff --git a/apps/a_speech_timer/screenshot3.png b/apps/a_speech_timer/screenshot3.png new file mode 100644 index 000000000..7b67b6f01 Binary files /dev/null and b/apps/a_speech_timer/screenshot3.png differ diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index 03e920a9a..f5638fdd2 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -9,3 +9,4 @@ 0.09: Actual Bangle.js 1 pixels as of 13 Oct 2021 0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021) 0.11: Bangle.js2: New pixels, btn1 to exit +0.12: Actual pixels as of 29th Nov 2021 diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js index 32e5bafae..978d36193 100644 --- a/apps/about/app-bangle2.js +++ b/apps/about/app-bangle2.js @@ -6,7 +6,7 @@ var ENV = process.env; var MEM = process.memory(); var s = require("Storage"); -var img = atob("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"); +var img = atob("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n/2wAAABBCQAEGEmBJFoAABIoooxwgPoAddoCEkFFFAEwwAGwAHJIIJBAAAG2AjDAG/n+AABBBBAggXi3/xI13/////4AAABICQDEk0mBAFqABIHCSH2wkgw4FDCCHnAooAoGAAAAAwFttGAAAAGGAkkkkn/ttrADAAAAAX6nPhStv////+mmbYBBCQZY2AGAAACABIoqSookEAAADFCSEnbbbAFigAAAMEAFA2wAAA/q6gEkAA4AAFBBEAkgEHghJiR13////880cYBAIDLLAAAAAkCABAHCSHCAkBBHAJKCDbFArAAEEgkIMkAFAGAAAAvq6DE8AYGPOFAAEAggEUiOOIJbf////+mmbYAAABZZA2wEggiQQQAowoAQCBIHBICIDAFFYA2GEEENMEAFAGAQQYtqSw0kDDAHAFCCEYggcHgJxIAfb////840BJA9FALIAAAggkACCkEnAHACopB/JJJJDbFrDSww0AEAIwAFAkgSTZJmBwwgjbGIOFBJEYkgcAAH/BpbbAAADAAmIQHFoABHMkkEAggAAgkgggEgEDbG222PvvFdCAwwwBJJIAAAAAAQQZBgB2wgjbAAAADAAbADYAUg4FFkjAAHDgE0IQA9FAABMkgAAgEAAkEEABJJJDr2GG2PvvFAoSDADBJJAAGWAAAoABJgBwwAAAAAARP8gYYYYAWg4FIijbDDbkEmQSQAAAAEkkgAAAAgAAAAggAAgjb2222PvvbDbADYbBJJAACiAAAooBhk5YDH/CACCBP8gbADYAUg4BFkjDADDgk0gQQkgAAGAAAGABJg2AAHEHAAEAF2AA1PvvDbAYDDDYBAYAGWAAAoEkA/ADY//6CCARP8gSQAAAbbYAAtrADDDwAmgSQAkkkAYDAFGSIkGAAASkkAggA+228N//AYDDDAAbBAYAbYAAAov/gA/DY446HCCAJJFQFAEkAYAMgSrDDDDSQMEkB4AAwGADAouWIIGAAAW0kA44AFotBJASbaQYDAAGxYYYYYAAos//8HAYD446A6AQABFQFAEAgbYIESrDDAbQBIoQYEgGAAYAYFCWJBwAAASkkAIIkQDAADbabaAA2AAABbbYbYgAtt778AAAA//6/6CAABFQFAEAgYYIEtobAAgSIIGABAAAAGADDDH/JBAAAA4A4ABAigFAASTiaahABgdoBAAAAFtAAAn/gAAtoH/CQSAQIBFSVAEkAbJMgJIBJEgRAIqSA4gAAAYDYYfnAAAHHAAH/AIIkQBQAATjTThABgYqBAAA2ttoAAd8YJAAoA+2SWywBIFtFtEAgYYAA54BAggAAECABkgAAGADADH/AAAHHAAAAAAAigoDAATcccZhhgdqCH/AwssoAAbzYIAFAAGACGAQ4w4w4w4w4bYAAJIBBkkAAACSP/4AAAYAdvQQAAAAAGAAAA/AH+AEAATbjjYgggaCSJJA2ttoAAYYYIAAAA+GAGAwbbgEJMMAAAAAA2wBJAgAg5//J/4AAGAAF/CAAAAGWWWWLI/GH4AoAQQbbbAEAkiSA//AwllgAAYRYJAA3AH2AAGwbbAAG2wAG2wAAOIAAAAEEPJJJP8kkAEUlvCJIAAAwwwwLIAGJJGAACJASQYAAAACAIAA2EkAAAaIYBAA/AHFAAAADAAAwAG2wnOAAEAAAAX4E5/JJJ8nkFEigAC/4AAAwxwwLIAGJB4AAAIIAQb7AwASQYAAAgAkgARIEBAAA/4AAAAHHHEn8gA2AIgAAggAQAH4gAAAJJM/8tEkSAAtqSJAGAGAAAgAIhIEAAIOCG4GAAGFzbYuAgAgACIBtJAAA44EAA5IggEn8gGAwgIM3EACQAAEkAAbe20nkFEAQQ1wAQIIwxwwkgkkkACYAAIIQAYAwwwAQYGAIgAkgRABoAAAAAomgoxgkgEn8gADY5hg+EAAAFtAAH4YGSkkkAICCCGuCQIIAAAAoA222AAAAAJASQ4AG2AAozAGAgAAgIJItAAAAAAECA8gEAAAAAAoFQDPcggAQFFAQAADEkkngBxAQQA1wAJA23gJFAkkkAFwQAQAGAD7AwAQUEAoAkgkgFEkoGDtoooAWSACAQAAAAAHAQYAxggGwFtDbbAAYAE//AIACAAAAAJIOk/JAoAAgAHIUkQAAABJAAAGEgGMIAAAArsJtwztFFFFSSQSQQAkQ/9AoEAGGVQk1AAFroAbYAEngBxFtAA/HABBG4kISQ4EAAAAQQQCCFFotAFFsEtAAAAAFdEMAwDdoooAQSCCCQAmgJIbAkkG2qoG2wADrtoqSJEACAIFkoAAHABGOE4IAAAkkAAAiSgCCAIAIAAAAAAAAH//FoFFAGbbYAAAACACAQAUgSQSX4AGGqsE2ASDbvooABEATQbdkoI/HQAAACQAkkgAAAAAkEgAAFJANGG2GAYAAYHAQQFFAAwZJAAEgkBIAA444QQQX6SSSVUkxwXUgttqSBEAQQYdtBI/AAAAAQCw/X4w84AAgggAQFIINGGAGADADknAEAYAYwwZAH/EEEIAAABJASQSABAiBqsE2ISU8oooBBECACbdAOxICSAAAWyA6S4AigAAgAgAQFIBNGAGGADDD2nGEGD7AGAZBHPEEEIAAAAgAQAQQBkEBVUExwQUgooqSJMgFAYFFhJJSSQAAQCAkkgA84AAgAgCCFIAN2G2GwAYY03A2wXAH/6ZJH/EAEBIAAHHAQASABgkB"); var imgHeight = g.imageMetrics(img).height; var imgScroll = Math.floor(Math.random()*imgHeight); diff --git a/apps/about/bangle1-about-screenshot.png b/apps/about/bangle1-about-screenshot.png new file mode 100644 index 000000000..092f93dae Binary files /dev/null and b/apps/about/bangle1-about-screenshot.png differ diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index 5560f00bc..35fa0e386 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -1 +1,5 @@ 0.01: New App! +0.02: Remove messages on disconnect + Fix music control +0.03: Handling of message actions (ok/clear) +0.04: Android icon now goes to settings page with 'find phone' diff --git a/apps/android/app.js b/apps/android/app.js index b210886fd..9464d1b8b 100644 --- a/apps/android/app.js +++ b/apps/android/app.js @@ -1,2 +1,3 @@ -// Config app not implemented yet -setTimeout(()=>load("messages.app.js"),10); +Bangle.loadWidgets(); +Bangle.drawWidgets(); +eval(require("Storage").read("android.settings.js"))(()=>load()); diff --git a/apps/android/boot.js b/apps/android/boot.js index dd19f9500..97e3a5641 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -12,7 +12,7 @@ /* TODO: Call handling, fitness */ var HANDLERS = { // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add - "notify" : function() { event.t="add";require("messages").pushMessage(event); }, + "notify" : function() { Object.assign(event,{t:"add",positive:true, negative:true});require("messages").pushMessage(event); }, // {t:"notify~",id:int, title:string} // modified "notify~" : function() { event.t="modify";require("messages").pushMessage(event); }, // {t:"notify-",id:int} // remove @@ -33,7 +33,16 @@ // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num} "musicinfo" : function() { require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"})); - } + }, + // {"t":"call","cmd":"incoming/end","name":"Bob","number":"12421312"}) + "call" : function() { + Object.assign(event, { + t:event.cmd=="incoming"?"add":"remove", + id:"call", src:"Phone", + positive:true, negative:true, + title:event.name||"Call", body:"Incoming call\n"+event.number}); + require("messages").pushMessage(event); + }, }; var h = HANDLERS[event.t]; if (h) h(); else console.log("GB Unknown",event); @@ -42,6 +51,7 @@ // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); } NRF.on("connect", () => setTimeout(sendBattery, 2000)); + NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect setInterval(sendBattery, 10*60*1000); // Health tracking Bangle.on('health', health=>{ @@ -50,6 +60,12 @@ // Music control Bangle.musicControl = cmd => { // play/pause/next/previous/volumeup/volumedown - gbSend({ t: "music", m:cmd }); - } + gbSend({ t: "music", n:cmd }); + }; + // Message response + Bangle.messageResponse = (msg,response) => { + if (msg.id=="call") return gbSend({ t: "call", n:response?"ACCEPT":"REJECT" }); + if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS" }); + // error/warn here? + }; })(); diff --git a/apps/android/settings.js b/apps/android/settings.js new file mode 100644 index 000000000..d241397a4 --- /dev/null +++ b/apps/android/settings.js @@ -0,0 +1,18 @@ +(function(back) { + function gb(j) { + Bluetooth.println(JSON.stringify(j)); + } + var mainmenu = { + "" : { "title" : "Android" }, + "< Back" : back, + "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, + "Find Phone" : () => E.showMenu({ + "" : { "title" : "Find Phone" }, + "< Back" : ()=>E.showMenu(mainmenu), + "On" : _=>gb({t:"findPhone",n:true}), + "Off" : _=>gb({t:"findPhone",n:false}), + }), + "Messages" : ()=>load("messages.app.js") + }; + E.showMenu(mainmenu); +}) diff --git a/apps/authentiwatch/ChangeLog b/apps/authentiwatch/ChangeLog index 7b83706bf..50cf3fcea 100644 --- a/apps/authentiwatch/ChangeLog +++ b/apps/authentiwatch/ChangeLog @@ -1 +1,3 @@ +0.03: Add "Calculating" placeholder, update JSON save format +0.02: Fix JSON save format 0.01: First release diff --git a/apps/authentiwatch/README.md b/apps/authentiwatch/README.md index 403770c2b..8d0e74a0c 100644 --- a/apps/authentiwatch/README.md +++ b/apps/authentiwatch/README.md @@ -1,5 +1,8 @@ # Authentiwatch - 2FA Authenticator +* GitHub: https://github.com/andrewgoz/Authentiwatch <-- Report bugs here +* Bleeding edge AppLoader: https://andrewgoz.github.io/Authentiwatch/ + ## Supports * Google Authenticator compatible 2-factor authentication diff --git a/apps/authentiwatch/app-icon.js b/apps/authentiwatch/app-icon.js index 27ced695e..c901fb843 100644 --- a/apps/authentiwatch/app-icon.js +++ b/apps/authentiwatch/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mUywkBiIADCxoTFAAcQGBwY/DDQIKDBiMDDCgGCBI4YMGAIDFDCAFEBQwYLFgIYEGQgYMApoYJGAJjFMogYMSQgCDDBwDCY4oMEDBZgHHQQYQf4oYVBgwYQBogYPPYZpFDBKMEDAbdDCxT9IDYIFFABqSEAogySQYoWNFgrFDJZoQBJggYRBwhLGDBwyFDCZGEDCYAEDGrIMbwhnGDEpLGAwxlLFQgQDJiYoFDDAZDDCpMDMpQOCNxQYNBo4KKBpwYYBYJ8NeJgYkLBQY8UYQXVGQIwN")) +require("heatshrink").decompress(atob("mEwxH+AH4AD64ADFlgAFF04INFz4LUF0QwjEBwv/FzwwgF/4v/F6nMAAWi1AFD5nOeEHPEweoFooAB5/X5wvdFwotG5nN6/WAoQuaEoguHSYPQLwIIDF8uo5ouB6AJEFzuiFwup5/WFwI6GL0esXYKMBHYy9j1WqfBSOhBIYKJF8gAKF/4v6cZAvhGDAuWSDAvXMCwuYF+AwUFzX+0XGGAgxKFrYuBAAQxEeg4tcF4oABBQnGAAgv/F6b5KXsIvIGAqNnF/69fX8ZeSF7btNR8IuOF75ePL8ouOd74NKF8IANF94wEF1QAXA")) diff --git a/apps/authentiwatch/app.js b/apps/authentiwatch/app.js index 43eff4709..85c76b5d1 100644 --- a/apps/authentiwatch/app.js +++ b/apps/authentiwatch/app.js @@ -6,8 +6,13 @@ const algos = { "SHA256":{sha:crypto.SHA256,retsz:32,blksz:64 }, "SHA1" :{sha:crypto.SHA1 ,retsz:20,blksz:64 }, }; +const calculating = "Calculating"; +const notokens = "No tokens"; +const notsupported = "Not supported"; -var tokens = require("Storage").readJSON("authentiwatch.json", true) || []; +var settings = require("Storage").readJSON("authentiwatch.json", true) || {tokens:[],misc:{}}; +if (settings.data ) tokens = settings.data ; /* v0.02 settings */ +if (settings.tokens) tokens = settings.tokens; /* v0.03+ settings */ // QR Code Text // @@ -66,9 +71,8 @@ function do_hmac(key, message, algo) { var v = new DataView(ret, ret[ret.length - 1] & 0x0F, 4); return v.getUint32(0) & 0x7FFFFFFF; } -function hotp(token) { +function hotp(d, token, dohmac) { var tick; - var d = new Date(); if (token.period > 0) { // RFC6238 - timed var seconds = Math.floor(d.getTime() / 1000); @@ -81,15 +85,17 @@ function hotp(token) { var v = new DataView(msg.buffer); v.setUint32(0, tick >> 16 >> 16); v.setUint32(4, tick & 0xFFFFFFFF); - var ret = ""; - try { - var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); - ret = "" + hash % Math.pow(10, token.digits); - while (ret.length < token.digits) { - ret = "0" + ret; + var ret = calculating; + if (dohmac) { + try { + var hash = do_hmac(b32decode(token.secret), msg, token.algorithm.toUpperCase()); + ret = "" + hash % Math.pow(10, token.digits); + while (ret.length < token.digits) { + ret = "0" + ret; + } + } catch(err) { + ret = notsupported; } - } catch(err) { - ret = "Not supported"; } return {hotp:ret, next:((token.period > 0) ? ((tick + 1) * token.period * 1000) : d.getTime() + 30000)}; } @@ -109,7 +115,7 @@ function drawToken(id, r) { var y1 = r.y; var x2 = r.x + r.w - 1; var y2 = r.y + r.h - 1; - var adj; + var adj, sz; g.setClipRect(Math.max(x1, Bangle.appRect.x ), Math.max(y1, Bangle.appRect.y ), Math.min(x2, Bangle.appRect.x2), Math.min(y2, Bangle.appRect.y2)); if (id == state.curtoken) { @@ -129,7 +135,7 @@ function drawToken(id, r) { adj = (y1 + y2) / 2; } g.clearRect(x1, y1, x2, y2); - g.drawString(tokens[id].label, (x1 + x2) / 2, adj, false); + g.drawString(tokens[id].label.substr(0, 10), (x1 + x2) / 2, adj, false); if (id == state.curtoken) { if (tokens[id].period > 0) { // timed - draw progress bar @@ -143,7 +149,10 @@ function drawToken(id, r) { adj = 5; } // digits just below label - g.setFont("Vector", (state.otp.length > 8) ? 26 : 30); + sz = 30; + do { + g.setFont("Vector", sz--); + } while (g.stringWidth(state.otp) > (r.w - adj)); g.drawString(state.otp, (x1 + x2) / 2 + adj, y1 + 16, false); } // shaded lines top and bottom @@ -157,6 +166,9 @@ function draw() { var d = new Date(); if (state.curtoken != -1) { var t = tokens[state.curtoken]; + if (state.otp == calculating) { + state.otp = hotp(d, t, true).hotp; + } if (d.getTime() > state.nextTime) { if (state.hide == 0) { // auto-hide the current token @@ -167,7 +179,7 @@ function draw() { state.nextTime = 0; } else { // time to generate a new token - var r = hotp(t); + var r = hotp(d, t, state.otp != ""); state.nextTime = r.next; state.otp = r.hotp; if (t.period <= 0) { @@ -195,7 +207,13 @@ function draw() { if (state.drawtimer) { clearTimeout(state.drawtimer); } - state.drawtimer = setTimeout(draw, (tokens[state.curtoken].period > 0) ? 1000 : state.nexttime - d.getTime()); + var dly; + if (tokens[state.curtoken].period > 0) { + dly = (state.otp == calculating) ? 1 : 1000; + } else { + dly = state.nexttime - d.getTime(); + } + state.drawtimer = setTimeout(draw, dly); if (tokens[state.curtoken].period <= 0) { state.hide = 0; } @@ -210,7 +228,7 @@ function draw() { } else { g.setFont("Vector", 30); g.setFontAlign(0, 0, 0); - g.drawString("No tokens", Bangle.appRect.x + Bangle.appRect.w / 2,Bangle.appRect.y + Bangle.appRect.h / 2, false); + g.drawString(notokens, Bangle.appRect.x + Bangle.appRect.w / 2, Bangle.appRect.y + Bangle.appRect.h / 2, false); } } @@ -231,6 +249,7 @@ function onTouch(zone, e) { if (y > Bangle.appRect.h) { state.listy += (y - Bangle.appRect.h); } + state.otp = ""; } state.nextTime = 0; state.curtoken = id; @@ -257,8 +276,10 @@ function onSwipe(e) { } if (e == -1 && state.curtoken != -1 && tokens[state.curtoken].period <= 0) { tokens[state.curtoken].period--; - require("Storage").writeJSON("authentiwatch.json", tokens); + let newsettings={tokens:tokens,misc:settings.misc}; + require("Storage").writeJSON("authentiwatch.json", newsettings); state.nextTime = 0; + state.otp = ""; state.hide = 2; draw(); } diff --git a/apps/authentiwatch/app.png b/apps/authentiwatch/app.png index 208fb63b3..8775d3e40 100644 Binary files a/apps/authentiwatch/app.png and b/apps/authentiwatch/app.png differ diff --git a/apps/authentiwatch/interface.html b/apps/authentiwatch/interface.html index 12c0c1d8d..26533b17b 100644 --- a/apps/authentiwatch/interface.html +++ b/apps/authentiwatch/interface.html @@ -35,8 +35,9 @@ const otpAuthUrl = 'otpauth://'; const tokentypes = ['TOTP (Timed)', 'HOTP (Counter)']; -/* Array of TOTP tokens */ -var tokens=[]; +/* Settings */ +var settings = {tokens:[], misc:{}}; +var tokens = settings.tokens; /* Remove any non-base-32 characters from the given string and collapses * whitespace to a single space. Optionally removes all whitespace from @@ -261,6 +262,7 @@ qrcode.callback = res => { scanning = false; editToken(parseInt(document.forms['edittoken'].elements['tokenid'].value)); t['label'] = (t['issuer'] == '') ? t['account'] : t['issuer'] + ' (' + t['account'] + ')'; + t['label'] = t['label'].substr(0, 10); var fe = document.forms['edittoken'].elements; if (res.startsWith(otpAuthUrl + 'hotp/')) { t['period'] = '30'; @@ -319,21 +321,21 @@ function doScan() { */ function loadTokens() { Util.showModal('Loading...'); - Puck.eval(`require('Storage').read(${JSON.stringify('authentiwatch.json')})`,data=>{ + Puck.eval(`require('Storage').readJSON(${JSON.stringify('authentiwatch.json')})`,data=>{ Util.hideModal(); - try { - tokens = JSON.parse(data); - updateTokens(); - } catch { - tokens = []; - } + if (data.data ) settings.tokens = data.data ; /* v0.02 settings */ + if (data.tokens) settings.tokens = data.tokens; /* v0.03+ settings */ + if (data.misc ) settings.misc = data.misc ; /* v0.03+ settings */ + tokens = settings.tokens; + updateTokens(); }); } /* Save settings as a JSON file on the watch. */ function saveTokens() { Util.showModal('Saving...'); - Puck.write(`\x10require('Storage').write(${JSON.stringify('authentiwatch.json')},${JSON.stringify(tokens)})\n`,()=>{ + let newsettings={tokens:tokens,misc:settings.misc}; + Puck.write(`\x10require('Storage').writeJSON(${JSON.stringify('authentiwatch.json')},${JSON.stringify(newsettings)})\n`,()=>{ Util.hideModal(); }); } diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index c56967d3d..316660fc6 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -5,4 +5,5 @@ 0.05: Clock does not start if app Languages is not installed 0.06: Improve accuracy 0.07: Update to use Bangle.setUI instead of setWatch -0.08: Use theme colors, Layout library \ No newline at end of file +0.08: Use theme colors, Layout library +0.09: Fix time/date disappearing after fullscreen notification diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index 2c6d66e45..5d46a1cb4 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -24,7 +24,7 @@ function renderBar(l) { return; } const width = this.fraction*l.w; - g.fillRect(l.x, l.y, width-1, l.y+l.height-1); + g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1); } const Layout = require("Layout"); @@ -78,7 +78,7 @@ function dateText(date) { return `${dayName} ${dayMonth}`; } -draw = function draw() { +draw = function draw(force) { if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled const date = new Date(); layout.time.label = timeText(date); @@ -86,6 +86,10 @@ draw = function draw() { layout.date.label = dateText(date); const SECONDS_PER_MINUTE = 60; layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE; + if (force) { + Bangle.drawWidgets(); + layout.forgetLazyState(); + } layout.render(); // schedule update at start of next second const millis = date.getMilliseconds(); @@ -96,7 +100,7 @@ draw = function draw() { Bangle.setUI("clock"); Bangle.on("lcdPower", function(on) { if (on) { - draw(); + draw(true); } }); g.reset().clear(); diff --git a/apps/battleship/bangle1-battle-ship-screenshot.png b/apps/battleship/bangle1-battle-ship-screenshot.png new file mode 100644 index 000000000..56225b32d Binary files /dev/null and b/apps/battleship/bangle1-battle-ship-screenshot.png differ diff --git a/apps/bclock/bangle1-binary-clock-screenshot.png b/apps/bclock/bangle1-binary-clock-screenshot.png new file mode 100644 index 000000000..bc7ce611b Binary files /dev/null and b/apps/bclock/bangle1-binary-clock-screenshot.png differ diff --git a/apps/beebclock/bangle1-beeb-clock-screenshot.png b/apps/beebclock/bangle1-beeb-clock-screenshot.png new file mode 100644 index 000000000..00cb92e5c Binary files /dev/null and b/apps/beebclock/bangle1-beeb-clock-screenshot.png differ diff --git a/apps/berlinc/berlin-clock-screenshot.png b/apps/berlinc/berlin-clock-screenshot.png new file mode 100644 index 000000000..92a4c7928 Binary files /dev/null and b/apps/berlinc/berlin-clock-screenshot.png differ diff --git a/apps/binwatch/Background176_center.img b/apps/binwatch/Background176_center.img new file mode 100644 index 000000000..4d4b587de Binary files /dev/null and b/apps/binwatch/Background176_center.img differ diff --git a/apps/binwatch/Background240_center.img b/apps/binwatch/Background240_center.img new file mode 100644 index 000000000..abf95107d Binary files /dev/null and b/apps/binwatch/Background240_center.img differ diff --git a/apps/binwatch/Background240_center.png b/apps/binwatch/Background240_center.png index 6fa35f93f..c2b108f4d 100644 Binary files a/apps/binwatch/Background240_center.png and b/apps/binwatch/Background240_center.png differ diff --git a/apps/binwatch/ChangeLog b/apps/binwatch/ChangeLog index bf4f5075a..1e54f489c 100644 --- a/apps/binwatch/ChangeLog +++ b/apps/binwatch/ChangeLog @@ -1,3 +1,4 @@ 0.01: start of development 0.02: first running version for BangleJs2 0.03: corrected icon, added screen shot, extended description +0.04: corrected format of background image (raw binary) diff --git a/apps/binwatch/app.js b/apps/binwatch/app.js index 56e153dbf..28d7a06a5 100644 --- a/apps/binwatch/app.js +++ b/apps/binwatch/app.js @@ -12,7 +12,6 @@ require("Font7x11Numeric7Seg").add(Graphics); require("Font5x7Numeric7Seg").add(Graphics); - /* constants and definitions */ /* Bangle 2: 176 x 176 */ @@ -63,7 +62,7 @@ const V2_BAT_SIZE_Y = 2; const V2_SCREEN_SIZE_X = 176; const V2_SCREEN_SIZE_Y = 176; -const V2_BACKGROUND_IMAGE = "Background176_center.png"; +const V2_BACKGROUND_IMAGE = "binwatch.bg176.img"; const V2_BG_COLOR = 0; const V2_FG_COLOR = 1; @@ -91,7 +90,7 @@ const V1_BAT_SIZE_X = 3; const V1_BAT_SIZE_Y = 5; const V1_SCREEN_SIZE_X = 240; const V1_SCREEN_SIZE_Y = 240; -const V1_BACKGROUND_IMAGE = "Background240_center.png"; +const V1_BACKGROUND_IMAGE = "binwatch.bg240.img"; const V1_BG_COLOR = 1; const V1_FG_COLOR = 0; @@ -293,7 +292,7 @@ function setRuntimeValues(resolution) { bat_size_x = V1_BAT_SIZE_X; bat_size_y = V1_BAT_SIZE_Y; - setWatch(toggleDateTime, BTN1, { repeat : true, edge: "falling"}); + setWatch(toggleDateTime, BTN1, { repeat : true, edge: "falling"}); } else { x_step = V2_X_STEP; @@ -362,8 +361,7 @@ function draw() { updateVTime(); g.clear(); g.drawImages([{image:cgimg}, - {image:require("Storage").read(backgroundImage)}, -// { x:bt_x, y:bt_y, rotate: 0, image:require("Storage").read("bt-icon.png")}, + {image:require("Storage").read(backgroundImage)} ]); drawBT(g, NRF.getSecurityStatus().connected); // Bangle.drawWidgets(); diff --git a/apps/blackjack/bangle1-black-jack-game-screenshot.png b/apps/blackjack/bangle1-black-jack-game-screenshot.png new file mode 100644 index 000000000..532b784f4 Binary files /dev/null and b/apps/blackjack/bangle1-black-jack-game-screenshot.png differ diff --git a/apps/blobclk/bangle1-large-digit-blob-clock-screenshot.png b/apps/blobclk/bangle1-large-digit-blob-clock-screenshot.png new file mode 100644 index 000000000..fcad01e50 Binary files /dev/null and b/apps/blobclk/bangle1-large-digit-blob-clock-screenshot.png differ diff --git a/apps/blobclk/bangle2-large-digit-blob-clock-screenshot.png b/apps/blobclk/bangle2-large-digit-blob-clock-screenshot.png new file mode 100644 index 000000000..5cf48bda7 Binary files /dev/null and b/apps/blobclk/bangle2-large-digit-blob-clock-screenshot.png differ diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 98f80efd9..ffc2be495 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -40,3 +40,4 @@ 0.35: Add Bangle.appRect polyfill Don't set beep vibration up on Bangle.js 2 (built in) 0.36: Add comments to .boot0 to make debugging a bit easier +0.37: Remove Quiet Mode settings: now handled by Quiet Mode Schedule app diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index d642426c2..daf311fe6 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -78,13 +78,7 @@ boot += `E.on('errorFlag', function(errorFlags) { if (global.save) boot += `global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }\n`; // Apply any settings-specific stuff if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; -if (s.quiet && s.qmOptions) boot+=`Bangle.setOptions(${E.toJS(s.qmOptions)});\n`; -if (s.quiet && s.qmBrightness) { - if (s.qmBrightness!=1) boot+=`Bangle.setLCDBrightness(${s.qmBrightness});\n`; -} else { - if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; -} -if (s.quiet && s.qmTimeout) boot+=`Bangle.setLCDTimeout(${s.qmTimeout});\n`; +if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${s.passkey}, mitm:1, display:1});\n`; if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; // Pre-2v10 firmwares without a theme/setUI diff --git a/apps/chargeanim/bangle-charge-animation-screenshot.png b/apps/chargeanim/bangle-charge-animation-screenshot.png new file mode 100644 index 000000000..83ef1dbda Binary files /dev/null and b/apps/chargeanim/bangle-charge-animation-screenshot.png differ diff --git a/apps/chargeanim/bangle2-charge-animation-screenshot.png b/apps/chargeanim/bangle2-charge-animation-screenshot.png new file mode 100644 index 000000000..c3fb7c8c8 Binary files /dev/null and b/apps/chargeanim/bangle2-charge-animation-screenshot.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot1.png b/apps/choozi/bangle1-choozi-screenshot1.png new file mode 100644 index 000000000..104024958 Binary files /dev/null and b/apps/choozi/bangle1-choozi-screenshot1.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot2.png b/apps/choozi/bangle1-choozi-screenshot2.png new file mode 100644 index 000000000..f3b6868bf Binary files /dev/null and b/apps/choozi/bangle1-choozi-screenshot2.png differ diff --git a/apps/cliclockJS2Enhanced/ChangeLog b/apps/cliclockJS2Enhanced/ChangeLog new file mode 100644 index 000000000..c7cb9e2c6 --- /dev/null +++ b/apps/cliclockJS2Enhanced/ChangeLog @@ -0,0 +1,2 @@ +0.01: Submitted to App Loader +0.02: Removed unneded code, added HID controlls thanks to t0m1o1 for his code :p diff --git a/apps/cliclockJS2Enhanced/app.js b/apps/cliclockJS2Enhanced/app.js index 314e32375..70e86f3d6 100644 --- a/apps/cliclockJS2Enhanced/app.js +++ b/apps/cliclockJS2Enhanced/app.js @@ -4,24 +4,96 @@ var fontsizeTime = g.getWidth()>200 ? 4 : 4; var fontheight = 10*fontsize; var fontheightTime = 10*fontsizeTime; var locale = require("locale"); -var marginTop = 40; +var marginTop = 25; var flag = false; -var hrtOn = false; -var hrtStr = "Hrt: ??? bpm"; +var storage = require('Storage'); -const NONE_MODE = "none"; -const ID_MODE = "id"; -const VER_MODE = "ver"; -const BATT_MODE = "batt"; -const MEM_MODE = "mem"; -const STEPS_MODE = "step"; -const HRT_MODE = "hrt"; -const NONE_FN_MODE = "no_fn"; -const HRT_FN_MODE = "fn_hrt"; +const settings = storage.readJSON('setting.json',1) || { HID: false }; + +var sendHid, next, prev, toggle, up, down, profile; +var lasty = 0; +var lastx = 0; + +if (settings.HID=="kbmedia") { + profile = 'Music'; + sendHid = function (code, cb) { + try { + NRF.sendHIDReport([1,code], () => { + NRF.sendHIDReport([1,0], () => { + if (cb) cb(); + }); + }); + } catch(e) { + print(e); + } + }; + next = function (cb) { sendHid(0x01, cb); }; + prev = function (cb) { sendHid(0x02, cb); }; + toggle = function (cb) { sendHid(0x10, cb); }; + up = function (cb) {sendHid(0x40, cb); }; + down = function (cb) { sendHid(0x80, cb); }; +} else { + E.showPrompt("Enable HID?",{title:"HID disabled"}).then(function(enable) { + if (enable) { + settings.HID = "kbmedia"; + require("Storage").write('setting.json', settings); + setTimeout(load, 1000, "hidmsicswipe.app.js"); + } else setTimeout(load, 1000); + }); +} + +if (next) { + setWatch(function(e) { + var len = e.time - e.lastTime; + E.showMessage('lock'); + setTimeout(drawApp, 1000); + Bangle.setLocked(true); + }, BTN1, { edge:"falling",repeat:true,debounce:50}); + Bangle.on('drag', function(e) { + if(!e.b){ + console.log(lasty); + console.log(lastx); + if(lasty > 40){ + writeLine('Down', 3); + // setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"volumedown"})); + down(() => {}); + } + else if(lasty < -40){ + writeLine('Up', 3); + // setTimeout(drawApp, 1000); + //Bluetooth.println(JSON.stringify({t:"music", n:"volumeup"})); + + up(() => {}); + } else if(lastx < -40){ + writeLine('Prev', 3); + // setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"previous"})); + prev(() => {}); + } else if(lastx > 40){ + writeLine('Next', 3); + // setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"next"})); + next(() => {}); + } else if(lastx==0 && lasty==0){ + writeLine('play/pause', 3); + //setTimeout(drawApp, 1000); + // Bluetooth.println(JSON.stringify({t:"music", n:"play"})); + + toggle(() => {}); + } + lastx = 0; + lasty = 0; + } + else{ + lastx = lastx + e.dx; + lasty = lasty + e.dy; + } + }); + +} -let infoMode = NONE_MODE; -let functionMode = NONE_FN_MODE; let textCol = g.theme.dark ? "#0f0" : "#080"; @@ -33,13 +105,12 @@ function drawAll(){ function updateRest(now){ writeLine(locale.dow(now),1); writeLine(locale.date(now,1),2); - drawInfo(5); } function updateTime(){ if (!Bangle.isLCDOn()) return; let now = new Date(); writeLine(locale.time(now,1),0); - writeLine(flag?" ":"_",3); + writeLine(flag?" ":"_ ",3); flag = !flag; if(now.getMinutes() == 0) updateRest(now); @@ -65,142 +136,13 @@ function writeLine(str,line){ var y = marginTop+(line-1)*fontheight+fontheightTime; g.setFont("6x8",fontsize); g.setColor(textCol).setFontAlign(-1,-1); - g.clearRect(0,y,((str.length+1)*20),y+fontheight-1); + g.clearRect(0,y,((str.length+10)*40),y+fontheightTime-1); writeLineStart(line); g.drawString(str,25,y); } } -function drawInfo(line) { - let val; - let str = ""; - let col = textCol; // green - - //console.log("drawInfo(), infoMode=" + infoMode + " funcMode=" + functionMode); - - switch(functionMode) { - case NONE_FN_MODE: - break; - case HRT_FN_MODE: - col = g.theme.dark ? "#0ff": "#088"; // cyan - str = "HRM: " + (hrtOn ? "ON" : "OFF"); - drawModeLine(line,str,col); - return; - } - - switch(infoMode) { - case NONE_MODE: - col = g.theme.bg; - str = ""; - break; - case HRT_MODE: - str = hrtStr; - break; - case STEPS_MODE: - str = "Steps: " + stepsWidget().getSteps(); - break; - case ID_MODE: - val = NRF.getAddress().split(":"); - str = "Id: " + val[4] + val[5]; - break; - case VER_MODE: - str = "Fw: " + process.env.VERSION; - break; - case MEM_MODE: - val = process.memory(); - str = "Memory: " + Math.round(val.usage*100/val.total) + "%"; - break; - case BATT_MODE: - default: - str = "Battery: " + E.getBattery() + "%"; - } - - drawModeLine(line,str,col); -} - -function drawModeLine(line, str, col) { - g.setColor(col); - var y = marginTop+line*fontheight; - g.fillRect(0, y, 239, y+fontheight-1); - g.setColor(g.theme.bg).setFontAlign(0, 0); - g.drawString(str, g.getWidth()/2, y+fontheight/2); -} - -function changeInfoMode() { - switch(functionMode) { - case NONE_FN_MODE: - break; - case HRT_FN_MODE: - hrtOn = !hrtOn; - Bangle.buzz(); - Bangle.setHRMPower(hrtOn ? 1 : 0); - if (hrtOn) infoMode = HRT_MODE; - return; - } - - switch(infoMode) { - case NONE_MODE: - if (stepsWidget() !== undefined) - infoMode = hrtOn ? HRT_MODE : STEPS_MODE; - else - infoMode = VER_MODE; - break; - case HRT_MODE: - if (stepsWidget() !== undefined) - infoMode = STEPS_MODE; - else - infoMode = VER_MODE; - break; - case STEPS_MODE: - infoMode = ID_MODE; - break; - case ID_MODE: - infoMode = VER_MODE; - break; - case VER_MODE: - infoMode = BATT_MODE; - break; - case BATT_MODE: - infoMode = MEM_MODE; - break; - case MEM_MODE: - default: - infoMode = NONE_MODE; - } -} - -function changeFunctionMode() { - //console.log("changeFunctionMode()"); - switch(functionMode) { - case NONE_FN_MODE: - functionMode = HRT_FN_MODE; - break; - case HRT_FN_MODE: - default: - functionMode = NONE_FN_MODE; - } - //console.log(functionMode); - -} - -function stepsWidget() { - if (WIDGETS.activepedom !== undefined) { - return WIDGETS.activepedom; - } else if (WIDGETS.wpedom !== undefined) { - return WIDGETS.wpedom; - } - return undefined; -} - -Bangle.on('HRM', function(hrm) { - if(hrm.confidence > 90){ - hrtStr = "Hrt: " + hrm.bpm + " bpm"; - } else { - hrtStr = "Hrt: ??? bpm"; - } -}); - g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -211,6 +153,5 @@ Bangle.on('lcdPower',function(on) { var click = setInterval(updateTime, 1000); // Show launcher when button pressed Bangle.setUI("clockupdown", btn=>{ - if (btn<0) changeInfoMode(); drawAll(); }); diff --git a/apps/clotris/bangle1-clock-tris-screenshot.png b/apps/clotris/bangle1-clock-tris-screenshot.png new file mode 100644 index 000000000..4b7a7257f Binary files /dev/null and b/apps/clotris/bangle1-clock-tris-screenshot.png differ diff --git a/apps/counter/bangle1-counter-screenshot.png b/apps/counter/bangle1-counter-screenshot.png new file mode 100644 index 000000000..1d6c471bf Binary files /dev/null and b/apps/counter/bangle1-counter-screenshot.png differ diff --git a/apps/cprassist/bangle1-CPR-assist-screenshot.png b/apps/cprassist/bangle1-CPR-assist-screenshot.png new file mode 100644 index 000000000..9d217efce Binary files /dev/null and b/apps/cprassist/bangle1-CPR-assist-screenshot.png differ diff --git a/apps/cscsensor/ChangeLog b/apps/cscsensor/ChangeLog index 9af9f9926..8f23fa9f3 100644 --- a/apps/cscsensor/ChangeLog +++ b/apps/cscsensor/ChangeLog @@ -3,3 +3,5 @@ 0.03: Save total distance traveled 0.04: Add sensor battery level indicator 0.05: Add cadence sensor support +0.06: Now read wheel rev as well as cadence sensor + Improve connection code diff --git a/apps/cscsensor/README.md b/apps/cscsensor/README.md index e19ebe60e..9740fd9cf 100644 --- a/apps/cscsensor/README.md +++ b/apps/cscsensor/README.md @@ -9,10 +9,16 @@ Currently the app displays the following data: - maximum speed - trip distance traveled - total distance traveled -- an icon with the battery status of the remote sensor +- an icon with the battery status of the remote sensor Button 1 resets all measurements except total distance traveled. The latter gets preserved by being written to storage every 0.1 miles and upon exiting the app. If the watch app has not received an update from the sensor for at least 10 seconds, pushing button 3 will attempt to reconnect to the sensor. Button 2 switches between the display for cycling speed and cadence. Values displayed are imperial or metric (depending on locale), cadence is in RPM, the wheel circumference can be adjusted in the global settings app. + +# TODO + +* Use Layout Library to provide proper Bangle.js 2 support +* Turn CSC sensor support into a library +* Support for `Recorder` app, to allow CSC readings to be logged alongside GPS diff --git a/apps/cscsensor/cscsensor.app.js b/apps/cscsensor/cscsensor.app.js index 3d4120269..e2af0db16 100644 --- a/apps/cscsensor/cscsensor.app.js +++ b/apps/cscsensor/cscsensor.app.js @@ -5,6 +5,8 @@ var characteristic; const SETTINGS_FILE = 'cscsensor.json'; const storage = require('Storage'); +const W = g.getWidth(); +const H = g.getHeight(); class CSCSensor { constructor() { @@ -75,7 +77,7 @@ class CSCSensor { var dist = this.distFactor*(this.lastRevs-this.lastRevsStart)*this.wheelCirc/63360.0; var ddist = Math.round(100*dist)/100; var tdist = Math.round(this.distFactor*this.totaldist*10)/10; - var dspeed = Math.round(10*this.distFactor*this.speed)/10; + var dspeed = Math.round(10*this.distFactor*this.speed)/10; var dmins = Math.floor(this.movingTime/60).toString(); if (dmins.length<2) dmins = "0"+dmins; var dsecs = (Math.floor(this.movingTime) % 60).toString(); @@ -152,7 +154,7 @@ class CSCSensor { var qChanged = false; if (event.target.uuid == "0x2a5b") { if (event.target.value.getUint8(0, true) & 0x2) { - // crank revolution + // crank revolution - if enabled const crankRevs = event.target.value.getUint16(1, true); const crankTime = event.target.value.getUint16(3, true); if (crankTime > this.lastCrankTime) { @@ -161,44 +163,43 @@ class CSCSensor { } this.lastCrankRevs = crankRevs; this.lastCrankTime = crankTime; - } else { - // wheel revolution - var wheelRevs = event.target.value.getUint32(1, true); - var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); - if (dRevs>0) { - qChanged = true; - this.totaldist += dRevs*this.wheelCirc/63360.0; - if ((this.totaldist-this.settings.totaldist)>0.1) { - this.settings.totaldist = this.totaldist; - storage.writeJSON(SETTINGS_FILE, this.settings); - } - } - this.lastRevs = wheelRevs; - if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; - var wheelTime = event.target.value.getUint16(5, true); - var dT = (wheelTime-this.lastTime)/1024; - var dBT = (Date.now()-this.lastBangleTime)/1000; - this.lastBangleTime = Date.now(); - if (dT<0) dT+=64; - if (Math.abs(dT-dBT)>3) dT = dBT; - this.lastTime = wheelTime; - this.speed = this.lastSpeed; - if (dRevs>0 && dT>0) { - this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; - this.speedFailed = 0; - this.movingTime += dT; - } - else { - this.speedFailed++; - qChanged = false; - if (this.speedFailed>3) { - this.speed = 0; - qChanged = (this.lastSpeed>0); - } - } - this.lastSpeed = this.speed; - if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; } + // wheel revolution + var wheelRevs = event.target.value.getUint32(1, true); + var dRevs = (this.lastRevs>0 ? wheelRevs-this.lastRevs : 0); + if (dRevs>0) { + qChanged = true; + this.totaldist += dRevs*this.wheelCirc/63360.0; + if ((this.totaldist-this.settings.totaldist)>0.1) { + this.settings.totaldist = this.totaldist; + storage.writeJSON(SETTINGS_FILE, this.settings); + } + } + this.lastRevs = wheelRevs; + if (this.lastRevsStart<0) this.lastRevsStart = wheelRevs; + var wheelTime = event.target.value.getUint16(5, true); + var dT = (wheelTime-this.lastTime)/1024; + var dBT = (Date.now()-this.lastBangleTime)/1000; + this.lastBangleTime = Date.now(); + if (dT<0) dT+=64; + if (Math.abs(dT-dBT)>3) dT = dBT; + this.lastTime = wheelTime; + this.speed = this.lastSpeed; + if (dRevs>0 && dT>0) { + this.speed = (dRevs*this.wheelCirc/63360.0)*3600/dT; + this.speedFailed = 0; + this.movingTime += dT; + } + else { + this.speedFailed++; + qChanged = false; + if (this.speedFailed>3) { + this.speed = 0; + qChanged = (this.lastSpeed>0); + } + } + this.lastSpeed = this.speed; + if (this.speed>this.maxSpeed && (this.movingTime>3 || this.speed<20) && this.speed<50) this.maxSpeed = this.speed; } if (qChanged && this.qUpdateScreen) this.updateScreen(); } @@ -215,44 +216,47 @@ function getSensorBatteryLevel(gatt) { }); } -function parseDevice(d) { - device = d; - g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Found device", 120, 120).flip(); - device.gatt.connect().then(function(ga) { - gatt = ga; - g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Connected", 120, 120).flip(); - return gatt.getPrimaryService("1816"); -}).then(function(s) { - service = s; - return service.getCharacteristic("2a5b"); -}).then(function(c) { - characteristic = c; - characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); - return characteristic.startNotifications(); -}).then(function() { - console.log("Done!"); - g.clearRect(0, 60, 239, 239).setColor(1, 1, 1).flip(); - getSensorBatteryLevel(gatt); - mySensor.updateScreen(); -}).catch(function(e) { - g.clearRect(0, 60, 239, 239).setColor(1, 0, 0).setFontAlign(0, 0, 0).drawString("ERROR"+e, 120, 120).flip(); - console.log(e); -})} - function connection_setup() { - NRF.setScan(); mySensor.screenInit = true; - NRF.setScan(parseDevice, { filters: [{services:["1816"]}], timeout: 2000}); - g.clearRect(0, 48, 239, 239).setFontVector(18).setFontAlign(0, 0, 0).setColor(0, 1, 0); - g.drawString("Scanning for CSC sensor...", 120, 120); + E.showMessage("Scanning for CSC sensor..."); + NRF.requestDevice({ filters: [{services:["1816"]}]}).then(function(d) { + device = d; + E.showMessage("Found device"); + return device.gatt.connect(); + }).then(function(ga) { + gatt = ga; + E.showMessage("Connected"); + return gatt.getPrimaryService("1816"); + }).then(function(s) { + service = s; + return service.getCharacteristic("2a5b"); + }).then(function(c) { + characteristic = c; + characteristic.on('characteristicvaluechanged', (event)=>mySensor.updateSensor(event)); + return characteristic.startNotifications(); + }).then(function() { + console.log("Done!"); + g.reset().clearRect(Bangle.appRect).flip(); + getSensorBatteryLevel(gatt); + mySensor.updateScreen(); + }).catch(function(e) { + E.showMessage(e.toString(), "ERROR"); + console.log(e); + }); } connection_setup(); -setWatch(function() { mySensor.reset(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN1, {repeat:true, debounce:20}); -E.on('kill',()=>{ if (gatt!=undefined) gatt.disconnect(); mySensor.settings.totaldist = mySensor.totaldist; storage.writeJSON(SETTINGS_FILE, mySensor.settings); }); -setWatch(function() { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); }, BTN3, {repeat:true, debounce:20}); -setWatch(function() { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, 239, 239); mySensor.updateScreen(); }, BTN2, {repeat:true, debounce:20}); -NRF.on('disconnect', connection_setup); +E.on('kill',()=>{ + if (gatt!=undefined) gatt.disconnect(); + mySensor.settings.totaldist = mySensor.totaldist; + storage.writeJSON(SETTINGS_FILE, mySensor.settings); +}); +NRF.on('disconnect', connection_setup); // restart if disconnected +Bangle.setUI("updown", d=>{ + if (d<0) { mySensor.reset(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); } + if (d==0) { if (Date.now()-mySensor.lastBangleTime>10000) connection_setup(); } + if (d>0) { mySensor.toggleDisplayCadence(); g.clearRect(0, 48, W, H); mySensor.updateScreen(); } +}); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/ctrclk/bangle1-center-clock-screenshot.png b/apps/ctrclk/bangle1-center-clock-screenshot.png new file mode 100644 index 000000000..613fa4fb5 Binary files /dev/null and b/apps/ctrclk/bangle1-center-clock-screenshot.png differ diff --git a/apps/cubescramble/ChangeLog b/apps/cubescramble/ChangeLog index 6de5b7211..46852864a 100644 --- a/apps/cubescramble/ChangeLog +++ b/apps/cubescramble/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial Release 0.02: Replace icon with one found on https://icons8.com 0.03: Re-render icon fixing display in settings +0.04: Improved UX and display solve time diff --git a/apps/cubescramble/README.md b/apps/cubescramble/README.md index 779e32489..1c1603372 100644 --- a/apps/cubescramble/README.md +++ b/apps/cubescramble/README.md @@ -1,12 +1,11 @@ # Cube Scramble -A random scramble generator for the 3x3 Rubik's cube +A random scramble generator for the 3x3 Rubik's cube with a basic timer. ## Future features I'm keen to complete this project with -* Add a timer * Add the ability for times to be stored and exported ## Requests diff --git a/apps/cubescramble/bangle1-cube-scramble-screenshot.png b/apps/cubescramble/bangle1-cube-scramble-screenshot.png new file mode 100644 index 000000000..5a35238e3 Binary files /dev/null and b/apps/cubescramble/bangle1-cube-scramble-screenshot.png differ diff --git a/apps/cubescramble/bangle2-cube-scramble-screenshot.png b/apps/cubescramble/bangle2-cube-scramble-screenshot.png new file mode 100644 index 000000000..ae37b4aff Binary files /dev/null and b/apps/cubescramble/bangle2-cube-scramble-screenshot.png differ diff --git a/apps/cubescramble/cube-scramble.js b/apps/cubescramble/cube-scramble.js index c0b1d11c3..73c4e95ef 100644 --- a/apps/cubescramble/cube-scramble.js +++ b/apps/cubescramble/cube-scramble.js @@ -1,4 +1,3 @@ - // Scramble code from: https://raw.githubusercontent.com/bjcarlson42/blog-post-sample-code/master/Rubik's%20Cube%20JavaScript%20Scrambler/part_two.js const makeScramble = () => { const options = ["F", "F2", "F'", "R", "R2", "R'", "U", "U2", "U'", "B", "B2", "B'", "L", "L2", "L'", "D", "D2", "D'"]; @@ -59,16 +58,36 @@ const getRandomInt = max => Math.floor(Math.random() * Math.floor(max)); // retu const getRandomIntBetween = (min, max) => Math.floor(Math.random() * (max - min) + min); const presentScramble = () => { - g.clear(); - E.showMessage(makeScramble().join(" ")); + showPrompt(makeScramble().join(" "), { + buttons: {"solve": true, "reset": false} + }).then((v) => { + if (v) { + const start = new Date(); + showPrompt(" ", { + buttons: {"stop": true} + }).then(() => { + const time = parseFloat(((new Date()).getTime() - start.getTime()) / 1000); + showPrompt(String(time.toFixed(3)), { + buttons: {"next": true} + }).then(() => { + presentScramble(); + }); + }); + } else { + presentScramble(); + } + }); +}; + +const showPrompt = (text, options = {}) => { + options.title = options.title || "cube scramble"; + return E.showPrompt(text, options); }; const init = () => { + Bangle.setLCDTimeout(0); + Bangle.setLCDPower(1); presentScramble(); - - setWatch(() => { - presentScramble(); - }, BTN1, {repeat:true}); }; init(); diff --git a/apps/dclock/bangle1-dev-clock-screenshot.png b/apps/dclock/bangle1-dev-clock-screenshot.png new file mode 100644 index 000000000..ac136e48e Binary files /dev/null and b/apps/dclock/bangle1-dev-clock-screenshot.png differ diff --git a/apps/dclock/bangle2-dev-clock-screenshot.png b/apps/dclock/bangle2-dev-clock-screenshot.png new file mode 100644 index 000000000..0deb6dc2e Binary files /dev/null and b/apps/dclock/bangle2-dev-clock-screenshot.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot1.png b/apps/demoapp/bangle1-demo-loop-screenshot1.png new file mode 100644 index 000000000..9618f7044 Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot1.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot2.png b/apps/demoapp/bangle1-demo-loop-screenshot2.png new file mode 100644 index 000000000..0d39685ba Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot2.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot3.png b/apps/demoapp/bangle1-demo-loop-screenshot3.png new file mode 100644 index 000000000..2a98f79a1 Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot3.png differ diff --git a/apps/demoapp/bangle1-demo-loop-screenshot4.png b/apps/demoapp/bangle1-demo-loop-screenshot4.png new file mode 100644 index 000000000..8f43cac50 Binary files /dev/null and b/apps/demoapp/bangle1-demo-loop-screenshot4.png differ diff --git a/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png b/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png new file mode 100644 index 000000000..b668794b1 Binary files /dev/null and b/apps/devstopwatch/bangle1-dev-stopwatch-screenshot.png differ diff --git a/apps/dotclock/bangle1-dot-clock-screenshot.png b/apps/dotclock/bangle1-dot-clock-screenshot.png new file mode 100644 index 000000000..767cd2d55 Binary files /dev/null and b/apps/dotclock/bangle1-dot-clock-screenshot.png differ diff --git a/apps/dotclock/bangle2-dot-clcok-screenshot.png b/apps/dotclock/bangle2-dot-clcok-screenshot.png new file mode 100644 index 000000000..3aadddb8f Binary files /dev/null and b/apps/dotclock/bangle2-dot-clcok-screenshot.png differ diff --git a/apps/emojuino/ChangeLog b/apps/emojuino/ChangeLog index 5560f00bc..1c99f1970 100644 --- a/apps/emojuino/ChangeLog +++ b/apps/emojuino/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Upgraded text to images, added welcome screen and subtitles. diff --git a/apps/emojuino/emojuino.js b/apps/emojuino/emojuino.js index 3de92fa6c..5b7670652 100644 --- a/apps/emojuino/emojuino.js +++ b/apps/emojuino/emojuino.js @@ -4,29 +4,48 @@ */ -// Emojis are integer pairs with the form [ image, Unicode code point ] +// Emoji images are 96px x 96px, 4bpp (https://www.espruino.com/Image+Converter) +// and adapted from Font Awesome 5 +const GRIN = "sFgwkBiIATDwoaUFi4ynQZ4uuGDzlTF1wwaFyowYFy4wWiAvZgIutGCgubSKRecMCQudMCBeeMCAufMBxegMBwuhMBheiMBgujMBRekMBQvvF0qQIL0xgIF94unSA4vuR1CQGF94upSAovuR1SQEF94urSAY/PCBivQF5z/DEBQ+DEB5ePCJYOEMBgNNF8MBHpogNHwqBNF/4vsEAovOX7TviBhYgFD5Q/EEJoANEAY/OLxgAQPx5edAH4A/AH4A/AH4A/AEUQF1sBF/4v/F/4vviILJBRQANEZYLJHQIMKFpYABQhIiKC4QaMIhBHLF6AAVEhRQIF8ZuCF5B6GACYjMF9ZrOF8jAiKRgvvSEJROBo5gYEBw+IMCwfPB5BgWDxBPHCCBeVJxBgdJqIvJMCQcTCRAwRFxJ8KChQwODKwVJGBouKbZgXLDBQVLPBoZLDYxDMLxocQACLXOMBwARFxxgfLx5gfFyBgdLyIwcFyaRbFygwZFywwXFzAwVFzQwTFzgwRFzwxOFsIyKDSg"; +const MEH = "sFgwkBiIATDwoaUFi4ynQZ4uuGDzlTF1wwaFyowYFy4wWiAvZgIutGCgubSKRecMCQudMCBeeMCAufMBxegMBwuhMBheiMBgujMBRekMBQvvF0qQIL0xgIF94unSA4vuR1CQGF94upSAovuR1SQEF94urSAY/PCBivQF5z/DEBQ+DEB5ePCJYOEMBgNNF8MBHpogNHwqBNF/4vsEAovOX7TviBhYgFD5Q/EEJoANEAY/OLxgAQPx5edAH4A/AH4A/AH4A/AEUQF1sBF/4v/F/4vviIvtiIv/F9qeBACDgNB5ouSECAOLFyaBMKAYvrByQvgSBS/fD4jAfXxwQMADxAQF8iQLADjeGF96QoFwxgnLw4vwSEwuIMEpeJMEouKMEZeLMEYuMMEJeNMEIuOMD5ePMD4uQMDpeRGDguTSLYuUGDIuWGC4uYGCouaGCYucGCIueGJwthGRQaUA"; +const FROWN = "sFgwkBiIATDwoaUFi4ynQZ4uuGDzlTF1wwaFyowYFy4wWiAvZgIutGCgubSKRecMCQudMCBeeMCAufMBxegMBwuhMBheiMBgujMBRekMBQvvF0qQIL0xgIF94unSA4vuR1CQGF94upSAovuR1SQEF94urSAY/PCBivQF5z/DEBQ+DEB5ePCJYOEMBgNNF8MBHpogNHwqBNF/4vsEAovOX7TviBhYgFD5Q/EEJoANEAY/OLxgAQPx5edAH4A/AH4A/AH4A/AEUQF1sBF/4v/F/4vUgMRAAQZWFqwxWCgIuZGCYvSFxIcUFzYdTOZyNKSKQdCCJwuNMB5NDLzZOPIKAviCJguPJxpNEF94RLRyBONIKAvHNRQvRCKAMUJpIvOZxx9WAEbSTADReHF+CQmFxBglLxJglFxRgjLxZgjFxhghLxpghFxxgfLx5gfFyBgdLyIwcFyaRbFygwZFywwXFzAwVFzQwTFzgwRFzwxOFsIyKDSg"; +const THUMBS_UP = "sFgwkBiIAaiAiBDzYAQKYZQcLyAwsF4qSpcoxgoF4xgnRwwvxSEwvvFw4vwYEwv/F/4AOiAv/R1Av/F/6+PgIv/RzwvjLxQvkFxTujLxYvjFxaOiLxgvvR1wviR3gviR3YviFxg6iF7AwVRxowhFzUAgIvuMCSObF6YucSCJedF6IudSARQIHQheeAAIgKGAYufF+CbMF/4v/WYQv/F/6yPF/6OeF9wgNL/4v/F/4vhEQIv/R/4v/F/7ueF/4v/Xx4v/F/4v/F/4v/F/4v/F7ogOF/6OSEAgHCiAvrAwQHHRz4v/F/4v/F58QF8cBE4wPDGLYvHB5aTaKwQvUMS4vYGCx8QF5AwULwgvWYiZJQIAowXDowvYGJyqRFx4bKDRQA=="; +const THUMBS_DOWN = "sFgwkBiIAbiAoGEroAHLZgttMcK9RXEZgmFyZgHDZA/JFyogFDZQwHFqovXLiyQHB5wtaF6gubF/4v/F/4vwgIv/F7wgPF/6QTF/4v/F/4v/F/4v/F/4AdF/4v/YCIv/F/4v9EQIv/R/4v/F/7ueL+gFBiMQF8oiBE4wHHF/6QQF/4v/YigvugInBiAvrM5QvvM4gvqMFgvDMD0BF55gegJPKgIvEMDoeLF4pgdJ5QuGF7gjHABaQbFyRgbFygvZFyqQOEixgYF8RgMgIv/SH5gPYH6QfF8aQvMBgvjMBaQjMBYvkMBQv/SEAv/F/7APF/6QfF/4v/F/0BF8sQF/4vnF0rAJF9yOmSBAunF4xeoSAouqMAYTQA=="; +const HEART = "sFgwkBiIA/AH4A/AH4AogAADC1EQC4gaQCo8BIqYwRCyxdJDJoVLMJYuMGBIVNGBQYNDI5FOO5IXODI4WWI6BgGCywYTDIYVVO6gvXSAoYTDIQVTMAgYTDIJFUMAgYUACyOXAC7XWF7YurSAYvuR1iQCF/4v/F54utAH4A/AH4A/AH4A/AGMQF1sBF/4v/F58RF9sRF/4vgYFi+BMFouCF+CQqRwYvwSFQuEMFJeFMFIuGME5eHME4uIMEpeJMEouKMEZeLMEYuMMEJeNMEIuOMD5ePMD4uQMDpeRMDouSMDZeTMDYuUMDJeVMDIuWMC5eXMC4uYMCpeZMCouaMCZebMCYucMCJedF+CQQFzxgPFz5gPF8JgMXr5gPF0RgLL0ZgLF0hgJL0pgJF0xgHL05gHF1BgFL1JgFF1QwDF1gA/AH4A/AH4AJA="; +const TX = "k8XwkBiIAYEYogLHBAUIiBNKGxooKEggvJCYYHDKxAMFAoRrOCRAsHCYqbNHQibLKAauOLBCJHQw6JMQBIJBRJDWJThK5JJJi5KbpaJKFBaKEE5ybGHRhcOACEQA"; + + +// Emojis are pairs with the form [ Image String, Unicode code point ] // For code points see https://unicode.org/emoji/charts/emoji-list.html const EMOJIS = [ - [ ':)', 0x1f642 ], // Slightly smiling - [ ':|', 0x1f610 ], // Neutral - [ ':(', 0x1f641 ], // Slightly frowning - [ '+1', 0x1f44d ], // Thumbs up - [ '-1', 0x1f44e ], // Thumbs down - [ '<3', 0x02764 ], // Heart + [ GRIN, 0x1f642 ], // Slightly smiling + [ MEH, 0x1f610 ], // Neutral + [ FROWN, 0x1f641 ], // Slightly frowning + [ THUMBS_UP, 0x1f44d ], // Thumbs up + [ THUMBS_DOWN, 0x1f44e ], // Thumbs down + [ HEART, 0x02764 ], // Heart ]; const EMOJI_TRANSMISSION_MILLISECONDS = 5000; const BLINK_PERIOD_MILLISECONDS = 500; const TRANSMIT_BUZZ_MILLISECONDS = 200; const CYCLE_BUZZ_MILLISECONDS = 50; +const WELCOME_MESSAGE = 'Emojuino:\r\n\r\n< Swipe >\r\nto select\r\n\r\nTap\r\nto transmit'; // Non-user-configurable constants const IMAGE_INDEX = 0; const CODE_POINT_INDEX = 1; +const EMOJI_PX = 96; +const EMOJI_X = (g.getWidth() - EMOJI_PX) / 2; +const EMOJI_Y = (g.getHeight() - EMOJI_PX) / 2; +const TX_X = 68; +const TX_Y = 12; +const FONT_SIZE = 24; const BTN_WATCH_OPTIONS = { repeat: true, debounce: 20, edge: "falling" }; const UNICODE_CODE_POINT_ELIDED_UUID = [ 0x49, 0x6f, 0x49, 0x44, 0x55, 0x54, 0x46, 0x2d, 0x33, 0x32 ]; + // Global variables let emojiIndex = 0; let isToggleOn = false; @@ -72,6 +91,7 @@ function transmitEmoji(image, codePoint, duration) { require('ble_eddystone_uid').advertise(UNICODE_CODE_POINT_ELIDED_UUID, instance); isTransmitting = true; + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], true); let displayIntervalId = setInterval(toggleImage, BLINK_PERIOD_MILLISECONDS, image); @@ -85,14 +105,14 @@ function terminateEmoji(displayIntervalId) { NRF.setAdvertising({ }); isTransmitting = false; clearInterval(displayIntervalId); - drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]); + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], false); } // Toggle the display between image/off function toggleImage(image) { if(isToggleOn) { - drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]); + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], true); } else { g.clear(); @@ -102,9 +122,15 @@ function toggleImage(image) { // Draw the given emoji -function drawImage(image) { +function drawImage(image, isTx) { g.clear(); - g.drawString(image, g.getWidth() / 2, g.getHeight() / 2); + g.drawImage(require("heatshrink").decompress(atob(image)), EMOJI_X, EMOJI_Y); + if(isTx) { + g.drawImage(require("heatshrink").decompress(atob(TX)), TX_X, TX_Y); + } + else { + g.drawString("< Swipe >", g.getWidth() / 2, g.getHeight() - FONT_SIZE); + } g.flip(); } @@ -131,15 +157,15 @@ function handleDrag(event) { // Special function to handle display switch on Bangle.on('lcdPower', (on) => { if(on) { - drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]); + drawImage(EMOJIS[emojiIndex][IMAGE_INDEX], false); } }); // On start: display the first emoji and handle drag and touch events g.clear(); -g.setFont('Vector', 80); +g.setFont('Vector', FONT_SIZE); g.setFontAlign(0, 0); -drawImage(EMOJIS[emojiIndex][IMAGE_INDEX]); +g.drawString(WELCOME_MESSAGE, g.getWidth() / 2, g.getHeight() / 2); Bangle.on('touch', handleTouch); Bangle.on('drag', handleDrag); diff --git a/apps/emojuino/screenshot-swipe.png b/apps/emojuino/screenshot-swipe.png new file mode 100644 index 000000000..a870724b9 Binary files /dev/null and b/apps/emojuino/screenshot-swipe.png differ diff --git a/apps/emojuino/screenshot-tx.png b/apps/emojuino/screenshot-tx.png new file mode 100644 index 000000000..212d41f88 Binary files /dev/null and b/apps/emojuino/screenshot-tx.png differ diff --git a/apps/emojuino/screenshot-welcome.png b/apps/emojuino/screenshot-welcome.png new file mode 100644 index 000000000..4cf1fecdf Binary files /dev/null and b/apps/emojuino/screenshot-welcome.png differ diff --git a/apps/fd6fdetect/ChangeLog b/apps/fd6fdetect/ChangeLog index 3c82c3ca7..b85df5ace 100644 --- a/apps/fd6fdetect/ChangeLog +++ b/apps/fd6fdetect/ChangeLog @@ -1 +1,2 @@ 0.1: Added source code +0.2: Added a README file diff --git a/apps/fd6fdetect/README.md b/apps/fd6fdetect/README.md new file mode 100644 index 000000000..1a7cce8bd --- /dev/null +++ b/apps/fd6fdetect/README.md @@ -0,0 +1,3 @@ +# FD6FDetect + +An app dedicated to letting you know how many Exposure Notification beacons are near you. diff --git a/apps/files/files-icon.js b/apps/files/files-icon.js index 7e55db9e0..7f7ea4d0c 100644 --- a/apps/files/files-icon.js +++ b/apps/files/files-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwghC/AEkIxAABwUiAAwKBC6+AC6ERiIXDGBAXPGA8JzIAByQXKGA4XUA4eDmYAGJwQXVxEizAXPIgIXDwWZC6uIxIwCC6eIGAQX/C9i/FC5mCCw0yC5wAMC/4Xnx//ABf4C/Xzdw8zn4XkL/5f/L+oUDI6YX3AB4XeAH4AdA==")) +require("heatshrink").decompress(atob("mEw4cA///7c0AYMXlm3gf42s1yvb5xT/ABdJkmStu27YCCtMkCKOACJdm7YRCyARQyQRLBwIRDoARTgVLtu3K4tJl4RQkvpCJdbtwRBkm5CKGZCKGTCKGSsgR/R4gRHpIMBCInaCJIIBARAR/CJtPB5FLCI1KEhMSCLN//4AE/QRbI/5H/CI4PCGpwRXp4RIpZFDCIQiJAQIRWAH4AGA")) diff --git a/apps/gbdebug/ChangeLog b/apps/gbdebug/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gbdebug/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gbdebug/README.md b/apps/gbdebug/README.md new file mode 100644 index 000000000..47b1525b8 --- /dev/null +++ b/apps/gbdebug/README.md @@ -0,0 +1,26 @@ +# Gadgetbridge Debug + +This is useful if your Bangle isn't responding to the Gadgetbridge +Android app properly. + +This app disables all existing Gadgetbridge handlers and then displays the +messages that come from Gadgetbridge on the screen +of the watch. It also saves the last 10 messages in a variable +called `history`. + +More info on Gadgetbridge at http://www.espruino.com/Gadgetbridge + +## Usage + +* Run the `GB Debug` app on your Bangle +* Connect your Bangle to Gadgetbridge +* Do whatever was causing you problems (eg receiving a call) +* The Gadgetbridge message should now be displayed on-screen + +If you want to get the *actual* data rather than copying it from the screen. + +* Ensure the `GB Debug` app is kept running after the above steps +* Disconnect Gadgetbridge from the Bangle +* Connect the Web IDE on your PC +* Type `show()` on the left-hand side of the IDE and the +last 10 messages from Gadgetbridge will be shown. diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js new file mode 100644 index 000000000..a701ef3a9 --- /dev/null +++ b/apps/gbdebug/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA=")) diff --git a/apps/gbdebug/app.js b/apps/gbdebug/app.js new file mode 100644 index 000000000..ee5e46999 --- /dev/null +++ b/apps/gbdebug/app.js @@ -0,0 +1,21 @@ +E.showMessage("Waiting for message"); +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +var history = []; + +GB = function(e) { + if (history.length > 10) + history = history.slice(history.length-10); + history.push(e); + + var s = JSON.stringify(e,null,2); + + g.reset().clear(Bangle.appRect); + g.setFont("6x8").setFontAlign(-1,0); + g.drawString(s, 10, g.getHeight()/2); +}; + +function show() { + print(JSON.stringify(history,null,2)); +} diff --git a/apps/gbdebug/app.png b/apps/gbdebug/app.png new file mode 100644 index 000000000..f70bce7ad Binary files /dev/null and b/apps/gbdebug/app.png differ diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog index 42ef60ab2..9cebf0a31 100644 --- a/apps/gbmusic/ChangeLog +++ b/apps/gbmusic/ChangeLog @@ -4,3 +4,4 @@ 0.04: Setting to disable touch controls, minor bugfix 0.05: Setting to disable double/triple press control, remove touch controls setting, reduce fadeout flicker 0.06: Bangle.js 2 support +0.07: Fix "previous" button image diff --git a/apps/gbmusic/app.js b/apps/gbmusic/app.js index 328b4a1ae..f514dfccd 100644 --- a/apps/gbmusic/app.js +++ b/apps/gbmusic/app.js @@ -303,7 +303,7 @@ function drawControls() { l.up.col = cc("volumeup" in tCommand); l.down.col = cc("volumedown" in tCommand); } - l.prev.icon = (stat==="play") ? "pause" : "prev"; + l.prev.icon = (stat==="play") ? "pause" : "previous"; l.prev.col = cc("prev" in tCommand || "pause" in tCommand); l.next.icon = (stat==="play") ? "next" : "play"; l.next.col = cc("next" in tCommand || "play" in tCommand); diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js index afd0be4fb..f9c7cde90 100644 --- a/apps/gbridge/settings.js +++ b/apps/gbridge/settings.js @@ -23,6 +23,7 @@ } var mainmenu = { "" : { "title" : "Gadgetbridge" }, + "< Back" : back, "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" }, "Show Icon" : { value: settings().showIcon, @@ -34,8 +35,7 @@ value: !!settings().hrm, format: v => v?"Yes":"No", onchange: v => updateSetting('hrm', v) - }, - "< Back" : back, + } }; var findPhone = { diff --git a/apps/getup/bangle1-get-up-screenshot.png b/apps/getup/bangle1-get-up-screenshot.png new file mode 100644 index 000000000..3bd950280 Binary files /dev/null and b/apps/getup/bangle1-get-up-screenshot.png differ diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog index c91003914..cb22dd13f 100644 --- a/apps/gpsrec/ChangeLog +++ b/apps/gpsrec/ChangeLog @@ -26,3 +26,5 @@ 0.22: Ensure Bangle.setGPSPower uses 'gpsrec' as a tag 0.23: Fix issue where tracks wouldn't record when running from OpenStMap if a period hadn't been set up first 0.24: Better support for Bangle.js 2, avoid widget area for Graphs, smooth graphs more +0.25: Fix issue where if Bangle.js 2 got a GPS fix but no reported time, errors could be caused by the widget (fix #935) +0.26: Multiple bugfixes diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js index 164124257..df3353930 100644 --- a/apps/gpsrec/app.js +++ b/apps/gpsrec/app.js @@ -249,10 +249,10 @@ function plotTrack(info) { g.fillCircle(ox,oy,5); if (info.qOSTM) g.setColor(0, 0, 0); else g.setColor(1,1,1); - g.drawString(require("locale").distance(dist),120,220); + g.drawString(require("locale").distance(dist),g.getWidth() / 2, g.getHeight() - 20); g.setFont("6x8",2); g.setFontAlign(0,0,3); - g.drawString("Back",230,200); + g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40); setWatch(function() { viewTrack(info.fn, info); }, global.BTN3||BTN1); @@ -330,13 +330,13 @@ function plotGraph(info, style) { height: g.getHeight()-(24+8), axes : true, gridy : grid, - gridx : 50, + gridx : infn.length / 3, title: title, xlabel : x=>Math.round(x*dur/(60*infn.length))+" min" // minutes }); g.setFont("6x8",2); g.setFontAlign(0,0,3); - g.drawString("Back",230,200); + g.drawString("Back",g.getWidth() - 10, g.getHeight() - 40); setWatch(function() { viewTrack(info.fn, info); }, global.BTN3||BTN1); diff --git a/apps/gpsrec/widget.js b/apps/gpsrec/widget.js index 6a47f04c5..995f5f73b 100644 --- a/apps/gpsrec/widget.js +++ b/apps/gpsrec/widget.js @@ -26,6 +26,7 @@ fixToggle = !fixToggle; WIDGETS["gpsrec"].draw(); if (hasFix) { + if (fix.time===undefined) fix.time = new Date(); // Bangle.js 2 can provide a fix before time it seems var period = fix.time.getTime() - lastFixTime; if (period+500 > settings.period*1000) { // round up lastFixTime = fix.time.getTime(); diff --git a/apps/hcclock/bangle1-high-contrast-clock-screenshot.png b/apps/hcclock/bangle1-high-contrast-clock-screenshot.png new file mode 100644 index 000000000..f3cd85e70 Binary files /dev/null and b/apps/hcclock/bangle1-high-contrast-clock-screenshot.png differ diff --git a/apps/hrings/bangle1-hypno-rings-screenshot.png b/apps/hrings/bangle1-hypno-rings-screenshot.png new file mode 100644 index 000000000..66f8bcba2 Binary files /dev/null and b/apps/hrings/bangle1-hypno-rings-screenshot.png differ diff --git a/apps/impwclock/bangle1-impercise-word-clock-screenshot.png b/apps/impwclock/bangle1-impercise-word-clock-screenshot.png new file mode 100644 index 000000000..9521c06a0 Binary files /dev/null and b/apps/impwclock/bangle1-impercise-word-clock-screenshot.png differ diff --git a/apps/intervalTimer/ChangeLog b/apps/intervalTimer/ChangeLog new file mode 100644 index 000000000..d62860265 --- /dev/null +++ b/apps/intervalTimer/ChangeLog @@ -0,0 +1 @@ +0.01: First Release \ No newline at end of file diff --git a/apps/intervalTimer/README.md b/apps/intervalTimer/README.md new file mode 100644 index 000000000..d57c16e9c --- /dev/null +++ b/apps/intervalTimer/README.md @@ -0,0 +1,34 @@ +# Interval Timer + +An interval timer for workouts and whatever else! + +## Usage + +First set the active time (i.e. the number of seconds to perform exercises). + + + +Next set the rest time (i.e. number of seconds to rest between exercises). + + + +Finally choose the number of sets to perform. + + + +Active time will be shown in red, rest time in green. The watch will buzz whenever active or rest time gets to 0. + + + + +You can press the physical button during timer countdown to pause the timer. + + + +View after all sets are completed. Press menu to change settings or restart to start timer again with the same settings. + + + +## Creator + +James Gough diff --git a/apps/intervalTimer/app-icon.js b/apps/intervalTimer/app-icon.js new file mode 100644 index 000000000..1ca594050 --- /dev/null +++ b/apps/intervalTimer/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwg96hWq1WgDCgXWxGZzOICqQABC4QABCyIXFDBsICIeJyfznAFBwAWPC4Of///mYYMCwgXBl4XB/4xCFxwABn4XCDAQwICw2ICwf/+YwJxGDHoQXHGARGIn/4C5QwBJAwQDC5QLCIw6GEC5BIGIwQLBJAgXGJAwXEJAgXPHgoXIEYIXFLwRIFC484C4h2DJAoIFPA+Ix4MGAAJoDHYgXKf4QXUJAYJGC5p5CF6hIBO44XNABIXGEw4AIU4rXFC5jvFc5AAHxAXGQwwAHQAIXcPCB2FC4RgOB4IXFJBxGHJB5GHJAYwKFwIXIJAIwKFwJGHGAYYICwIuIGAeImYWFmYJBFxIYEwZjC+YtCCxZJDAA4WMDBIWODIwVRAH4AXA==")) \ No newline at end of file diff --git a/apps/intervalTimer/app.js b/apps/intervalTimer/app.js new file mode 100644 index 000000000..fd57dbe2b --- /dev/null +++ b/apps/intervalTimer/app.js @@ -0,0 +1,306 @@ +/** + +Interval Timer + +An app for the Bangle.js watch + +*/ + +var Layout = require("Layout"); + +// Globals +var timerMode; // 'active' || 'rest' +var numSets = 1; +var activeTime = 20; +var restTime = 10; +var counter; +var setsRemaining; +var counterInterval; +var outOfTimeTimeout; +var timerIsPaused; +var timerLayout; + +/** Called to initialize the timer layout */ +function initTimerLayout() { + timerLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"40%", pad: 10, label:"00:00", id:"time" }, + {type:"txt", font:"6x8:2", label:"0", id:"set" } + ] + }, {btns: [ + {label: "Stop", cb: l => { + if (timerIsPaused){ + timerIsPaused = false; + resumeTimer(); + } + else{ + timerIsPaused = true; + pauseTimer(); + } + } + } + ] + }); +} + +/** Pauses the timer by clearing the counterInterval */ +function pauseTimer() { + if (counterInterval){ + clearTimeout(counterInterval); + counterInterval = undefined; + } + // update layout to display "Paused" + timerLayout.clear(timerLayout.time); + timerLayout.time.label = "||"; + timerLayout.clear(timerLayout.set); + timerLayout.set.label = "Paused"; + timerLayout.render(); +} + +/** Reumes the timer by setting the counterInterval again */ +function resumeTimer() { + if (!counterInterval){ + counterInterval = setInterval(countDown, 1000); + } + // display the timer values again. + timerLayout.clear(timerLayout.time); + timerLayout.time.label = counter; + timerLayout.clear(timerLayout.set); + timerLayout.set.label = `Sets: ${setsRemaining}`; + timerLayout.render(); +} + +/** Display 'Done' view, called when all sets are completed */ +function outOfTime() { + var stopLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"30%", label:"Done!", id:"time" }, + ] + }, {btns: [ + // menu button allows user to modify times and sets + {label:"Menu", cb: l=> { + if (outOfTimeTimeout){ + clearTimeout(outOfTimeTimeout); + outOfTimeTimeout = undefined; + } + //stopLayout.remove(); + setup(); + } + }, + // restart button runs timer again with the same settings + {label:"Restart", cb: l=> { + if (outOfTimeTimeout){ + clearTimeout(outOfTimeTimeout); + outOfTimeTimeout = undefined; + } + //stopLayout.remove(); + timerMode = 'active'; + startTimer(); + } + } + ]}); + + if (counterInterval) return; + setsRemaining = numSets; + g.clear(); + stopLayout.render(); + Bangle.buzz(500); + Bangle.beep(200, 4000) + .then(() => new Promise(resolve => setTimeout(resolve,200))) + .then(() => Bangle.beep(200, 3000)); +} + +/** Function called by the counterInterval at each second. + Updates the timer display values. +*/ +function countDown() { + // Out of time + if (counter<=0) { + if(timerMode === 'active'){ + timerMode = 'rest'; + startTimer(); + return; + } + else{ + --setsRemaining; + if (setsRemaining === 0){ + clearInterval(counterInterval); + counterInterval = undefined; + //setWatch(startTimer, (process.env.HWVERSION==2) ? BTN1 : BTN2); + outOfTime(); + return; + } + timerMode = 'active'; + startTimer(); + return; + } + } + + timerLayout.clear(timerLayout.time); + timerLayout.time.label = counter; + timerLayout.render(); + counter--; +} + +/** Start the interval timer. */ +function startTimer() { + timerIsPaused = false; + g.clear(); + if(timerMode === 'active'){ + counter = activeTime; + timerLayout.time.col = '#f00'; + } + else{ + counter = restTime; + timerLayout.time.col = '#0f0'; + } + + timerLayout.clear(timerLayout.set); + timerLayout.set.label = `Sets: ${setsRemaining}`; + timerLayout.render(); + Bangle.buzz(); + countDown(); + if (!counterInterval){ + counterInterval = setInterval(countDown, 1000); + } +} + +/** Menu step in which user sets the number of sets to be performed. */ +function setNumSets(){ + g.clear(); + var menuLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"Number Sets", id:"title" }, + {type:"txt", font:"30%", pad: 20, label: numSets, id:"value" }, + {type:"btn", font:"6x8:2", label:"Back", cb: l => { + setRestTime(); + } + } + ] + }, {btns: [ + {label:"+", cb: l=> { + incrementNumSets(); + }}, + {label:"Go", cb: l=> { + setsRemaining = numSets; + initTimerLayout(); + startTimer(); + }}, + {label:"-", cb: l=>{ + decrementNumSets(); + }} + ]}); + menuLayout.render(); + + const incrementNumSets = () => { + ++numSets; + menuLayout.clear(menuLayout.numSets); + menuLayout.value.label = numSets; + menuLayout.render(); + }; + + const decrementNumSets = () => { + if(numSets === 1){ + return; + } + --numSets; + menuLayout.clear(menuLayout.numSets); + menuLayout.value.label = numSets; + menuLayout.render(); + }; +} + +/** Menu step in which user sets the number of seconds of rest time for each set. */ +function setRestTime(){ + g.clear(); + var menuLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"Rest Time", id:"title" }, + {type:"txt", font:"30%", pad: 20, label: restTime, id:"value" }, + {type:"btn", font:"6x8:2", label:"Back", cb: l => { + setActiveTime(); + } + } + ] + }, {btns: [ + {label:"+", cb: l=> { + incrementRestTime(); + }}, + {label:"OK", cb: l=>setNumSets()}, + {label:"-", cb: l=>{ + decrementRestTime(); + }} + ]}); + menuLayout.render(); + + const incrementRestTime = () => { + restTime += 5; + menuLayout.clear(menuLayout.restTime); + menuLayout.value.label = restTime; + menuLayout.render(); + }; + + const decrementRestTime = () => { + if(restTime === 0){ + return; + } + restTime -= 5; + menuLayout.clear(menuLayout.restTime); + menuLayout.value.label = restTime; + menuLayout.render(); + }; +} + +/** Menu step in which user sets the number of seconds of active time for each set. */ +function setActiveTime(){ + g.clear(); + var menuLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"6x8:2", label:"Active Time", id:"title" }, + {type:"txt", font:"30%", pad: 20, label: activeTime, id:"value" } + ] + }, {btns: [ + {font:"20%", label:"+", fillx:1, cb: l=> { + incrementActiveTime(); + }}, + {label:"OK", cb: l => setRestTime()}, + {type:"btn", font:"20%", label:"-", fillx:1, cb: l=> { + decrementActiveTime(); + } + } + ]}); + menuLayout.render(); + + const incrementActiveTime = () => { + activeTime += 5; + menuLayout.clear(menuLayout.activeTime); + menuLayout.value.label = activeTime; + menuLayout.render(); + }; + + const decrementActiveTime = () => { + if(activeTime === 0){ + return; + } + activeTime -= 5; + menuLayout.clear(menuLayout.activeTime); + menuLayout.value.label = activeTime; + menuLayout.render(); + }; +} + +/** Start the setup menu, walks through setting active time, rest time, and number of sets. */ +function setup(){ + if (timerLayout){ + // remove timerLayout, otherwise it's pause button callback will still be registered + timerLayout.remove(timerLayout); + timerLayout = undefined; + } + Bangle.setUI(); // remove all existing input handlers + timerMode = 'active'; + setActiveTime(); +} + +// this keeps the watch LCD lit up +Bangle.setLCDPower(1); +setup(); \ No newline at end of file diff --git a/apps/intervalTimer/app.png b/apps/intervalTimer/app.png new file mode 100644 index 000000000..782c449b3 Binary files /dev/null and b/apps/intervalTimer/app.png differ diff --git a/apps/intervalTimer/images/done.png b/apps/intervalTimer/images/done.png new file mode 100644 index 000000000..d210540d1 Binary files /dev/null and b/apps/intervalTimer/images/done.png differ diff --git a/apps/intervalTimer/images/pause.png b/apps/intervalTimer/images/pause.png new file mode 100644 index 000000000..727380799 Binary files /dev/null and b/apps/intervalTimer/images/pause.png differ diff --git a/apps/intervalTimer/images/set-active.png b/apps/intervalTimer/images/set-active.png new file mode 100644 index 000000000..75b86150b Binary files /dev/null and b/apps/intervalTimer/images/set-active.png differ diff --git a/apps/intervalTimer/images/set-rest.png b/apps/intervalTimer/images/set-rest.png new file mode 100644 index 000000000..e33c9eb02 Binary files /dev/null and b/apps/intervalTimer/images/set-rest.png differ diff --git a/apps/intervalTimer/images/set-sets.png b/apps/intervalTimer/images/set-sets.png new file mode 100644 index 000000000..3d5a9107f Binary files /dev/null and b/apps/intervalTimer/images/set-sets.png differ diff --git a/apps/intervalTimer/images/timer1.png b/apps/intervalTimer/images/timer1.png new file mode 100644 index 000000000..3d1cb6350 Binary files /dev/null and b/apps/intervalTimer/images/timer1.png differ diff --git a/apps/intervalTimer/images/timer2.png b/apps/intervalTimer/images/timer2.png new file mode 100644 index 000000000..026774ba2 Binary files /dev/null and b/apps/intervalTimer/images/timer2.png differ diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index 5560f00bc..895f50e04 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -1 +1,3 @@ 0.01: New App! +0.02: Remove messages on disconnect +0.03: Handling of message actions (ok/clear) diff --git a/apps/ios/boot.js b/apps/ios/boot.js index c3ccb9275..c3a30170d 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -95,7 +95,14 @@ E.on('AMS',a=>{ Bangle.musicControl = cmd => { // play, pause, playpause, next, prev, volup, voldown, repeat, shuffle, skipforward, skipback, like, dislike, bookmark NRF.amsCommand(cmd); -} +}; +// Message response +Bangle.messageResponse = (msg,response) => { + if (isFinite(msg.id)) return NRF.sendANCSAction(msg.id, response);//true/false + // error/warn here? +}; +// remove all messages on disconnect +NRF.on("disconnect", () => require("messages").clearAll()); /* // For testing... diff --git a/apps/jbm8b_IT/bangle1-magic-8-ball-italiano-screenshot.png b/apps/jbm8b_IT/bangle1-magic-8-ball-italiano-screenshot.png new file mode 100644 index 000000000..8bc2c7e9b Binary files /dev/null and b/apps/jbm8b_IT/bangle1-magic-8-ball-italiano-screenshot.png differ diff --git a/apps/largeclock/bangle1-large-clock-screenshot.png b/apps/largeclock/bangle1-large-clock-screenshot.png new file mode 100644 index 000000000..756ae994b Binary files /dev/null and b/apps/largeclock/bangle1-large-clock-screenshot.png differ diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index acabd9b11..3b9dbc30c 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -6,4 +6,5 @@ 0.06: Use Bangle.setUI for buttons 0.07: Theme colours fix 0.08: Merge Bangle.js 1 and 2 launchers -0.09: Added Scaling factor to settings and changed to vector font for Bangle.js2 +0.09: Bangle.js 2 - pressing the button goes back to clock (fix #971) + After 10s of being locked, the launcher goes back to the clock screen diff --git a/apps/launch/app-bangle1.js b/apps/launch/app-bangle1.js index 3d4682e55..f779f5de4 100644 --- a/apps/launch/app-bangle1.js +++ b/apps/launch/app-bangle1.js @@ -64,3 +64,12 @@ Bangle.setUI("updown",dir=>{ }); Bangle.loadWidgets(); Bangle.drawWidgets(); +// 10s of inactivity goes back to clock +if (Bangle.setLocked) Bangle.setLocked(false); // unlock initially +var lockTimeout; +Bangle.on('lock', locked => { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) + lockTimeout = setTimeout(_=>load(), 10000); +}); diff --git a/apps/launch/app-bangle2.js b/apps/launch/app-bangle2.js index 161a226e5..3e858e60b 100644 --- a/apps/launch/app-bangle2.js +++ b/apps/launch/app-bangle2.js @@ -52,3 +52,16 @@ E.showScroller({ } } }); + +// pressing button goes back +setWatch(_=>load(), BTN1, {edge:"falling"}); + +// 10s of inactivity goes back to clock +Bangle.setLocked(false); // unlock initially +var lockTimeout; +Bangle.on('lock', locked => { + if (lockTimeout) clearTimeout(lockTimeout); + lockTimeout = undefined; + if (locked) + lockTimeout = setTimeout(_=>load(), 10000); +}); diff --git a/apps/lazyclock/bangle1-lazy-clock-screenshot.png b/apps/lazyclock/bangle1-lazy-clock-screenshot.png new file mode 100644 index 000000000..282adc289 Binary files /dev/null and b/apps/lazyclock/bangle1-lazy-clock-screenshot.png differ diff --git a/apps/lcars/ChangeLog b/apps/lcars/ChangeLog index 750e7ddfc..85bcbad36 100644 --- a/apps/lcars/ChangeLog +++ b/apps/lcars/ChangeLog @@ -1,2 +1,6 @@ -0.01: Launch app +0.01: Launch app. 0.02: Swipe left/right to set an alarm. +0.03: New design with different icons if gps, hrm or compass is on. +0.04: Inluded LCARS Logo. +0.05: Additional icons for (1) charging and (2) bat < 30%. +0.06: Fix - Alarm disabled, if clock was closed \ No newline at end of file diff --git a/apps/lcars/README.md b/apps/lcars/README.md index 1a73a0d71..3acaacb4d 100644 --- a/apps/lcars/README.md +++ b/apps/lcars/README.md @@ -11,6 +11,9 @@ the [Pedometer widget](https://banglejs.com/apps/#pedometer%20widget). * Shows the number of daily steps * Swipe left/right to activate an alarm +## Icons +
+ ## Creator Made by [David Peer](https://github.com/peerdavid) \ No newline at end of file diff --git a/apps/lcars/background.png b/apps/lcars/background.png deleted file mode 100644 index 1ee4297c6..000000000 Binary files a/apps/lcars/background.png and /dev/null differ diff --git a/apps/lcars/bg_large.png b/apps/lcars/bg_large.png new file mode 100644 index 000000000..dd5bda4f3 Binary files /dev/null and b/apps/lcars/bg_large.png differ diff --git a/apps/lcars/bg_small.png b/apps/lcars/bg_small.png new file mode 100644 index 000000000..8030c0ddb Binary files /dev/null and b/apps/lcars/bg_small.png differ diff --git a/apps/lcars/lcars.app.js b/apps/lcars/lcars.app.js index c30cdfda6..9b7244ece 100644 --- a/apps/lcars/lcars.app.js +++ b/apps/lcars/lcars.app.js @@ -1,16 +1,78 @@ +const filename = "lcars.setting.json"; +const Storage = require("Storage"); +let settings = Storage.readJSON(filename,1) || { + alarm: -1, +}; + /* * Requirements and globals */ const locale = require('locale'); -var alarm = -1; -var img = { +var backgroundImage = { width : 176, height : 151, bpp : 3, - transparent : 0, - buffer : require("heatshrink").decompress(atob("gF58+eAR14IN1fvv374CN7yD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/AH4A/AH4A/AB1z588+YCN+RBuj158+eARyD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf4AUhyD/gEDQaHz4BCuQaNAIN0PQaHIIN0BQaF5IN0AQaHPkBBug6DQ8iEvQaE8yBBuhyDPAQNAINsBQaACBkhCuQaACpVo0cQaACo4CFGjyD/AAMPQf4ACQf4ADgiD+AH4A/AH8J02atICIwEAgPnz15AR3gEgM27dt2wCTF4IABgYROgN9+/fAR14ILsaQBKDakwjKF5oABKZ6DwgxTPQeEmQf5cPQeMBLhyDxgJTRQd0JKaKDuhKD/gENQf6D/F4VNQf8AKaKDvKBYnBAGZQKzBB1QZOwIGqDJsBA2QZJA3QZGYIPCDH4CD/0xA4QY+wIPKDGwCD/tpB6Qf6DHthA5QY1oIPSD/QY9gQf/bIPaD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/AF8JQYgCdsEHnnz54CJgIdLwEAhqDEATtggPnz15ARHkgIdLIIKAgQcCAgQcAA/gAA==")) + transparent : 2, + buffer : require("heatshrink").decompress(atob("AAUEufPnnzATkAg4daIIXnz15ATvkwEDDrUAgPHQDyDghyAeQcNzJQ0cuPHATCDBDrUDJQ1AgAA3jjOF+BA4T4KDFyBB5Qf4ABQAaD9QAaD/QesH8CD/n/8Qf8//+AQfsB///GQ6D2h5BJQf6D7/yD8jl/IIIABjiD5n4/DAAWAQe8B//8QYfH//x4CD2HwMDQIf4AoP4Qesf/56BQYYFBuP/Qev//0AQYoKBn/gQecH/lwQwQADBYaDzGoZBHR4OAQehBKj5BBsuWrICDBAIAofYZBFBAZ6qIJJ6DQZBB3IAiDDgZBygJ6EIIn8IOqDKIIscuPHAQdwINkHIJEfIIPnz15AQeAINT+CHwcPAYI1BIIU8+fPAQbOqg56BQYcAgKD4IIv4RgSDCAQSD34AIC//wBYSDyO4P+IIoIB+E/8AFBQeL7B//HHYJKE+P/AoSDygF/QQJBF//4AoSDygEBQYgFBj/xZYaDzgE/PoIAE/wMDQeZBB/jICAAMcuAMDQevgQwR0CvyD3gP/BAxBEQek4A40OQe4ANQegAMQf6D/AAccQf8Ak6DFyCD/QfcDQYueIPMAuaDE+fBIPMOQYoCb8glB7dt2wCW2EAgKDFATkAg2atOmAS5eBhKDigyDZ2zHCjiD/AAMChEgwQCcQb4AiQb5BiQbscuPHATyDfyfPnnzATnwQbsBQD6DghKAeQcJoHiFBggCYQYVhdwQATgOmgVPNAnOECwAGQYIZXgM2dI1wIL2aoCDYibsF4CD/QcGYILGmyaDFwCD/QfaADQf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D4jCD/ADKDnILSD/Qf6DEHO6DJIP6D/Qf6D/QY8cuPHAQdAQfPz588AQeAQf8cuCD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6DqoCD5HO6DJIP6D/Qf6D/QY8cuPHAQdwE7sGzCDZ+fPngCDwBBe7aD/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/Qf6D/QfcTQYvAQf6DgzVAQbECp6DE5yD5gCDFATqDCsOAIKtB00AhKDEATnwQYVt2wCXQwKDltOmAS6IC2aD82BBCQccaQbGAA==")) } -Graphics.prototype.setFontAntonioMedium = function(scale) { +var iconEarth = { + text: "EARTH", + width : 50, height : 50, bpp : 3, + buffer : require("heatshrink").decompress(atob("AFtx48ECBsDwU5k/yhARLjgjBjlzAQMQEZcIkOP/fn31IEZgCBnlz58cEpM4geugEgwU/8+WNZJHDuHHvgmBCQ8goEOnVgJoMnyV58mACItHI4X8uAFBuVHnnz4BuGxk4////Egz3IkmWvPgNw8f/prB//BghTC+AjE7848eMjNnzySBwUJkmf/BuGuPDAQIjBiPHhhTCSQnjMo0ITANJn44Dg8MuFBggCCiFBcAJ0Bv5xEh+ITo2OhHkyf/OIQdBWwVHhgjBNwUE+fP/5EEgePMoYLBhMgyVJk/+BQQdC688I4XxOIc8v//NAvr+QEBj/5NwKVBy1/QYUciPBhk1EAJrC+KeC489QYaMBgU/8BNB9+ChEjz1Jkn/QYMBDQIgCcYTCCiP/nlzJQmenMAgV4//uy/9wRaB/1J8iVCcAfHjt9TYYICnhKCgRKBw159/v//r927OIeeoASBDQccvv3791KYVDBYPLJQeCnPnz//AAP6ocEjEkXgMgJQtz79fLAP8KYkccAcJ8Gf/f/xu/cAMQ4eP5MlyQRCMolx40YsOGBAPfnnzU4KVDpKMBvz8Dh0/8me7IICgkxJQXPIgZTD58sEgcJk+eNoONnFBhk4/5uB/pcDg5KD+4mEv4CBXISVDhEn31/8/+mH7x//JQK5CAAMB4JBCnnxJQf/+fJEgkAa4L+CAQOOjMn/1bXIRxDJQXx58f//Hhlz/88EgsChMgz/Zs/+nfkyV/8huDOI6SD498NwoACi1Z8+S/Plz17/+QCI7jC+ZxBmfPnojIAAMDcYWSp//2wRJEwq2GABECjMgNYwAmA=")) +} + +var iconSaturn = { + text: "SATURN", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("AH4A/AEkQuPHCJ0ChEAwARNjAjBjgjOhs06Q2OEYVx4ARMhEggUMkANIDoIgBoEEgEBNxJEC6ZrBAAMwNxAjDNYcHNxIjB7dtEwIHBwRoKj158+cuPEjlwCRAjC23bpu0wRNDAAsHEYWeEwaSJ6YjCAQUNSRQjEzxQBWZMNEYlsmg2JWAIjCz95SoJuJggjDtuw6dMG5JKCz998wFBJRVNEYW0yaVBJRNhJQN9+4pCzhKJmBKC4YpB/fINxIgCzFxSoQ3J4ENm3CAQPb98wbpEcAQMYWwKYBNxMDXgc2/fv3g2IEAOAgAjBjy5CEhEMfYICBgfPnjdLjj+CgMHiC3JknDhhoINw4jCAB0IJQIANR4QjPAH4A/AFA")) +} + +var iconMoon = { + text: "MOON", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("AH4AQjlx44CCCZsg8eOkHDwAQKEYgmPhEgEQM48AOIgMHEYoCB4ATI8UAmH/x04JoRuJsImHuBKLn37EwZuIgEQOI8cEpXj/yYBhE8+YNGgkYoJxITBUPnAaC///nC+FjBuIOJZEB8YeCh/8AoYACoMEEAnEjhQDPQJKJ/DCDAoi5DoLdHAoMQgLjFWYPOnngh02IwXzwDjEgPGEYS8BI4MBYoSVG4fP/nghkAgZrDkngJQqSG4gvBg4sBQgkImHihEAWwP8ZBMBEYl5/+cSoVAGQIUFh04weJn///0gj/OEw5KEz45BzhuCTYQAEgePB4IACAoJuBnAQEa4XHjxKB//xFgWHJQsCRgMDEonipwjENwUBDQNx8+evvn/hTDLw3igE+EgZxB8UOXIvEJQUfEYOfv53DEQkgga5BJQvzx84cAj+CDoNh8/eEYJKDuCSEcocnEon+/7xEgFBIIcfB4Mf/IICXI2DgDdBAAn758gCIq5Dv4zBvJuIOIfjEgvP/ARHgwdCB4P3AoTdFAAk4EYk8SQgAFTALaDSQwAGh08//vnDmBABYmEEZYAzA==")) +} + +var iconMars = { + text: "MARS", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("AH4ATjlwCJ+Dh0wwAQMg0cuPHjFhCZkDps0yVJkmQCBMEjFx42atOmzQmLhMkEYQCCCREQoOGEYmmzB0IEY4CBkARGoJKBEYQCEzgSGkGSpAjDyYCCphuGiFhJQgCD8ASFgRHGAQKbB6BuHJRGeOIsINxEk6dNmARDgMEjQjHAQPnVQojIyZKB6YSDNwK5FAQt54BuDXJIjBEwK5EgxKKXgq5BJRdgXIojJAQJKMcAM0EwM2JUApDoCVFExa7FkGCgAmIkAREEwUEjAmHCIgABhEggQmFpACBCIojBEwRQCzVhwkQU4YADgQmBwQCCI4IFBCAojFAQojGJQQjDAQgRGEZICBEo4gFyUIkilFJQUYEAZrBAQMYNw5KDSQSbCNwwABgOGEwgCBsPACQ5xGwdNnARJcAVh48evvnCJK8Chs+/fv33gCRcB48cuPHCBYA/ADAA==")) +} + +var iconSatellite = { + text: "GPS ON", + width : 50, height : 50, bpp : 3, + transparent : 2, + buffer : require("heatshrink").decompress(atob("pMkyQC/ATGXhIRPyNl0gmPjlwCJ9ly1aCJ1c+fHJR1Hy1ZJR1I+fPnlx6QRLpe+/JKBr5KMuYjBJQMdCJce/fvJQW0CJUlEYQCBSpvvJQbXJjl0NwnzNxGQwEOnHhgF78+WqQyIrFx48cAQXz4ShJgAABh0+8cP//9LJEhg4jDuP3//0LhGQgYlBgeAn///5cIy8MuAmDCIP/9I4HkmCEYMOgHfCQWkCI0cuBuDgF/CIP+CI1Ny1IkeAgHANwIAB/QRFrj7BhkxEwQRC/4RFpbXDgSVBg4RCSorXDI4MJAQMfCIP8cwImDn37fwN58+kwHgLgSVFub7CI4NyBAJKDLgkuEYX78+evKtCLg0jEYRKC58JMoRcFkwjDJQTFDl65EkojEAQMdcwn/+gFC3YjEJQLXEpYRDWwQmEdI6SHAQO0CJUkx4jDF4gCIJQgRMXIjCEARIjCCJ2XEYPKCJqJBJQIROcAUpCJ0kybaDARtdCKAC2kAA=")) +} + +var iconAlarm = { + text: "TIMER", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("kmSpICEp//BAwCJn/+CJ8k//5CKAABCJs8uPH//x48EI5YjCAARNKEYUcv//jgFBExEnEYoAC+QmHIgIgC/gpCuPBCI2fIgU4AQXjA4P8CIuTEYZKBAolwHApXBEAWP//jxwpBAALaFDoYCIiQmDDIP4EAT+CEwnJEwYjLAQLaFEYomDKALmDNwoCIOIZuD8AkFgCYDHAQjMAQTdDNwOAEg0Dx0/cYeREZtxQYOTHgJuHOIvkXJy8DNwIACJQ8Ah4NDAAfxEZARHOIIkHg4jQAQb1CQ4KVJgEOnDIBSoIjNAQPBcAaVJcAKVBcDGOcD7OBMQM48BuH8f//JKCnhKNggRBkmfTQJxBEwhuD/gRCyVHJRlyCIVJXgYmB8ZQBAoIKBXIQmCOIt/NxAUCOIImCIgIpCBAJuDAQZEE/huIAQWTDgImBTYQGC8gRFcYpKFCI8kDwQAFCJBfBEAX/+IjBiQRIEw4jJAQc8v//NYwCIOgJrIJpA1OcwbaFAQWQA=")) +} + +var iconCharging = { + text: "CHARGE", + width : 50, height : 50, bpp : 3, + transparent : 5, + buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A")) +} + +var iconNoBattery = { + text: "NO BAT", + width : 50, height : 50, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA")) +} + +// Font to use: +// +Graphics.prototype.setFontAntonioSmall = function(scale) { // Actual height 18 (17 - 0) g.setFontCustom(atob("AAAAAAAAAAAAAAAf4Mf/sYAMAAAAAAfgAfAAAAAfgAeAAAAAAiAAj8H/4fyEAv8f/gfiAAgAAAAD54H98eOPHn8Hz8AhwAAAP8Af+AYGAYCAf+AP8MAB8AHwA+AD4AfAAcf4A/8AwMAwMA/8Af4AAAAAwGD8f/8f8MY/cfz4PD8AHMAAAfAAeAAAAAAAAP/+f//YADAAAQABYADf//P/+AAAAAANAAPAAfwAfgAPAANAAAAAAEAAEAA/AA/AAEAAEAAAAAAZAAfAAYAAAAIAAIAAIAAIAAAAAAAAAMAAMAAAAAAAAEAB8Af4H+AfwAcAAAAAP/4f/8YAMf/8f/8H/wAAAAAAEAAMAAf/8f/8f/8AAAAAAAAAHgcfh8cH8YPMf8MPwEAAAAAAOB4eB8YYMY4Mf/8Pn4AAAAAgAHwA/wPwwf/8f/8AAwAAgAAAf54f58ZwMZwMY/8Qf4AAAAAAP/4f/8YYMYYMff8HP4AAAQAAYAAYD8Y/8f/AfgAcAAAAAAAAPv4f/8YYMY8Mf/8Pn4AAAAAAP94f98YGMcMMf/8H/wAAAAAABgwBgwAAAAAABgABg/Bg8AAAAEAAOAAbAA7gAxgBwwASAAbAAbAAbAAbAASAAAAAxwA5gAbAAPAAOAAAAPAAfHcYPcf8Af4AHgAAAAAAAB/gH/wOA4Y/MZ/sbAsbBkb/MZ/sOBsH/AAAAAAMAP8f/4fwwf4wH/8AH8AAMAAAf/8f/8YYMYYMf/8P/4ADgAAAP/4f/8YAMYAMfj8Pj4AAAAAAf/8f/8YAMYAMf/8P/4B/AAAAf/8f/8YMMYMMYIMAAAAAAf/8f/8YYAYYAYYAAAAAAAP/4f/8YAMYIMfP8Pv8AAAAAAf/8f/8AMAAMAf/8f/8f/8AAAAAAf/8f/8AAAAAAAD4AB8AAMf/8f/4f/gAAAAAAf/8f/8A+AD/gfj4eA8QAEAAAf/8f/8AAMAAMAAMAAAf/8f/8f8AB/wAB8AP8P/Af/8f/8AAAAAAf/8f/8HwAA+AAPwf/8f/8AAAAAAP/4f/8YAMYAMf/8P/4AAAAAAf/8f/8YGAYGAf8AP8ABAAAAAf/w//4wAYwAc//+f/yAAAAAAf/8f/8YMAYMAf/8f/8DA8CAAPj4fz8Y4MeeMfP8HD4YAAYAAf/8f/8YAAQAAAAAf/4f/8AAMAAMf/8f/4AAAYAAf4AP/4AP8AP8f/4fwAQAAYAAf8AP/8AD8D/8f8Af8AD/8AD8f/8f8AAAAQAEeB8P/4B/AP/4fA8QAEYAAfAAP4AB/8H/8fwAcAAAAMYD8Y/8f/MfwMcAMAAAf/+f//YADYADAAAAAAfAAf8AB/wAH8AAMQACYADf//f//AAAAA"), 32, atob("BAUHCAcTCAQFBQgGBAYFBggICAgICAgICAgEBQYGBggNCAgICAcHCAkECAgGCwkICAgIBwYICAwHBwYGBgY="), 18+(scale<<8)+(1<<16)); } @@ -28,71 +90,78 @@ function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; - draw(true); + draw(); }, 60000 - (Date.now() % 60000)); } -function draw(queue){ +function draw(){ + + // First handle alarm to show this correctly afterwards + handleAlarm(); + + // Next draw the watch face g.reset(); g.clearRect(0, 24, g.getWidth(), g.getHeight()); // Draw background image - g.drawImage(img, 0, 24); + g.drawImage(backgroundImage, 0, 24); + + // Draw symbol + var bat = E.getBattery(); + var timeInMinutes = getCurrentTimeInMinutes(); + + var iconImg = + isAlarmEnabled() ? iconAlarm : + Bangle.isCharging() ? iconCharging : + bat < 30 ? iconNoBattery : + Bangle.isGPSOn() ? iconSatellite : + timeInMinutes % 4 == 0 ? iconSaturn : + timeInMinutes % 4 == 1 ? iconMars : + timeInMinutes % 4 == 2 ? iconMoon : + iconEarth; + g.drawImage(iconImg, 115, 115); + + // Alarm within symbol + g.setFontAlign(0,0,0); + g.setFontAntonioSmall(); + g.drawString(iconImg.text, 115+25, 102); + if(isAlarmEnabled() > 0){ + g.drawString(getAlarmMinutes(), 115+25, 115+25); + } // Write time var currentDate = new Date(); var timeStr = locale.time(currentDate,1); g.setFontAlign(0,0,0); g.setFontAntonioLarge(); - g.drawString(timeStr, 100, 50); + g.drawString(timeStr, 60, 55); // Write date - g.setFontAlign(1,-1, 0); - g.setFontAntonioMedium(); + g.setFontAlign(-1,-1, 0); + g.setFontAntonioSmall(); var dayName = locale.dow(currentDate, true).toUpperCase(); var day = currentDate.getDate(); - g.drawString(day, 170, 30); - g.drawString(dayName, 170, 50); - - // Alarm - g.setFontAlign(-1,-1,0); - g.drawString("TMR:", 30, 107); - var alrmText = alarm >= 0 ? "T-"+alarm : "OFF"; - g.drawString(alrmText, 65, 107); + g.drawString(day, 100, 35); + g.drawString(dayName, 100, 55); // Draw battery - var bat = E.getBattery(); - var charging = Bangle.isCharging() ? "*" : ""; - g.drawString("BAT:", 30, 127); - g.drawString(charging + bat+ "%", 65, 127); + g.drawString("BAT:", 25, 98); + g.drawString(bat+ "%", 62, 98); // Draw steps var steps = getSteps(); - g.drawString("STEP:", 30, 147); - g.drawString(steps, 65, 147); + g.drawString("STEP:", 25, 121); + g.drawString(steps, 62, 121); - // GPS - var gpsText = Bangle.isGPSOn() ? "ON" : "OFF"; - g.drawString("GPS:", 115, 107); - g.drawString(gpsText, 149, 107); - - - // HRM - var gpsText = Bangle.isHRMOn() ? "ON" : "OFF"; - g.drawString("HRM:", 115, 127); - g.drawString(gpsText, 149, 127); - - // CMP - var compassText = Bangle.isCompassOn() ? "ON" : "OFF"; - g.drawString("CMP:", 115, 147); - g.drawString(compassText, 149, 147); + // Temperature + g.setFontAlign(-1,-1,0); + g.drawString("TEMP:", 25, 144); + g.drawString(Math.floor(E.getTemperature()) + "C", 62, 144); // Queue draw in one minute - if(queue){ - queueDraw(); - } + queueDraw(); } /* @@ -113,42 +182,45 @@ function stepsWidget() { return undefined; } + /* * Handle alarm */ -var alarmTimeout; -function queueAlarm() { - if (alarmTimeout) clearTimeout(alarmTimeout); - alarmTimeout = setTimeout(function() { - alarmTimeout = undefined; - handleAlarm(); - }, 60000 - (Date.now() % 60000)); +function getCurrentTimeInMinutes(){ + return Math.floor(Date.now() / (1000*60)); +} + +function isAlarmEnabled(){ + return settings.alarm > 0; +} + +function getAlarmMinutes(){ + var currentTime = getCurrentTimeInMinutes(); + return settings.alarm - currentTime; } function handleAlarm(){ + if(!isAlarmEnabled()){ + return; + } - // Check each minute - if(alarm > 0){ - alarm--; - queueAlarm(); - } + if(getAlarmMinutes() > 0){ + return; + } - // After n minutes, inform the user - if(alarm == 0){ - alarm = -1; + // Alarm + var t = 300; + Bangle.buzz(t, 1) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)) + .then(() => new Promise(resolve => setTimeout(resolve, t))) + .then(() => Bangle.buzz(t, 1)); - var t = 300; - Bangle.buzz(t, 1) - .then(() => new Promise(resolve => setTimeout(resolve, t))) - .then(() => Bangle.buzz(t, 1)) - .then(() => new Promise(resolve => setTimeout(resolve, t))) - .then(() => Bangle.buzz(t, 1)) - .then(() => new Promise(resolve => setTimeout(resolve, t))) - .then(() => Bangle.buzz(t, 1)); - } - - // Update UI - draw(false); + // Update alarm state to disabled + settings.alarm = -1; + Storage.writeJSON(filename, settings); } @@ -158,19 +230,27 @@ function handleAlarm(){ Bangle.on('swipe',function(dir) { // Increase alarm if(dir == -1){ - alarm = alarm < 0 ? 0 : alarm; - alarm += 5; - queueAlarm(); + if(isAlarmEnabled()){ + settings.alarm += 5; + } else { + settings.alarm = getCurrentTimeInMinutes() + 5; + } } // Decrease alarm if(dir == +1){ - alarm -= 5; - alarm = alarm <= 0 ? -1 : alarm; + if(isAlarmEnabled() && (settings.alarm-5 > getCurrentTimeInMinutes())){ + settings.alarm -= 5; + } else { + settings.alarm = -1; + } } // Update UI - draw(false); + draw(); + + // Update alarm state + Storage.writeJSON(filename, settings); }); @@ -179,7 +259,7 @@ Bangle.on('swipe',function(dir) { */ Bangle.on('lcdPower',on=>{ if (on) { - draw(true); // draw immediately, queue redraw + draw(); // draw immediately, queue redraw } else { // stop draw timer if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; @@ -194,7 +274,7 @@ Bangle.loadWidgets(); // Clear the screen once, at startup and draw clock g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); -draw(true); +draw(); // After drawing the watch face, we can draw the widgets Bangle.drawWidgets(); \ No newline at end of file diff --git a/apps/lcars/screenshot.png b/apps/lcars/screenshot.png index dc577c4d0..70db639eb 100644 Binary files a/apps/lcars/screenshot.png and b/apps/lcars/screenshot.png differ diff --git a/apps/life/bangle1-game-of-life-screenshot.png b/apps/life/bangle1-game-of-life-screenshot.png new file mode 100644 index 000000000..f6e8c78a1 Binary files /dev/null and b/apps/life/bangle1-game-of-life-screenshot.png differ diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 3d64cf8d7..288dc6dde 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -9,3 +9,4 @@ 0.07: Improve handling of non-ASCII characters (fix #469) 0.08: Added Mavigation units and en_NAV 0.09: Added New Zealand en_NZ +0.10: Apply 12hour setting to time diff --git a/apps/locale/locale.html b/apps/locale/locale.html index 3d806b44b..90a2e8d40 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -146,7 +146,7 @@ exports = { name : "en_GB", currencySym:"£", "%-m": "d.getMonth()+1", "%d": "('0'+d.getDate()).slice(-2)", "%-d": "d.getDate()", - "%HH": "('0'+d.getHours()).slice(-2)", + "%HH": "('0'+getHours(d)).slice(-2)", "%MM": "('0'+d.getMinutes()).slice(-2)", "%SS": "('0'+d.getSeconds()).slice(-2)", "%A": "day.split(',')[d.getDay()]", @@ -178,6 +178,13 @@ var month = ${js(locale.month + ',' + locale.abmonth)}; function round(n) { return n < 10 ? Math.round(n * 10) / 10 : Math.round(n); } +var is12; +function getHours(d) { + var h = d.getHours(); + if (is12===undefined) is12 = (require('Storage').readJSON('setting.json',1)||{})["12hour"]; + if (!is12) return h; + return (h%12==0) ? 12 : h%12; +} exports = { name: ${js(locale.lang)}, currencySym: ${js(locale.currency_symbol)}, diff --git a/apps/mandlebrotclock/ChangeLog b/apps/mandlebrotclock/ChangeLog new file mode 100644 index 000000000..d7bda0d78 --- /dev/null +++ b/apps/mandlebrotclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: Initial Release + \ No newline at end of file diff --git a/apps/mandlebrotclock/README.md b/apps/mandlebrotclock/README.md new file mode 100644 index 000000000..8628a61d0 --- /dev/null +++ b/apps/mandlebrotclock/README.md @@ -0,0 +1,9 @@ +# Mandlebrot Clock + +A simple clock themed on the mandlebrot set. + +Written by [James Milner](https://www.github.com/jameslmilner) + + + + \ No newline at end of file diff --git a/apps/mandlebrotclock/app.png b/apps/mandlebrotclock/app.png new file mode 100644 index 000000000..95ab99a91 Binary files /dev/null and b/apps/mandlebrotclock/app.png differ diff --git a/apps/mandlebrotclock/mandlebrotclock-icon.js b/apps/mandlebrotclock/mandlebrotclock-icon.js new file mode 100644 index 000000000..a2898e734 --- /dev/null +++ b/apps/mandlebrotclock/mandlebrotclock-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+vdvwMzq8CrGCwVewNRluCxAHBAAOsxAAB1gAD1oBB2fWAAPO1mBGZIvCrECq4bBglYmglBGgIxBFQItDFQQsC2es6/XF4OrwOsqwvIt4vBxFdgder0uwUoLQRXE1oqB1nQ1nW2RbCA4PW6HP52rF5d7KoKNBmcDIYIzBrBaB1vPFAOz2RVB1ml54qB1fQFwQvB5+kwSQJWIQoExAFBRYaBB0pVCQYRiB0wDC1erFoPO5ul02sF5QnBAAYFBwbkF0ul1eIAQOqOwLlBIwL7BGIOkAANvxErF49dF4dYGoJfBLoIwD6AfBEgJbBwIEBqwGBqw4BU4Osvd1uteSBDiEFIKLDdQey6ytBEQNWAQOBwMyKYcrAAILBWIRgIEofQ1mJAoS6B2ez665B5+rLQMrq1WWBAACHwJNBCA5WCbgPQ1pYBFYOl6CMB6vP0prB1l6kguLAAWBJgKPHRIOz03Q6+z2QsBVgOrdgOlvaKBLhhiG6AUGJoJOB6GmMgPPcQLHCdAgtRSYgHFrDKBXYWBLQOk0qlBcgNWBYJdSAAcCC4qOBAILzE62l0mCIYVWvQuVAAMsAokzR4WJ1us2fW6K/BMwMrgErAQIAcq+sGAOtF4Os1vXF4I5B1mlFzSQELwU0xGtAIOzF4LCBBgOrLbYwDwUuwVeYIiRB6ukwLBDF7QwCwVYKgJgBGAOt6PW54vB1i7cq2rVoNYFQJfCMAXW62rM4QWDGoPXMwNWAgIMDAw2B67XDlezwUAgYsCwWJLwK9B1YnBwLSEAwIeCBgXXBoQGDHgMr64vEDIOIwNXSAJfBF4RgB1elfQK+GqweCGIIvBCgJUCF4QHBF4rqBRIS/BxKOC1qPB54wBF4pSDE4IjCcAQ6BGYIPCNYYYCl1SKYI0BMwIvBDoIvBPgR1EDgdWKAINDFwIECFoIABbItRulYMYhfCF4Y8BCoYbBAANWEYJfCZALuCIgi/GveeRoIuBXgOt1uy6HV5+kF4olBAAIeBGIIDCAAILCRQYMCNgWs0uqEQOs2fQ6+y63R0vJ1d7q+IUwgAXNoOl5xeBGAOrdYPW6A5BHQWteAovXwWq569BVoWl0ur0g8BVAMrq2lU4gAVq2m1gvC1gwBSAOrLgSiECgIvZq+CKwPWL4IvBXoPQ0uBXQxiBLzCHCW4ItBxGt2fXMAN71iJGYK8r1jqBF4PXL4QvB62r1a+BF4yXBFytWxGB0us6/XdoWzF4TKBwKPGH4IwULgIoB55eB2YGCXoPQ5xeBq+BvUkOolXGAMBXaOCruCwXQ2es1ovC0vP0ulKoOmwWsSgI2BwV70rKBHQIuORgWkwWl2QvBAAXX1YJBwOrAQOAvYxBHoN65HOBQIIBqyeGFgZEBwJ2BKgIqC1ogC2XW0osB1fQ62k5+qMgJoBC4PQfgLYBEYIABNoNWljjCHgNeBgWr63W2QvBxOJBIWr54uCYgL0BLAIsCBIIKB1T+BVwN8WAJcBNQIABIgQGB1fX2RdBXoOJFQWzSIOz1uzAoIwBFgXX2ZHBOIRDCWAOBRgQtC53P1OB0wlBMgQuBdwQAF1oxBEwI7B1p0CBgIIBAAPP0mBcgNWBYOkBYbfB6wtCxCaFGYQKBAoQvBOQIACHoey2ey6D2D0uC0yIBLIILB0pJBEIU6wU0FQbEBF4hnFA4ZlBNoRhCGAJYBHYSKD1eyEYJfBrxfCAwNeAILVBwZZExIABGATNCGARvBCoIMBFwJzDAIderFYwWJsgyBCoI1BAYIABF4QeBL4IvDOIIvDL4PPBYIuCKQQRBEAWsrE0AocQAQJpBGgRNCIQIECCQQzD6Gr0qMBbwYADJ4ZUBl1YBAVelwpBNIQDBFIImCl2CagIVBAATkC5/WFwhLFFoMtwM0E4MtltevggBgcDwITCrEzxEulz5CDgNkMIer6GyLogsCwWmI4MzrFXGAMEA==")) \ No newline at end of file diff --git a/apps/mandlebrotclock/mandlebrotclock.js b/apps/mandlebrotclock/mandlebrotclock.js new file mode 100644 index 000000000..16cc8dfb8 --- /dev/null +++ b/apps/mandlebrotclock/mandlebrotclock.js @@ -0,0 +1,34 @@ +// MIT License - James Milner 2021 + +const mandlebrotBmp = { + width: 176, + height: 176, + bpp: 8, + transparent: 254, + buffer: require("heatshrink").decompress( + atob( + "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" + ) + ), +}; + +function draw() { + g.drawImage(mandlebrotBmp); + // work out how to display the current time + const d = new Date(); + const h = d.getHours(), + m = d.getMinutes(); + const time = h + ":" + ("0" + m).substr(-2); + + // Reset the state of the graphics library + g.reset(); + g.setColor(1, 1, 1); + g.setFont("Vector", 30); + g.drawString(time, 70, 68, false); +} + +g.clear(); + +// draw immediately at first +draw(); +var secondInterval = setInterval(draw, 1000); diff --git a/apps/mandlebrotclock/mandlebrotclock.png b/apps/mandlebrotclock/mandlebrotclock.png new file mode 100644 index 000000000..19601fe2e Binary files /dev/null and b/apps/mandlebrotclock/mandlebrotclock.png differ diff --git a/apps/mandlebrotclock/screenshot_mandlebrotclock.png b/apps/mandlebrotclock/screenshot_mandlebrotclock.png new file mode 100644 index 000000000..542cff324 Binary files /dev/null and b/apps/mandlebrotclock/screenshot_mandlebrotclock.png differ diff --git a/apps/marioclock/bangle1-mario-clock-screenshot.png b/apps/marioclock/bangle1-mario-clock-screenshot.png new file mode 100644 index 000000000..ae2dc7800 Binary files /dev/null and b/apps/marioclock/bangle1-mario-clock-screenshot.png differ diff --git a/apps/mclock/bangle1-morphing-clock-screenshot.png b/apps/mclock/bangle1-morphing-clock-screenshot.png new file mode 100644 index 000000000..e8a6decaa Binary files /dev/null and b/apps/mclock/bangle1-morphing-clock-screenshot.png differ diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index 4f7df3859..87094a091 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -1,3 +1,10 @@ 0.01: New App! 0.02: Add 'messages' library 0.03: Fixes for Bangle.js 1 +0.04: Add require("messages").clearAll() +0.05: Handling of message actions (ok/clear) +0.06: New messages now go at the start (fix #898) + Answering true/false now exits the messages app if no new messages + Back now marks a message as read + Clicking top-left opens a menu which allows you to delete a message or mark unread +0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909) diff --git a/apps/messages/app.js b/apps/messages/app.js index 987d9184b..cb2b5c2cd 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -16,7 +16,8 @@ {"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"} // maps {"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="} - +// call +{"t":"add","id":"call","src":"Phone","name":"Bob","number":"12421312",positive:true,negative:true} */ var Layout = require("Layout"); @@ -45,7 +46,10 @@ var MESSAGES = require("Storage").readJSON("messages.json",1)||[]; if (!Array.isArray(MESSAGES)) MESSAGES=[]; var onMessagesModified = function(msg) { // TODO: if new, show this new one - if (msg.new) Bangle.buzz(); + if (msg.new) { + if (WIDGETS["messages"]) WIDGETS["messages"].buzz(); + else Bangle.buzz(); + } showMessage(msg.id); }; function saveMessages() { @@ -55,9 +59,16 @@ function saveMessages() { function getBackImage() { return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); } +function getPosImage() { + return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); +} +function getNegImage() { + return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); +} function getMessageImage(msg) { if (msg.img) return atob(msg.img); var s = (msg.src||"").toLowerCase(); + if (s=="Phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); @@ -103,7 +114,7 @@ function showMapMessage(msg) { msg.new = false; saveMessages(); layout = undefined; - checkMessages(); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); }); } @@ -118,7 +129,7 @@ function showMusicMessage(msg) { msg.new = false; saveMessages(); layout = undefined; - checkMessages(); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); } layout = new Layout({ type:"v", c: [ {type:"h", fillx:1, bgCol:colBg, c: [ @@ -140,6 +151,22 @@ function showMusicMessage(msg) { layout.render(); } +function showMessageSettings(msg) { + E.showMenu({"":{"title":"Message"}, + "< Back" : () => showMessage(msg.id), + "Delete" : () => { + MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); + saveMessages(); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + }, + "Mark Unread" : () => { + msg.new = true; + saveMessages(); + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0}); + }, + }); +} + function showMessage(msgid) { var msg = MESSAGES.find(m=>m.id==msgid); if (!msg) return checkMessages(); // go home if no message found @@ -154,47 +181,74 @@ function showMessage(msgid) { if (g.setFont(titleFont).stringWidth(title) > w) title = g.wrapString(title, w).join("\n"); } + var buttons = [ + {type:"btn", src:getBackImage(), cb:()=>{ + msg.new = false; // read mail + saveMessages(); + checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:1}); + }} // back + ]; + if (msg.positive) { + buttons.push({type:"btn", src:getPosImage(), cb:()=>{ + msg.new = false; saveMessages(); + Bangle.messageResponse(msg,true); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + }}); + } + if (msg.negative) { + buttons.push({type:"btn", src:getNegImage(), cb:()=>{ + console.log("Response"); + msg.new = false; saveMessages(); + Bangle.messageResponse(msg,false); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1}); + }}); + } layout = new Layout({ type:"v", c: [ {type:"h", fillx:1, bgCol:colBg, c: [ - { type:"img", src:getMessageImage(msg), pad:2 }, + { type:"btn", src:getMessageImage(msg), cb:()=>showMessageSettings(msg) }, { type:"v", fillx:1, c: [ {type:"txt", font:fontMedium, label:msg.src||"Message", bgCol:colBg, fillx:1, pad:2 }, title?{type:"txt", font:titleFont, label:title, bgCol:colBg, fillx:1, pad:2 }:{}, ]}, ]}, {type:"txt", font:fontMedium, label:msg.body||"", wrap:true, fillx:1, filly:1, pad:2 }, - {type:"h",fillx:1, c: [ - {type:"btn", src:getBackImage(), cb:()=>checkMessages(true)}, // back - msg.new?{type:"btn", src:atob("HRiBAD///8D///wj///Fj//8bj//x3z//Hvx/8/fx/j+/x+Ad/B4AL8Rh+HxwH+PHwf+cf5/+x/n/PH/P8cf+cx5/84HwAB4fgAD5/AAD/8AAD/wAAD/AAAD8A=="), cb:()=>{ - msg.new = false; // read mail - saveMessages(); - checkMessages(); - }}:{} - ]} + {type:"h",fillx:1, c: buttons} ]}); g.clearRect(Bangle.appRect); layout.render(); } -function checkMessages(forceShowMenu) { + +/* options = { + clockIfNoMsg : bool + clockIfAllRead : bool + showMsgIfUnread : bool +} +*/ +function checkMessages(options) { + options=options||{}; // If no messages, just show 'no messages' and return - if (!MESSAGES.length) - return E.showPrompt("No Messages",{ + if (!MESSAGES.length) { + if (!options.clockIfNoMsg) return E.showPrompt("No Messages",{ title:"Messages", img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), buttons : {"Ok":1} }).then(() => { load() }); - // we have >0 messages - // If we have a new message, show it - if (!forceShowMenu) { - var newMessages = MESSAGES.filter(m=>m.new); - if (newMessages.length) - return showMessage(newMessages[0].id); + return load(); } + // we have >0 messages + var newMessages = MESSAGES.filter(m=>m.new); + // If we have a new message, show it + if (options.showMsgIfUnread && newMessages.length) + return showMessage(newMessages[0].id); + // no new messages - go to clock? + if (options.clockIfAllRead && newMessages.length==0) + return load(); + // Otherwise show a menu E.showScroller({ h : 48, - c : MESSAGES.length+1, + c : Math.max(MESSAGES.length+1,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) draw : function(idx, r) {"ram" var msg = MESSAGES[idx-1]; if (msg && msg.new) g.setBgColor(colBg); @@ -213,7 +267,7 @@ function checkMessages(forceShowMenu) { x += 50; } var m = msg.title+"\n"+msg.body; - if (msg.src) g.setFontAlign(1,-1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+2); + if (msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2); if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2); if (body) { g.setFontAlign(-1,-1).setFont("6x8"); @@ -235,4 +289,6 @@ function checkMessages(forceShowMenu) { g.clear(); Bangle.loadWidgets(); Bangle.drawWidgets(); -checkMessages(); +setTimeout(() => { + checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:1}); +},10); // if checkMessages wants to 'load', do that diff --git a/apps/messages/lib.js b/apps/messages/lib.js index f3ea242e5..3094b34e1 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -17,7 +17,10 @@ exports.pushMessage = function(event) { mIdx=-1; } else { // add/modify if (event.t=="add") event.new=true; // new message - if (mIdx<0) mIdx=messages.push(event)-1; + if (mIdx<0) { + mIdx=0; + messages.unshift(event); // add new messages to the beginning + } else Object.assign(messages[mIdx], event); } require("Storage").writeJSON("messages.json",messages); @@ -28,10 +31,25 @@ exports.pushMessage = function(event) { // otherwise load after a delay, to ensure we have all the messages if (exports.messageTimeout) clearTimeout(exports.messageTimeout); exports.messageTimeout = setTimeout(function() { - exports.messageTimeout = undefined; + exports.messageTimeout = undefined; // if we're in a clock or it's important, go straight to messages app if (Bangle.CLOCK || event.important) return load("messages.app.js"); if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know - WIDGETS.messages.newMessage(); + WIDGETS.messages.show(); }, 500); } +exports.clearAll = function(event) { + var messages, inApp = "undefined"!=typeof MESSAGES; + if (inApp) { + MESSAGES = []; + messages = MESSAGES; // we're in an app that has already loaded messages + } else // no app - empty messages + messages = []; + // Save all messages + require("Storage").writeJSON("messages.json",messages); + // update app if in app + if (inApp) return onMessagesModified(); + // if we have a widget, update it + if (global.WIDGETS && WIDGETS.messages) + WIDGETS.messages.hide(); +} diff --git a/apps/messages/settings.js b/apps/messages/settings.js new file mode 100644 index 000000000..ef6266cf6 --- /dev/null +++ b/apps/messages/settings.js @@ -0,0 +1,35 @@ +(function(back) { + function settings() { + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + if (settings.vibrate===undefined) settings.vibrate="."; + if (settings.repeat===undefined) settings.repeat=4; + return settings; + } + function updateSetting(setting, value) { + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + settings[setting] = value; + require('Storage').writeJSON("messages.settings.json", settings); + } + + var vibPatterns = ["Off", ".", "-", "--", "-.-", "---"]; + var currentVib = settings().vibrate; + var mainmenu = { + "" : { "title" : "Messages" }, + "< Back" : back, + 'Vibrate': { + value: Math.max(0,vibPatterns.indexOf(settings().vibrate)), + min: 0, max: vibPatterns.length, + format: v => vibPatterns[v]||"Off", + onchange: v => { + updateSetting("vibrate", vibPatterns[v]); + } + }, + 'Repeat': { + value: settings().repeat, + min: 2, max: 10, + format: v => v+"s", + onchange: v => updateSetting("repeat", v) + }, + }; + E.showMenu(mainmenu); +}) diff --git a/apps/messages/widget.js b/apps/messages/widget.js index eda4a85a5..3a22b40fd 100644 --- a/apps/messages/widget.js +++ b/apps/messages/widget.js @@ -1,3 +1,4 @@ + WIDGETS["messages"]={area:"tl",width:0,draw:function() { if (!this.width) return; var c = (Date.now()-this.t)/1000; @@ -5,16 +6,31 @@ WIDGETS["messages"]={area:"tl",width:0,draw:function() { g.clearRect(this.x,this.y,this.x+this.width,this.y+23); g.setFont("6x8:1x2").setFontAlign(0,0).drawString("MESSAGES", this.x+this.width/2, this.y+12); //if (c<60) Bangle.setLCDPower(1); // keep LCD on for 1 minute - if (c<120 && (Date.now()-this.l)>4000) { + let settings = require('Storage').readJSON("messages.settings.json", true) || {}; + if (settings.repeat===undefined) settings.repeat = 4; + if (c<120 && (Date.now()-this.l)>settings.repeat*1000) { this.l = Date.now(); - Bangle.buzz(); // buzz every 4 seconds + WIDGETS["messages"].buzz(); // buzz every 4 seconds } setTimeout(()=>WIDGETS["messages"].draw(), 1000); -},newMessage:function() { +},show:function() { WIDGETS["messages"].t=Date.now(); // first time WIDGETS["messages"].l=Date.now()-10000; // last buzz - if (WIDGETS["messages"].c!==undefined) return; // already called WIDGETS["messages"].width=64; Bangle.drawWidgets(); Bangle.setLCDPower(1);// turns screen on +},hide:function() { + delete WIDGETS["messages"].t; + delete WIDGETS["messages"].l; + WIDGETS["messages"].width=0; + Bangle.drawWidgets(); +},buzz:function() { + let v = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate || "."; + function b() { + var c = v[0]; + v = v.substr(1); + if (c==".") Bangle.buzz().then(()=>setTimeout(b,100)); + if (c=="-") Bangle.buzz(500).then(()=>setTimeout(b,100)); + } + b(); }}; diff --git a/apps/metronome/ChangeLog b/apps/metronome/ChangeLog index 894d62940..9bd33ca4e 100644 --- a/apps/metronome/ChangeLog +++ b/apps/metronome/ChangeLog @@ -4,3 +4,4 @@ 0.04: App shows instructions, Widgets remain visible, color changed 0.05: Buzz intensity and beats per bar can be changed via settings-app 0.06: Correct string position +0.07: Add support for Bangle.sjs2 \ No newline at end of file diff --git a/apps/metronome/README.md b/apps/metronome/README.md index f67b4adf1..05bd62a96 100644 --- a/apps/metronome/README.md +++ b/apps/metronome/README.md @@ -4,11 +4,12 @@ This metronome makes your watch blink and vibrate with a given rate. ## Usage -* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat. -* Use `BTN1` to increase the bmp value by one. -* Use `BTN3` to decrease the bmp value by one. +* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bpm while the text blinks and the watch softly vibrates with every beat. +* Use `BTN1` to increase the bpm value by one. +* Use `BTN3` to decrease the bpm value by one. * You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`. * Intensity of buzzing and the beats per bar (default 4) can be changed with the settings-app. The first beat per bar will be marked in red. +* On Bangle.js 2 tapping the center of the screen initiates bpm. in- or decreasing bpm can by 1 can be done by tapping left or right site of the screen. ## Attributions diff --git a/apps/metronome/bangle1-metronome-screenshot.png b/apps/metronome/bangle1-metronome-screenshot.png new file mode 100644 index 000000000..1d684235d Binary files /dev/null and b/apps/metronome/bangle1-metronome-screenshot.png differ diff --git a/apps/metronome/metronome.js b/apps/metronome/metronome.js index e5e45559e..ffcaa1cfb 100644 --- a/apps/metronome/metronome.js +++ b/apps/metronome/metronome.js @@ -3,10 +3,9 @@ var cindex=0; // index to iterate through colous var bpm=60; // ininital bpm value var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm var tindex=0; //index to iterate through time_diffs - - -Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app - +// set background colour +g.setTheme({bg:"#000"}); +Bangle.setLCDTimeout(undefined); //do not deactivate display while running this app const storage = require("Storage"); const SETTINGS_FILE = 'metronome.settings.json'; @@ -15,7 +14,7 @@ function setting(key) { //define default settings const DEFAULTS = { 'beatsperbar': 4, - 'buzzintens': 0.75, + 'buzzintens': 1.0, }; if (!settings) { loadSettings(); } return (key in settings) ? settings[key] : DEFAULTS[key]; @@ -40,6 +39,10 @@ function changecolor() { 7: { value: 0xFFFF, name: "White" }, }; g.setColor(colors[cindex].value); + if ((process.env.HWVERSION==2 )) { + g.drawLine(39,0,39,g.getWidth()/3); + g.drawLine(136,0,136,g.getWidth()/3); + } if (cindex == setting('beatsperbar')-1) { cindex = 0; } @@ -50,43 +53,73 @@ function changecolor() { } function updateScreen() { - g.reset().clearRect(0, 50, 250, 150); + g.reset().clearRect(0, 50, 250, 120); changecolor(); try { Bangle.buzz(50, setting('buzzintens')); } catch(err) { } g.setFont("Vector",40).setFontAlign(0,0); - g.drawString(Math.floor(bpm)+"bpm", g.getWidth()/2, 100); + g.drawString(Math.floor(bpm)+"bpm", g.getWidth()/2, g.getWidth()/2); } -Bangle.on('touch', function(button) { -// setting bpm by tapping the screen. Uses the mean time difference between several tappings. - if (tindex < time_diffs.length) { - if (Date.now()-tStart < 5000) { - time_diffs[tindex] = Date.now()-tStart; - } - } else { - tindex=0; - time_diffs[tindex] = Date.now()-tStart; - } - tindex += 1; - mean_time = 0.0; - for(count = 0; count < time_diffs.length; count++) { - mean_time += time_diffs[count]; - } - time_diff = mean_time/count; +//Write user instructuins to screen +function printInstructions() { + g.clear(1).setFont("4x6"); + g.setColor(-1); //set color to white + g.drawString('Drum the beat on the center\nof the screen to set tempo.', 30, g.getWidth()/3*2+15); + if(process.env.HWVERSION==1) { + g.drawString('Use BTN1 to increase, and\nBTN3 to decrease bpm value by 1.', 30, g.getWidth()/3*2+30); + } + else { + g.drawString('Touch left part of the screen\nto decrease, or the right site\nto increase bpm value by 1.', 30, g.getWidth()/3*2+30); + } +} - tStart = Date.now(); - clearInterval(time_diff); - bpm = (60 * 1000/(time_diff)); - updateScreen(); - clearInterval(interval); - interval = setInterval(updateScreen, 60000 / bpm); - return bpm; +Bangle.on('touch', function(zone, e) { +// setting bpm by tapping the screen. Uses the mean time difference between several tappings. + if ((process.env.HWVERSION==2 && e.x > 39 && e.x < 136) || process.env.HWVERSION==1){ + if (tindex < time_diffs.length) { + if (Date.now()-tStart < 5000) { + time_diffs[tindex] = Date.now()-tStart; + } + } else { + tindex=0; + time_diffs[tindex] = Date.now()-tStart; + } + tindex += 1; + mean_time = 0.0; + for (count = 0; count < time_diffs.length; count++) { + mean_time += time_diffs[count]; + } + time_diff = mean_time/count; + + tStart = Date.now(); + clearInterval(time_diff); + bpm = (60 * 1000/(time_diff)); + updateScreen(); + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + return bpm; + } + else if (e.x < 39) { + if (bpm > 1) { + bpm -= 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + } + } + else if (e.x > 136) { + if (bpm > 1) { + bpm += 1; + clearInterval(interval); + interval = setInterval(updateScreen, 60000 / bpm); + }} }); -// enable bpm finetuning via buttons. + +// enable bpm finetuning +if ((process.env.HWVERSION==1)) { setWatch(() => { bpm += 1; clearInterval(interval); @@ -101,10 +134,10 @@ setWatch(() => { } }, BTN3, {repeat:true}); +} interval = setInterval(updateScreen, 60000 / bpm); +printInstructions(); -g.clear(1).setFont("6x8"); -g.drawString('Touch the screen to set tempo.\nUse BTN1 to increase, and\nBTN3 to decrease bpm value by 1.', 25, 200); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/miclock/bangle1-mixed-clock-screenshot.png b/apps/miclock/bangle1-mixed-clock-screenshot.png new file mode 100644 index 000000000..079aa17df Binary files /dev/null and b/apps/miclock/bangle1-mixed-clock-screenshot.png differ diff --git a/apps/miclock2/bangle1-mixed-clock-2-screenshot.png b/apps/miclock2/bangle1-mixed-clock-2-screenshot.png new file mode 100644 index 000000000..29a9819c4 Binary files /dev/null and b/apps/miclock2/bangle1-mixed-clock-2-screenshot.png differ diff --git a/apps/minionclk/bangle1-minion-clock-screenshot.png b/apps/minionclk/bangle1-minion-clock-screenshot.png new file mode 100644 index 000000000..87038aa46 Binary files /dev/null and b/apps/minionclk/bangle1-minion-clock-screenshot.png differ diff --git a/apps/moonphase/bangle1-moon-phase-screenshot.png b/apps/moonphase/bangle1-moon-phase-screenshot.png new file mode 100644 index 000000000..1462cb1b3 Binary files /dev/null and b/apps/moonphase/bangle1-moon-phase-screenshot.png differ diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog new file mode 100644 index 000000000..7b83706bf --- /dev/null +++ b/apps/mylocation/ChangeLog @@ -0,0 +1 @@ +0.01: First release diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md new file mode 100644 index 000000000..fd597397a --- /dev/null +++ b/apps/mylocation/README.md @@ -0,0 +1,41 @@ +# My Location + + *Sets and stores GPS lat and lon of your preferred city* + +* Select one of the preset Cities or setup through the GPS +* Other Apps can read this information to do calculations based on location +* When the City shows ??? it means the location has been set through the GPS + +## Example Code + + const LOCATION_FILE = "mylocation.json"; + let location; + + // requires the myLocation app + function loadLocation() { + location = require("Storage").readJSON(LOCATION_FILE,1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + } + +## Screenshots + +### Select one of the Preset Cities + +* The presets are London, Newcastle, Edinburgh, Paris, New York, Tokyo + + + +### Or select 'Set By GPS' to start the GPS + + + +### While the GPS is running you will see: + + + +### When a GPS fix is received you will see: + + + + + +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/) diff --git a/apps/mylocation/mylocation.app.js b/apps/mylocation/mylocation.app.js new file mode 100644 index 000000000..fb2f73fa7 --- /dev/null +++ b/apps/mylocation/mylocation.app.js @@ -0,0 +1,75 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +const SETTINGS_FILE = "mylocation.json"; +let settings; + +// initialize with default settings... +let s = { + 'lat': 51.5072, + 'lon': 0.1276, + 'location': "London" +} + +function loadSettings() { + settings = require('Storage').readJSON(SETTINGS_FILE, 1) || s; +} + +function save() { + settings = s + require('Storage').write(SETTINGS_FILE, settings) +} + +const locations = ["London", "Newcastle", "Edinburgh", "Paris", "New York", "Tokyo","???"]; +const lats = [51.5072 ,54.9783 ,55.9533 ,48.8566 ,40.7128 ,35.6762, 0.0]; +const lons = [-0.1276 ,-1.6178 ,-3.1883 ,2.3522 , -74.0060 ,139.6503, 0.0]; + +function setFromGPS() { + Bangle.on('GPS', (gps) => { + //console.log("."); + if (gps.fix === 0) return; + //console.log("fix from GPS"); + s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' } + Bangle.buzz(1500); // buzz on first position + Bangle.setGPSPower(0); + save(); + + Bangle.setUI("updown", ()=>{ load() }); + E.showPrompt("Location has been saved from the GPS fix",{ + title:"Location Saved", + buttons : {"OK":1} + }).then(function(v) { + load(); // load default clock + }); + }); + + Bangle.setGPSPower(1); + E.showMessage("Waiting for GPS fix. Place watch in the open. Could take 10 minutes. Long press to abort", "GPS Running"); + Bangle.setUI("updown", undefined); +} + +function showMainMenu() { + console.log("showMainMenu"); + const mainmenu = { + '': { 'title': 'My Location' }, + 'Create your events on the week shown. Keep in note that your events repeat weekly.
+One you have created your events, Click .
+All day events are not supported. A feature that lets you get the calendar from your watch will be added in a future update.
+3;)e.pop()();if(e[1]
';
+ el.querySelector('table').style.height = '100px';
+ el.querySelector('div').style.height = '100%';
+ document.body.appendChild(el);
+ var div = el.querySelector('div');
+ var possible = div.offsetHeight > 0;
+ document.body.removeChild(el);
+ return possible;
+ }
+
+ var EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere
+ var Splitter = /** @class */ (function () {
+ function Splitter() {
+ this.getKeysForEventDefs = memoize(this._getKeysForEventDefs);
+ this.splitDateSelection = memoize(this._splitDateSpan);
+ this.splitEventStore = memoize(this._splitEventStore);
+ this.splitIndividualUi = memoize(this._splitIndividualUi);
+ this.splitEventDrag = memoize(this._splitInteraction);
+ this.splitEventResize = memoize(this._splitInteraction);
+ this.eventUiBuilders = {}; // TODO: typescript protection
+ }
+ Splitter.prototype.splitProps = function (props) {
+ var _this = this;
+ var keyInfos = this.getKeyInfo(props);
+ var defKeys = this.getKeysForEventDefs(props.eventStore);
+ var dateSelections = this.splitDateSelection(props.dateSelection);
+ var individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases*
+ var eventStores = this.splitEventStore(props.eventStore, defKeys);
+ var eventDrags = this.splitEventDrag(props.eventDrag);
+ var eventResizes = this.splitEventResize(props.eventResize);
+ var splitProps = {};
+ this.eventUiBuilders = mapHash(keyInfos, function (info, key) { return _this.eventUiBuilders[key] || memoize(buildEventUiForKey); });
+ for (var key in keyInfos) {
+ var keyInfo = keyInfos[key];
+ var eventStore = eventStores[key] || EMPTY_EVENT_STORE;
+ var buildEventUi = this.eventUiBuilders[key];
+ splitProps[key] = {
+ businessHours: keyInfo.businessHours || props.businessHours,
+ dateSelection: dateSelections[key] || null,
+ eventStore: eventStore,
+ eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]),
+ eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
+ eventDrag: eventDrags[key] || null,
+ eventResize: eventResizes[key] || null,
+ };
+ }
+ return splitProps;
+ };
+ Splitter.prototype._splitDateSpan = function (dateSpan) {
+ var dateSpans = {};
+ if (dateSpan) {
+ var keys = this.getKeysForDateSpan(dateSpan);
+ for (var _i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
+ var key = keys_1[_i];
+ dateSpans[key] = dateSpan;
+ }
+ }
+ return dateSpans;
+ };
+ Splitter.prototype._getKeysForEventDefs = function (eventStore) {
+ var _this = this;
+ return mapHash(eventStore.defs, function (eventDef) { return _this.getKeysForEventDef(eventDef); });
+ };
+ Splitter.prototype._splitEventStore = function (eventStore, defKeys) {
+ var defs = eventStore.defs, instances = eventStore.instances;
+ var splitStores = {};
+ for (var defId in defs) {
+ for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) {
+ var key = _a[_i];
+ if (!splitStores[key]) {
+ splitStores[key] = createEmptyEventStore();
+ }
+ splitStores[key].defs[defId] = defs[defId];
+ }
+ }
+ for (var instanceId in instances) {
+ var instance = instances[instanceId];
+ for (var _b = 0, _c = defKeys[instance.defId]; _b < _c.length; _b++) {
+ var key = _c[_b];
+ if (splitStores[key]) { // must have already been created
+ splitStores[key].instances[instanceId] = instance;
+ }
+ }
+ }
+ return splitStores;
+ };
+ Splitter.prototype._splitIndividualUi = function (eventUiBases, defKeys) {
+ var splitHashes = {};
+ for (var defId in eventUiBases) {
+ if (defId) { // not the '' key
+ for (var _i = 0, _a = defKeys[defId]; _i < _a.length; _i++) {
+ var key = _a[_i];
+ if (!splitHashes[key]) {
+ splitHashes[key] = {};
+ }
+ splitHashes[key][defId] = eventUiBases[defId];
+ }
+ }
+ }
+ return splitHashes;
+ };
+ Splitter.prototype._splitInteraction = function (interaction) {
+ var splitStates = {};
+ if (interaction) {
+ var affectedStores_1 = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents));
+ // can't rely on defKeys because event data is mutated
+ var mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents);
+ var mutatedStores_1 = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId);
+ var populate = function (key) {
+ if (!splitStates[key]) {
+ splitStates[key] = {
+ affectedEvents: affectedStores_1[key] || EMPTY_EVENT_STORE,
+ mutatedEvents: mutatedStores_1[key] || EMPTY_EVENT_STORE,
+ isEvent: interaction.isEvent,
+ };
+ }
+ };
+ for (var key in affectedStores_1) {
+ populate(key);
+ }
+ for (var key in mutatedStores_1) {
+ populate(key);
+ }
+ }
+ return splitStates;
+ };
+ return Splitter;
+ }());
+ function buildEventUiForKey(allUi, eventUiForKey, individualUi) {
+ var baseParts = [];
+ if (allUi) {
+ baseParts.push(allUi);
+ }
+ if (eventUiForKey) {
+ baseParts.push(eventUiForKey);
+ }
+ var stuff = {
+ '': combineEventUis(baseParts),
+ };
+ if (individualUi) {
+ __assign(stuff, individualUi);
+ }
+ return stuff;
+ }
+
+ function getDateMeta(date, todayRange, nowDate, dateProfile) {
+ return {
+ dow: date.getUTCDay(),
+ isDisabled: Boolean(dateProfile && !rangeContainsMarker(dateProfile.activeRange, date)),
+ isOther: Boolean(dateProfile && !rangeContainsMarker(dateProfile.currentRange, date)),
+ isToday: Boolean(todayRange && rangeContainsMarker(todayRange, date)),
+ isPast: Boolean(nowDate ? (date < nowDate) : todayRange ? (date < todayRange.start) : false),
+ isFuture: Boolean(nowDate ? (date > nowDate) : todayRange ? (date >= todayRange.end) : false),
+ };
+ }
+ function getDayClassNames(meta, theme) {
+ var classNames = [
+ 'fc-day',
+ "fc-day-" + DAY_IDS[meta.dow],
+ ];
+ if (meta.isDisabled) {
+ classNames.push('fc-day-disabled');
+ }
+ else {
+ if (meta.isToday) {
+ classNames.push('fc-day-today');
+ classNames.push(theme.getClass('today'));
+ }
+ if (meta.isPast) {
+ classNames.push('fc-day-past');
+ }
+ if (meta.isFuture) {
+ classNames.push('fc-day-future');
+ }
+ if (meta.isOther) {
+ classNames.push('fc-day-other');
+ }
+ }
+ return classNames;
+ }
+ function getSlotClassNames(meta, theme) {
+ var classNames = [
+ 'fc-slot',
+ "fc-slot-" + DAY_IDS[meta.dow],
+ ];
+ if (meta.isDisabled) {
+ classNames.push('fc-slot-disabled');
+ }
+ else {
+ if (meta.isToday) {
+ classNames.push('fc-slot-today');
+ classNames.push(theme.getClass('today'));
+ }
+ if (meta.isPast) {
+ classNames.push('fc-slot-past');
+ }
+ if (meta.isFuture) {
+ classNames.push('fc-slot-future');
+ }
+ }
+ return classNames;
+ }
+
+ function buildNavLinkData(date, type) {
+ if (type === void 0) { type = 'day'; }
+ return JSON.stringify({
+ date: formatDayString(date),
+ type: type,
+ });
+ }
+
+ var _isRtlScrollbarOnLeft = null;
+ function getIsRtlScrollbarOnLeft() {
+ if (_isRtlScrollbarOnLeft === null) {
+ _isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft();
+ }
+ return _isRtlScrollbarOnLeft;
+ }
+ function computeIsRtlScrollbarOnLeft() {
+ var outerEl = document.createElement('div');
+ applyStyle(outerEl, {
+ position: 'absolute',
+ top: -1000,
+ left: 0,
+ border: 0,
+ padding: 0,
+ overflow: 'scroll',
+ direction: 'rtl',
+ });
+ outerEl.innerHTML = '';
+ document.body.appendChild(outerEl);
+ var innerEl = outerEl.firstChild;
+ var res = innerEl.getBoundingClientRect().left > outerEl.getBoundingClientRect().left;
+ removeElement(outerEl);
+ return res;
+ }
+
+ var _scrollbarWidths;
+ function getScrollbarWidths() {
+ if (!_scrollbarWidths) {
+ _scrollbarWidths = computeScrollbarWidths();
+ }
+ return _scrollbarWidths;
+ }
+ function computeScrollbarWidths() {
+ var el = document.createElement('div');
+ el.style.overflow = 'scroll';
+ el.style.position = 'absolute';
+ el.style.top = '-9999px';
+ el.style.left = '-9999px';
+ document.body.appendChild(el);
+ var res = computeScrollbarWidthsForEl(el);
+ document.body.removeChild(el);
+ return res;
+ }
+ // WARNING: will include border
+ function computeScrollbarWidthsForEl(el) {
+ return {
+ x: el.offsetHeight - el.clientHeight,
+ y: el.offsetWidth - el.clientWidth,
+ };
+ }
+
+ function computeEdges(el, getPadding) {
+ if (getPadding === void 0) { getPadding = false; }
+ var computedStyle = window.getComputedStyle(el);
+ var borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0;
+ var borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0;
+ var borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0;
+ var borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
+ var badScrollbarWidths = computeScrollbarWidthsForEl(el); // includes border!
+ var scrollbarLeftRight = badScrollbarWidths.y - borderLeft - borderRight;
+ var scrollbarBottom = badScrollbarWidths.x - borderTop - borderBottom;
+ var res = {
+ borderLeft: borderLeft,
+ borderRight: borderRight,
+ borderTop: borderTop,
+ borderBottom: borderBottom,
+ scrollbarBottom: scrollbarBottom,
+ scrollbarLeft: 0,
+ scrollbarRight: 0,
+ };
+ if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side?
+ res.scrollbarLeft = scrollbarLeftRight;
+ }
+ else {
+ res.scrollbarRight = scrollbarLeftRight;
+ }
+ if (getPadding) {
+ res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;
+ res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;
+ res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
+ res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
+ }
+ return res;
+ }
+ function computeInnerRect(el, goWithinPadding, doFromWindowViewport) {
+ if (goWithinPadding === void 0) { goWithinPadding = false; }
+ var outerRect = doFromWindowViewport ? el.getBoundingClientRect() : computeRect(el);
+ var edges = computeEdges(el, goWithinPadding);
+ var res = {
+ left: outerRect.left + edges.borderLeft + edges.scrollbarLeft,
+ right: outerRect.right - edges.borderRight - edges.scrollbarRight,
+ top: outerRect.top + edges.borderTop,
+ bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom,
+ };
+ if (goWithinPadding) {
+ res.left += edges.paddingLeft;
+ res.right -= edges.paddingRight;
+ res.top += edges.paddingTop;
+ res.bottom -= edges.paddingBottom;
+ }
+ return res;
+ }
+ function computeRect(el) {
+ var rect = el.getBoundingClientRect();
+ return {
+ left: rect.left + window.pageXOffset,
+ top: rect.top + window.pageYOffset,
+ right: rect.right + window.pageXOffset,
+ bottom: rect.bottom + window.pageYOffset,
+ };
+ }
+ function computeClippedClientRect(el) {
+ var clippingParents = getClippingParents(el);
+ var rect = el.getBoundingClientRect();
+ for (var _i = 0, clippingParents_1 = clippingParents; _i < clippingParents_1.length; _i++) {
+ var clippingParent = clippingParents_1[_i];
+ var intersection = intersectRects(rect, clippingParent.getBoundingClientRect());
+ if (intersection) {
+ rect = intersection;
+ }
+ else {
+ return null;
+ }
+ }
+ return rect;
+ }
+ function computeHeightAndMargins(el) {
+ return el.getBoundingClientRect().height + computeVMargins(el);
+ }
+ function computeVMargins(el) {
+ var computed = window.getComputedStyle(el);
+ return parseInt(computed.marginTop, 10) +
+ parseInt(computed.marginBottom, 10);
+ }
+ // does not return window
+ function getClippingParents(el) {
+ var parents = [];
+ while (el instanceof HTMLElement) { // will stop when gets to document or null
+ var computedStyle = window.getComputedStyle(el);
+ if (computedStyle.position === 'fixed') {
+ break;
+ }
+ if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) {
+ parents.push(el);
+ }
+ el = el.parentNode;
+ }
+ return parents;
+ }
+
+ // given a function that resolves a result asynchronously.
+ // the function can either call passed-in success and failure callbacks,
+ // or it can return a promise.
+ // if you need to pass additional params to func, bind them first.
+ function unpromisify(func, success, failure) {
+ // guard against success/failure callbacks being called more than once
+ // and guard against a promise AND callback being used together.
+ var isResolved = false;
+ var wrappedSuccess = function () {
+ if (!isResolved) {
+ isResolved = true;
+ success.apply(this, arguments); // eslint-disable-line prefer-rest-params
+ }
+ };
+ var wrappedFailure = function () {
+ if (!isResolved) {
+ isResolved = true;
+ if (failure) {
+ failure.apply(this, arguments); // eslint-disable-line prefer-rest-params
+ }
+ }
+ };
+ var res = func(wrappedSuccess, wrappedFailure);
+ if (res && typeof res.then === 'function') {
+ res.then(wrappedSuccess, wrappedFailure);
+ }
+ }
+
+ var Emitter = /** @class */ (function () {
+ function Emitter() {
+ this.handlers = {};
+ this.thisContext = null;
+ }
+ Emitter.prototype.setThisContext = function (thisContext) {
+ this.thisContext = thisContext;
+ };
+ Emitter.prototype.setOptions = function (options) {
+ this.options = options;
+ };
+ Emitter.prototype.on = function (type, handler) {
+ addToHash(this.handlers, type, handler);
+ };
+ Emitter.prototype.off = function (type, handler) {
+ removeFromHash(this.handlers, type, handler);
+ };
+ Emitter.prototype.trigger = function (type) {
+ var args = [];
+ for (var _i = 1; _i < arguments.length; _i++) {
+ args[_i - 1] = arguments[_i];
+ }
+ var attachedHandlers = this.handlers[type] || [];
+ var optionHandler = this.options && this.options[type];
+ var handlers = [].concat(optionHandler || [], attachedHandlers);
+ for (var _a = 0, handlers_1 = handlers; _a < handlers_1.length; _a++) {
+ var handler = handlers_1[_a];
+ handler.apply(this.thisContext, args);
+ }
+ };
+ Emitter.prototype.hasHandlers = function (type) {
+ return (this.handlers[type] && this.handlers[type].length) ||
+ (this.options && this.options[type]);
+ };
+ return Emitter;
+ }());
+ function addToHash(hash, type, handler) {
+ (hash[type] || (hash[type] = []))
+ .push(handler);
+ }
+ function removeFromHash(hash, type, handler) {
+ if (handler) {
+ if (hash[type]) {
+ hash[type] = hash[type].filter(function (func) { return func !== handler; });
+ }
+ }
+ else {
+ delete hash[type]; // remove all handler funcs for this type
+ }
+ }
+
+ /*
+ Records offset information for a set of elements, relative to an origin element.
+ Can record the left/right OR the top/bottom OR both.
+ Provides methods for querying the cache by position.
+ */
+ var PositionCache = /** @class */ (function () {
+ function PositionCache(originEl, els, isHorizontal, isVertical) {
+ this.els = els;
+ var originClientRect = this.originClientRect = originEl.getBoundingClientRect(); // relative to viewport top-left
+ if (isHorizontal) {
+ this.buildElHorizontals(originClientRect.left);
+ }
+ if (isVertical) {
+ this.buildElVerticals(originClientRect.top);
+ }
+ }
+ // Populates the left/right internal coordinate arrays
+ PositionCache.prototype.buildElHorizontals = function (originClientLeft) {
+ var lefts = [];
+ var rights = [];
+ for (var _i = 0, _a = this.els; _i < _a.length; _i++) {
+ var el = _a[_i];
+ var rect = el.getBoundingClientRect();
+ lefts.push(rect.left - originClientLeft);
+ rights.push(rect.right - originClientLeft);
+ }
+ this.lefts = lefts;
+ this.rights = rights;
+ };
+ // Populates the top/bottom internal coordinate arrays
+ PositionCache.prototype.buildElVerticals = function (originClientTop) {
+ var tops = [];
+ var bottoms = [];
+ for (var _i = 0, _a = this.els; _i < _a.length; _i++) {
+ var el = _a[_i];
+ var rect = el.getBoundingClientRect();
+ tops.push(rect.top - originClientTop);
+ bottoms.push(rect.bottom - originClientTop);
+ }
+ this.tops = tops;
+ this.bottoms = bottoms;
+ };
+ // Given a left offset (from document left), returns the index of the el that it horizontally intersects.
+ // If no intersection is made, returns undefined.
+ PositionCache.prototype.leftToIndex = function (leftPosition) {
+ var _a = this, lefts = _a.lefts, rights = _a.rights;
+ var len = lefts.length;
+ var i;
+ for (i = 0; i < len; i += 1) {
+ if (leftPosition >= lefts[i] && leftPosition < rights[i]) {
+ return i;
+ }
+ }
+ return undefined; // TODO: better
+ };
+ // Given a top offset (from document top), returns the index of the el that it vertically intersects.
+ // If no intersection is made, returns undefined.
+ PositionCache.prototype.topToIndex = function (topPosition) {
+ var _a = this, tops = _a.tops, bottoms = _a.bottoms;
+ var len = tops.length;
+ var i;
+ for (i = 0; i < len; i += 1) {
+ if (topPosition >= tops[i] && topPosition < bottoms[i]) {
+ return i;
+ }
+ }
+ return undefined; // TODO: better
+ };
+ // Gets the width of the element at the given index
+ PositionCache.prototype.getWidth = function (leftIndex) {
+ return this.rights[leftIndex] - this.lefts[leftIndex];
+ };
+ // Gets the height of the element at the given index
+ PositionCache.prototype.getHeight = function (topIndex) {
+ return this.bottoms[topIndex] - this.tops[topIndex];
+ };
+ return PositionCache;
+ }());
+
+ /* eslint max-classes-per-file: "off" */
+ /*
+ An object for getting/setting scroll-related information for an element.
+ Internally, this is done very differently for window versus DOM element,
+ so this object serves as a common interface.
+ */
+ var ScrollController = /** @class */ (function () {
+ function ScrollController() {
+ }
+ ScrollController.prototype.getMaxScrollTop = function () {
+ return this.getScrollHeight() - this.getClientHeight();
+ };
+ ScrollController.prototype.getMaxScrollLeft = function () {
+ return this.getScrollWidth() - this.getClientWidth();
+ };
+ ScrollController.prototype.canScrollVertically = function () {
+ return this.getMaxScrollTop() > 0;
+ };
+ ScrollController.prototype.canScrollHorizontally = function () {
+ return this.getMaxScrollLeft() > 0;
+ };
+ ScrollController.prototype.canScrollUp = function () {
+ return this.getScrollTop() > 0;
+ };
+ ScrollController.prototype.canScrollDown = function () {
+ return this.getScrollTop() < this.getMaxScrollTop();
+ };
+ ScrollController.prototype.canScrollLeft = function () {
+ return this.getScrollLeft() > 0;
+ };
+ ScrollController.prototype.canScrollRight = function () {
+ return this.getScrollLeft() < this.getMaxScrollLeft();
+ };
+ return ScrollController;
+ }());
+ var ElementScrollController = /** @class */ (function (_super) {
+ __extends(ElementScrollController, _super);
+ function ElementScrollController(el) {
+ var _this = _super.call(this) || this;
+ _this.el = el;
+ return _this;
+ }
+ ElementScrollController.prototype.getScrollTop = function () {
+ return this.el.scrollTop;
+ };
+ ElementScrollController.prototype.getScrollLeft = function () {
+ return this.el.scrollLeft;
+ };
+ ElementScrollController.prototype.setScrollTop = function (top) {
+ this.el.scrollTop = top;
+ };
+ ElementScrollController.prototype.setScrollLeft = function (left) {
+ this.el.scrollLeft = left;
+ };
+ ElementScrollController.prototype.getScrollWidth = function () {
+ return this.el.scrollWidth;
+ };
+ ElementScrollController.prototype.getScrollHeight = function () {
+ return this.el.scrollHeight;
+ };
+ ElementScrollController.prototype.getClientHeight = function () {
+ return this.el.clientHeight;
+ };
+ ElementScrollController.prototype.getClientWidth = function () {
+ return this.el.clientWidth;
+ };
+ return ElementScrollController;
+ }(ScrollController));
+ var WindowScrollController = /** @class */ (function (_super) {
+ __extends(WindowScrollController, _super);
+ function WindowScrollController() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ WindowScrollController.prototype.getScrollTop = function () {
+ return window.pageYOffset;
+ };
+ WindowScrollController.prototype.getScrollLeft = function () {
+ return window.pageXOffset;
+ };
+ WindowScrollController.prototype.setScrollTop = function (n) {
+ window.scroll(window.pageXOffset, n);
+ };
+ WindowScrollController.prototype.setScrollLeft = function (n) {
+ window.scroll(n, window.pageYOffset);
+ };
+ WindowScrollController.prototype.getScrollWidth = function () {
+ return document.documentElement.scrollWidth;
+ };
+ WindowScrollController.prototype.getScrollHeight = function () {
+ return document.documentElement.scrollHeight;
+ };
+ WindowScrollController.prototype.getClientHeight = function () {
+ return document.documentElement.clientHeight;
+ };
+ WindowScrollController.prototype.getClientWidth = function () {
+ return document.documentElement.clientWidth;
+ };
+ return WindowScrollController;
+ }(ScrollController));
+
+ var Theme = /** @class */ (function () {
+ function Theme(calendarOptions) {
+ if (this.iconOverrideOption) {
+ this.setIconOverride(calendarOptions[this.iconOverrideOption]);
+ }
+ }
+ Theme.prototype.setIconOverride = function (iconOverrideHash) {
+ var iconClassesCopy;
+ var buttonName;
+ if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object
+ iconClassesCopy = __assign({}, this.iconClasses);
+ for (buttonName in iconOverrideHash) {
+ iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]);
+ }
+ this.iconClasses = iconClassesCopy;
+ }
+ else if (iconOverrideHash === false) {
+ this.iconClasses = {};
+ }
+ };
+ Theme.prototype.applyIconOverridePrefix = function (className) {
+ var prefix = this.iconOverridePrefix;
+ if (prefix && className.indexOf(prefix) !== 0) { // if not already present
+ className = prefix + className;
+ }
+ return className;
+ };
+ Theme.prototype.getClass = function (key) {
+ return this.classes[key] || '';
+ };
+ Theme.prototype.getIconClass = function (buttonName, isRtl) {
+ var className;
+ if (isRtl && this.rtlIconClasses) {
+ className = this.rtlIconClasses[buttonName] || this.iconClasses[buttonName];
+ }
+ else {
+ className = this.iconClasses[buttonName];
+ }
+ if (className) {
+ return this.baseIconClass + " " + className;
+ }
+ return '';
+ };
+ Theme.prototype.getCustomButtonIconClass = function (customButtonProps) {
+ var className;
+ if (this.iconOverrideCustomButtonOption) {
+ className = customButtonProps[this.iconOverrideCustomButtonOption];
+ if (className) {
+ return this.baseIconClass + " " + this.applyIconOverridePrefix(className);
+ }
+ }
+ return '';
+ };
+ return Theme;
+ }());
+ Theme.prototype.classes = {};
+ Theme.prototype.iconClasses = {};
+ Theme.prototype.baseIconClass = '';
+ Theme.prototype.iconOverridePrefix = '';
+
+ /// elements work best with integers. round up to ensure contents fits
+ }
+ function getSectionHasLiquidHeight(props, sectionConfig) {
+ return props.liquid && sectionConfig.liquid; // does the section do liquid-height? (need to have whole scrollgrid liquid-height as well)
+ }
+ function getAllowYScrolling(props, sectionConfig) {
+ return sectionConfig.maxHeight != null || // if its possible for the height to max out, we might need scrollbars
+ getSectionHasLiquidHeight(props, sectionConfig); // if the section is liquid height, it might condense enough to require scrollbars
+ }
+ // TODO: ONLY use `arg`. force out internal function to use same API
+ function renderChunkContent(sectionConfig, chunkConfig, arg) {
+ var expandRows = arg.expandRows;
+ var content = typeof chunkConfig.content === 'function' ?
+ chunkConfig.content(arg) :
+ createElement('table', {
+ className: [
+ chunkConfig.tableClassName,
+ sectionConfig.syncRowHeights ? 'fc-scrollgrid-sync-table' : '',
+ ].join(' '),
+ style: {
+ minWidth: arg.tableMinWidth,
+ width: arg.clientWidth,
+ height: expandRows ? arg.clientHeight : '', // css `height` on a
serves as a min-height
+ },
+ }, arg.tableColGroupNode, createElement('tbody', {}, typeof chunkConfig.rowContent === 'function' ? chunkConfig.rowContent(arg) : chunkConfig.rowContent));
+ return content;
+ }
+ function isColPropsEqual(cols0, cols1) {
+ return isArraysEqual(cols0, cols1, isPropsEqual);
+ }
+ function renderMicroColGroup(cols, shrinkWidth) {
+ var colNodes = [];
+ /*
+ for ColProps with spans, it would have been great to make a single
/ elements with colspans.
+ SOLUTION: making individual
+ _this.frameElRefs = new RefMap(); // the fc-daygrid-day-frame
+ _this.fgElRefs = new RefMap(); // the fc-daygrid-day-events
+ _this.segHarnessRefs = new RefMap(); // indexed by "instanceId:firstCol"
+ _this.rootElRef = createRef();
+ _this.state = {
+ framePositions: null,
+ maxContentHeight: null,
+ eventInstanceHeights: {},
+ };
+ return _this;
+ }
+ TableRow.prototype.render = function () {
+ var _this = this;
+ var _a = this, props = _a.props, state = _a.state, context = _a.context;
+ var options = context.options;
+ var colCnt = props.cells.length;
+ var businessHoursByCol = splitSegsByFirstCol(props.businessHourSegs, colCnt);
+ var bgEventSegsByCol = splitSegsByFirstCol(props.bgEventSegs, colCnt);
+ var highlightSegsByCol = splitSegsByFirstCol(this.getHighlightSegs(), colCnt);
+ var mirrorSegsByCol = splitSegsByFirstCol(this.getMirrorSegs(), colCnt);
+ var _b = computeFgSegPlacement(sortEventSegs(props.fgEventSegs, options.eventOrder), props.dayMaxEvents, props.dayMaxEventRows, options.eventOrderStrict, state.eventInstanceHeights, state.maxContentHeight, props.cells), singleColPlacements = _b.singleColPlacements, multiColPlacements = _b.multiColPlacements, moreCnts = _b.moreCnts, moreMarginTops = _b.moreMarginTops;
+ var isForcedInvisible = // TODO: messy way to compute this
+ (props.eventDrag && props.eventDrag.affectedInstances) ||
+ (props.eventResize && props.eventResize.affectedInstances) ||
+ {};
+ return (createElement("tr", { ref: this.rootElRef },
+ props.renderIntro && props.renderIntro(),
+ props.cells.map(function (cell, col) {
+ var normalFgNodes = _this.renderFgSegs(col, props.forPrint ? singleColPlacements[col] : multiColPlacements[col], props.todayRange, isForcedInvisible);
+ var mirrorFgNodes = _this.renderFgSegs(col, buildMirrorPlacements(mirrorSegsByCol[col], multiColPlacements), props.todayRange, {}, Boolean(props.eventDrag), Boolean(props.eventResize), false);
+ return (createElement(TableCell, { key: cell.key, elRef: _this.cellElRefs.createRef(cell.key), innerElRef: _this.frameElRefs.createRef(cell.key) /* FF problem, but okay to use for left/right. TODO: rename prop */, dateProfile: props.dateProfile, date: cell.date, showDayNumber: props.showDayNumbers, showWeekNumber: props.showWeekNumbers && col === 0, forceDayTop: props.showWeekNumbers /* even displaying weeknum for row, not necessarily day */, todayRange: props.todayRange, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, extraHookProps: cell.extraHookProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, extraDateSpan: cell.extraDateSpan, moreCnt: moreCnts[col], moreMarginTop: moreMarginTops[col], singlePlacements: singleColPlacements[col], fgContentElRef: _this.fgElRefs.createRef(cell.key), fgContent: ( // Fragment scopes the keys
+ createElement(Fragment, null,
+ createElement(Fragment, null, normalFgNodes),
+ createElement(Fragment, null, mirrorFgNodes))), bgContent: ( // Fragment scopes the keys
+ createElement(Fragment, null,
+ _this.renderFillSegs(highlightSegsByCol[col], 'highlight'),
+ _this.renderFillSegs(businessHoursByCol[col], 'non-business'),
+ _this.renderFillSegs(bgEventSegsByCol[col], 'bg-event'))) }));
+ })));
+ };
+ TableRow.prototype.componentDidMount = function () {
+ this.updateSizing(true);
+ };
+ TableRow.prototype.componentDidUpdate = function (prevProps, prevState) {
+ var currentProps = this.props;
+ this.updateSizing(!isPropsEqual(prevProps, currentProps));
+ };
+ TableRow.prototype.getHighlightSegs = function () {
+ var props = this.props;
+ if (props.eventDrag && props.eventDrag.segs.length) { // messy check
+ return props.eventDrag.segs;
+ }
+ if (props.eventResize && props.eventResize.segs.length) { // messy check
+ return props.eventResize.segs;
+ }
+ return props.dateSelectionSegs;
+ };
+ TableRow.prototype.getMirrorSegs = function () {
+ var props = this.props;
+ if (props.eventResize && props.eventResize.segs.length) { // messy check
+ return props.eventResize.segs;
+ }
+ return [];
+ };
+ TableRow.prototype.renderFgSegs = function (col, segPlacements, todayRange, isForcedInvisible, isDragging, isResizing, isDateSelecting) {
+ var context = this.context;
+ var eventSelection = this.props.eventSelection;
+ var framePositions = this.state.framePositions;
+ var defaultDisplayEventEnd = this.props.cells.length === 1; // colCnt === 1
+ var isMirror = isDragging || isResizing || isDateSelecting;
+ var nodes = [];
+ if (framePositions) {
+ for (var _i = 0, segPlacements_1 = segPlacements; _i < segPlacements_1.length; _i++) {
+ var placement = segPlacements_1[_i];
+ var seg = placement.seg;
+ var instanceId = seg.eventRange.instance.instanceId;
+ var key = instanceId + ':' + col;
+ var isVisible = placement.isVisible && !isForcedInvisible[instanceId];
+ var isAbsolute = placement.isAbsolute;
+ var left = '';
+ var right = '';
+ if (isAbsolute) {
+ if (context.isRtl) {
+ right = 0;
+ left = framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol];
+ }
+ else {
+ left = 0;
+ right = framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol];
+ }
+ }
+ /*
+ known bug: events that are force to be list-item but span multiple days still take up space in later columns
+ todo: in print view, for multi-day events, don't display title within non-start/end segs
+ */
+ nodes.push(createElement("div", { className: 'fc-daygrid-event-harness' + (isAbsolute ? ' fc-daygrid-event-harness-abs' : ''), key: key, ref: isMirror ? null : this.segHarnessRefs.createRef(key), style: {
+ visibility: isVisible ? '' : 'hidden',
+ marginTop: isAbsolute ? '' : placement.marginTop,
+ top: isAbsolute ? placement.absoluteTop : '',
+ left: left,
+ right: right,
+ } }, hasListItemDisplay(seg) ? (createElement(TableListItemEvent, __assign({ seg: seg, isDragging: isDragging, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange)))) : (createElement(TableBlockEvent, __assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange))))));
+ }
+ }
+ return nodes;
+ };
+ TableRow.prototype.renderFillSegs = function (segs, fillType) {
+ var isRtl = this.context.isRtl;
+ var todayRange = this.props.todayRange;
+ var framePositions = this.state.framePositions;
+ var nodes = [];
+ if (framePositions) {
+ for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
+ var seg = segs_1[_i];
+ var leftRightCss = isRtl ? {
+ right: 0,
+ left: framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol],
+ } : {
+ left: 0,
+ right: framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol],
+ };
+ nodes.push(createElement("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-daygrid-bg-harness", style: leftRightCss }, fillType === 'bg-event' ?
+ createElement(BgEvent, __assign({ seg: seg }, getSegMeta(seg, todayRange))) :
+ renderFill(fillType)));
+ }
+ }
+ return createElement.apply(void 0, __spreadArray([Fragment, {}], nodes));
+ };
+ TableRow.prototype.updateSizing = function (isExternalSizingChange) {
+ var _a = this, props = _a.props, frameElRefs = _a.frameElRefs;
+ if (!props.forPrint &&
+ props.clientWidth !== null // positioning ready?
+ ) {
+ if (isExternalSizingChange) {
+ var frameEls = props.cells.map(function (cell) { return frameElRefs.currentMap[cell.key]; });
+ if (frameEls.length) {
+ var originEl = this.rootElRef.current;
+ this.setState({
+ framePositions: new PositionCache(originEl, frameEls, true, // isHorizontal
+ false),
+ });
+ }
+ }
+ var limitByContentHeight = props.dayMaxEvents === true || props.dayMaxEventRows === true;
+ this.setState({
+ eventInstanceHeights: this.queryEventInstanceHeights(),
+ maxContentHeight: limitByContentHeight ? this.computeMaxContentHeight() : null,
+ });
+ }
+ };
+ TableRow.prototype.queryEventInstanceHeights = function () {
+ var segElMap = this.segHarnessRefs.currentMap;
+ var eventInstanceHeights = {};
+ // get the max height amongst instance segs
+ for (var key in segElMap) {
+ var height = Math.round(segElMap[key].getBoundingClientRect().height);
+ var instanceId = key.split(':')[0]; // deconstruct how renderFgSegs makes the key
+ eventInstanceHeights[instanceId] = Math.max(eventInstanceHeights[instanceId] || 0, height);
+ }
+ return eventInstanceHeights;
+ };
+ TableRow.prototype.computeMaxContentHeight = function () {
+ var firstKey = this.props.cells[0].key;
+ var cellEl = this.cellElRefs.currentMap[firstKey];
+ var fcContainerEl = this.fgElRefs.currentMap[firstKey];
+ return cellEl.getBoundingClientRect().bottom - fcContainerEl.getBoundingClientRect().top;
+ };
+ TableRow.prototype.getCellEls = function () {
+ var elMap = this.cellElRefs.currentMap;
+ return this.props.cells.map(function (cell) { return elMap[cell.key]; });
+ };
+ return TableRow;
+ }(DateComponent));
+ TableRow.addStateEquality({
+ eventInstanceHeights: isPropsEqual,
+ });
+ function buildMirrorPlacements(mirrorSegs, colPlacements) {
+ if (!mirrorSegs.length) {
+ return [];
+ }
+ var topsByInstanceId = buildAbsoluteTopHash(colPlacements); // TODO: cache this at first render?
+ return mirrorSegs.map(function (seg) { return ({
+ seg: seg,
+ isVisible: true,
+ isAbsolute: true,
+ absoluteTop: topsByInstanceId[seg.eventRange.instance.instanceId],
+ marginTop: 0,
+ }); });
+ }
+ function buildAbsoluteTopHash(colPlacements) {
+ var topsByInstanceId = {};
+ for (var _i = 0, colPlacements_1 = colPlacements; _i < colPlacements_1.length; _i++) {
+ var placements = colPlacements_1[_i];
+ for (var _a = 0, placements_1 = placements; _a < placements_1.length; _a++) {
+ var placement = placements_1[_a];
+ topsByInstanceId[placement.seg.eventRange.instance.instanceId] = placement.absoluteTop;
+ }
+ }
+ return topsByInstanceId;
+ }
+
+ var Table = /** @class */ (function (_super) {
+ __extends(Table, _super);
+ function Table() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.splitBusinessHourSegs = memoize(splitSegsByRow);
+ _this.splitBgEventSegs = memoize(splitSegsByRow);
+ _this.splitFgEventSegs = memoize(splitSegsByRow);
+ _this.splitDateSelectionSegs = memoize(splitSegsByRow);
+ _this.splitEventDrag = memoize(splitInteractionByRow);
+ _this.splitEventResize = memoize(splitInteractionByRow);
+ _this.rowRefs = new RefMap();
+ _this.handleRootEl = function (rootEl) {
+ _this.rootEl = rootEl;
+ if (rootEl) {
+ _this.context.registerInteractiveComponent(_this, {
+ el: rootEl,
+ isHitComboAllowed: _this.props.isHitComboAllowed,
+ });
+ }
+ else {
+ _this.context.unregisterInteractiveComponent(_this);
+ }
+ };
+ return _this;
+ }
+ Table.prototype.render = function () {
+ var _this = this;
+ var props = this.props;
+ var dateProfile = props.dateProfile, dayMaxEventRows = props.dayMaxEventRows, dayMaxEvents = props.dayMaxEvents, expandRows = props.expandRows;
+ var rowCnt = props.cells.length;
+ var businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCnt);
+ var bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCnt);
+ var fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCnt);
+ var dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCnt);
+ var eventDragByRow = this.splitEventDrag(props.eventDrag, rowCnt);
+ var eventResizeByRow = this.splitEventResize(props.eventResize, rowCnt);
+ var limitViaBalanced = dayMaxEvents === true || dayMaxEventRows === true;
+ // if rows can't expand to fill fixed height, can't do balanced-height event limit
+ // TODO: best place to normalize these options?
+ if (limitViaBalanced && !expandRows) {
+ limitViaBalanced = false;
+ dayMaxEventRows = null;
+ dayMaxEvents = null;
+ }
+ var classNames = [
+ 'fc-daygrid-body',
+ limitViaBalanced ? 'fc-daygrid-body-balanced' : 'fc-daygrid-body-unbalanced',
+ expandRows ? '' : 'fc-daygrid-body-natural', // will height of one row depend on the others?
+ ];
+ return (createElement("div", { className: classNames.join(' '), ref: this.handleRootEl, style: {
+ // these props are important to give this wrapper correct dimensions for interactions
+ // TODO: if we set it here, can we avoid giving to inner tables?
+ width: props.clientWidth,
+ minWidth: props.tableMinWidth,
+ } },
+ createElement(NowTimer, { unit: "day" }, function (nowDate, todayRange) { return (createElement(Fragment, null,
+ createElement("table", { className: "fc-scrollgrid-sync-table", style: {
+ width: props.clientWidth,
+ minWidth: props.tableMinWidth,
+ height: expandRows ? props.clientHeight : '',
+ } },
+ props.colGroupNode,
+ createElement("tbody", null, props.cells.map(function (cells, row) { return (createElement(TableRow, { ref: _this.rowRefs.createRef(row), key: cells.length
+ ? cells[0].date.toISOString() /* best? or put key on cell? or use diff formatter? */
+ : row // in case there are no cells (like when resource view is loading)
+ , showDayNumbers: rowCnt > 1, showWeekNumbers: props.showWeekNumbers, todayRange: todayRange, dateProfile: dateProfile, cells: cells, renderIntro: props.renderRowIntro, businessHourSegs: businessHourSegsByRow[row], eventSelection: props.eventSelection, bgEventSegs: bgEventSegsByRow[row].filter(isSegAllDay) /* hack */, fgEventSegs: fgEventSegsByRow[row], dateSelectionSegs: dateSelectionSegsByRow[row], eventDrag: eventDragByRow[row], eventResize: eventResizeByRow[row], dayMaxEvents: dayMaxEvents, dayMaxEventRows: dayMaxEventRows, clientWidth: props.clientWidth, clientHeight: props.clientHeight, forPrint: props.forPrint })); }))))); })));
+ };
+ // Hit System
+ // ----------------------------------------------------------------------------------------------------
+ Table.prototype.prepareHits = function () {
+ this.rowPositions = new PositionCache(this.rootEl, this.rowRefs.collect().map(function (rowObj) { return rowObj.getCellEls()[0]; }), // first cell el in each row. TODO: not optimal
+ false, true);
+ this.colPositions = new PositionCache(this.rootEl, this.rowRefs.currentMap[0].getCellEls(), // cell els in first row
+ true, // horizontal
+ false);
+ };
+ Table.prototype.queryHit = function (positionLeft, positionTop) {
+ var _a = this, colPositions = _a.colPositions, rowPositions = _a.rowPositions;
+ var col = colPositions.leftToIndex(positionLeft);
+ var row = rowPositions.topToIndex(positionTop);
+ if (row != null && col != null) {
+ var cell = this.props.cells[row][col];
+ return {
+ dateProfile: this.props.dateProfile,
+ dateSpan: __assign({ range: this.getCellRange(row, col), allDay: true }, cell.extraDateSpan),
+ dayEl: this.getCellEl(row, col),
+ rect: {
+ left: colPositions.lefts[col],
+ right: colPositions.rights[col],
+ top: rowPositions.tops[row],
+ bottom: rowPositions.bottoms[row],
+ },
+ layer: 0,
+ };
+ }
+ return null;
+ };
+ Table.prototype.getCellEl = function (row, col) {
+ return this.rowRefs.currentMap[row].getCellEls()[col]; // TODO: not optimal
+ };
+ Table.prototype.getCellRange = function (row, col) {
+ var start = this.props.cells[row][col].date;
+ var end = addDays(start, 1);
+ return { start: start, end: end };
+ };
+ return Table;
+ }(DateComponent));
+ function isSegAllDay(seg) {
+ return seg.eventRange.def.allDay;
+ }
+
+ var DayTableSlicer = /** @class */ (function (_super) {
+ __extends(DayTableSlicer, _super);
+ function DayTableSlicer() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.forceDayIfListItem = true;
+ return _this;
+ }
+ DayTableSlicer.prototype.sliceRange = function (dateRange, dayTableModel) {
+ return dayTableModel.sliceRange(dateRange);
+ };
+ return DayTableSlicer;
+ }(Slicer));
+
+ var DayTable = /** @class */ (function (_super) {
+ __extends(DayTable, _super);
+ function DayTable() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.slicer = new DayTableSlicer();
+ _this.tableRef = createRef();
+ return _this;
+ }
+ DayTable.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ return (createElement(Table, __assign({ ref: this.tableRef }, this.slicer.sliceProps(props, props.dateProfile, props.nextDayThreshold, context, props.dayTableModel), { dateProfile: props.dateProfile, cells: props.dayTableModel.cells, colGroupNode: props.colGroupNode, tableMinWidth: props.tableMinWidth, renderRowIntro: props.renderRowIntro, dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows, showWeekNumbers: props.showWeekNumbers, expandRows: props.expandRows, headerAlignElRef: props.headerAlignElRef, clientWidth: props.clientWidth, clientHeight: props.clientHeight, forPrint: props.forPrint })));
+ };
+ return DayTable;
+ }(DateComponent));
+
+ var DayTableView = /** @class */ (function (_super) {
+ __extends(DayTableView, _super);
+ function DayTableView() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.buildDayTableModel = memoize(buildDayTableModel);
+ _this.headerRef = createRef();
+ _this.tableRef = createRef();
+ return _this;
+ }
+ DayTableView.prototype.render = function () {
+ var _this = this;
+ var _a = this.context, options = _a.options, dateProfileGenerator = _a.dateProfileGenerator;
+ var props = this.props;
+ var dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator);
+ var headerContent = options.dayHeaders && (createElement(DayHeader, { ref: this.headerRef, dateProfile: props.dateProfile, dates: dayTableModel.headerDates, datesRepDistinctDays: dayTableModel.rowCnt === 1 }));
+ var bodyContent = function (contentArg) { return (createElement(DayTable, { ref: _this.tableRef, dateProfile: props.dateProfile, dayTableModel: dayTableModel, businessHours: props.businessHours, dateSelection: props.dateSelection, eventStore: props.eventStore, eventUiBases: props.eventUiBases, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, nextDayThreshold: options.nextDayThreshold, colGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, dayMaxEvents: options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows, showWeekNumbers: options.weekNumbers, expandRows: !props.isHeightAuto, headerAlignElRef: _this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint })); };
+ return options.dayMinWidth
+ ? this.renderHScrollLayout(headerContent, bodyContent, dayTableModel.colCnt, options.dayMinWidth)
+ : this.renderSimpleLayout(headerContent, bodyContent);
+ };
+ return DayTableView;
+ }(TableView));
+ function buildDayTableModel(dateProfile, dateProfileGenerator) {
+ var daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
+ return new DayTableModel(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit));
+ }
+
+ var TableDateProfileGenerator = /** @class */ (function (_super) {
+ __extends(TableDateProfileGenerator, _super);
+ function TableDateProfileGenerator() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ // Computes the date range that will be rendered.
+ TableDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) {
+ var dateEnv = this.props.dateEnv;
+ var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay);
+ var start = renderRange.start;
+ var end = renderRange.end;
+ var endOfWeek;
+ // year and month views should be aligned with weeks. this is already done for week
+ if (/^(year|month)$/.test(currentRangeUnit)) {
+ start = dateEnv.startOfWeek(start);
+ // make end-of-week if not already
+ endOfWeek = dateEnv.startOfWeek(end);
+ if (endOfWeek.valueOf() !== end.valueOf()) {
+ end = addWeeks(endOfWeek, 1);
+ }
+ }
+ // ensure 6 weeks
+ if (this.props.monthMode &&
+ this.props.fixedWeekCount) {
+ var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
+ diffWeeks(start, end));
+ end = addWeeks(end, 6 - rowCnt);
+ }
+ return { start: start, end: end };
+ };
+ return TableDateProfileGenerator;
+ }(DateProfileGenerator));
+
+ var dayGridPlugin = createPlugin({
+ initialView: 'dayGridMonth',
+ views: {
+ dayGrid: {
+ component: DayTableView,
+ dateProfileGeneratorClass: TableDateProfileGenerator,
+ },
+ dayGridDay: {
+ type: 'dayGrid',
+ duration: { days: 1 },
+ },
+ dayGridWeek: {
+ type: 'dayGrid',
+ duration: { weeks: 1 },
+ },
+ dayGridMonth: {
+ type: 'dayGrid',
+ duration: { months: 1 },
+ monthMode: true,
+ fixedWeekCount: true,
+ },
+ },
+ });
+
+ var AllDaySplitter = /** @class */ (function (_super) {
+ __extends(AllDaySplitter, _super);
+ function AllDaySplitter() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ AllDaySplitter.prototype.getKeyInfo = function () {
+ return {
+ allDay: {},
+ timed: {},
+ };
+ };
+ AllDaySplitter.prototype.getKeysForDateSpan = function (dateSpan) {
+ if (dateSpan.allDay) {
+ return ['allDay'];
+ }
+ return ['timed'];
+ };
+ AllDaySplitter.prototype.getKeysForEventDef = function (eventDef) {
+ if (!eventDef.allDay) {
+ return ['timed'];
+ }
+ if (hasBgRendering(eventDef)) {
+ return ['timed', 'allDay'];
+ }
+ return ['allDay'];
+ };
+ return AllDaySplitter;
+ }(Splitter));
+
+ var DEFAULT_SLAT_LABEL_FORMAT = createFormatter({
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: true,
+ meridiem: 'short',
+ });
+ function TimeColsAxisCell(props) {
+ var classNames = [
+ 'fc-timegrid-slot',
+ 'fc-timegrid-slot-label',
+ props.isLabeled ? 'fc-scrollgrid-shrink' : 'fc-timegrid-slot-minor',
+ ];
+ return (createElement(ViewContextType.Consumer, null, function (context) {
+ if (!props.isLabeled) {
+ return (createElement("td", { className: classNames.join(' '), "data-time": props.isoTimeStr }));
+ }
+ var dateEnv = context.dateEnv, options = context.options, viewApi = context.viewApi;
+ var labelFormat = // TODO: fully pre-parse
+ options.slotLabelFormat == null ? DEFAULT_SLAT_LABEL_FORMAT :
+ Array.isArray(options.slotLabelFormat) ? createFormatter(options.slotLabelFormat[0]) :
+ createFormatter(options.slotLabelFormat);
+ var hookProps = {
+ level: 0,
+ time: props.time,
+ date: dateEnv.toDate(props.date),
+ view: viewApi,
+ text: dateEnv.format(props.date, labelFormat),
+ };
+ return (createElement(RenderHook, { hookProps: hookProps, classNames: options.slotLabelClassNames, content: options.slotLabelContent, defaultContent: renderInnerContent$1, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-time": props.isoTimeStr },
+ createElement("div", { className: "fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame" },
+ createElement("div", { className: "fc-timegrid-slot-label-cushion fc-scrollgrid-shrink-cushion", ref: innerElRef }, innerContent)))); }));
+ }));
+ }
+ function renderInnerContent$1(props) {
+ return props.text;
+ }
+
+ var TimeBodyAxis = /** @class */ (function (_super) {
+ __extends(TimeBodyAxis, _super);
+ function TimeBodyAxis() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ TimeBodyAxis.prototype.render = function () {
+ return this.props.slatMetas.map(function (slatMeta) { return (createElement("tr", { key: slatMeta.key },
+ createElement(TimeColsAxisCell, __assign({}, slatMeta)))); });
+ };
+ return TimeBodyAxis;
+ }(BaseComponent));
+
+ var DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'short' });
+ var AUTO_ALL_DAY_MAX_EVENT_ROWS = 5;
+ var TimeColsView = /** @class */ (function (_super) {
+ __extends(TimeColsView, _super);
+ function TimeColsView() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.allDaySplitter = new AllDaySplitter(); // for use by subclasses
+ _this.headerElRef = createRef();
+ _this.rootElRef = createRef();
+ _this.scrollerElRef = createRef();
+ _this.state = {
+ slatCoords: null,
+ };
+ _this.handleScrollTopRequest = function (scrollTop) {
+ var scrollerEl = _this.scrollerElRef.current;
+ if (scrollerEl) { // TODO: not sure how this could ever be null. weirdness with the reducer
+ scrollerEl.scrollTop = scrollTop;
+ }
+ };
+ /* Header Render Methods
+ ------------------------------------------------------------------------------------------------------------------*/
+ _this.renderHeadAxis = function (rowKey, frameHeight) {
+ if (frameHeight === void 0) { frameHeight = ''; }
+ var options = _this.context.options;
+ var dateProfile = _this.props.dateProfile;
+ var range = dateProfile.renderRange;
+ var dayCnt = diffDays(range.start, range.end);
+ var navLinkAttrs = (options.navLinks && dayCnt === 1) // only do in day views (to avoid doing in week views that dont need it)
+ ? { 'data-navlink': buildNavLinkData(range.start, 'week'), tabIndex: 0 }
+ : {};
+ if (options.weekNumbers && rowKey === 'day') {
+ return (createElement(WeekNumberRoot, { date: range.start, defaultFormat: DEFAULT_WEEK_NUM_FORMAT }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("th", { ref: rootElRef, className: [
+ 'fc-timegrid-axis',
+ 'fc-scrollgrid-shrink',
+ ].concat(classNames).join(' ') },
+ createElement("div", { className: "fc-timegrid-axis-frame fc-scrollgrid-shrink-frame fc-timegrid-axis-frame-liquid", style: { height: frameHeight } },
+ createElement("a", __assign({ ref: innerElRef, className: "fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner" }, navLinkAttrs), innerContent)))); }));
+ }
+ return (createElement("th", { className: "fc-timegrid-axis" },
+ createElement("div", { className: "fc-timegrid-axis-frame", style: { height: frameHeight } })));
+ };
+ /* Table Component Render Methods
+ ------------------------------------------------------------------------------------------------------------------*/
+ // only a one-way height sync. we don't send the axis inner-content height to the DayGrid,
+ // but DayGrid still needs to have classNames on inner elements in order to measure.
+ _this.renderTableRowAxis = function (rowHeight) {
+ var _a = _this.context, options = _a.options, viewApi = _a.viewApi;
+ var hookProps = {
+ text: options.allDayText,
+ view: viewApi,
+ };
+ return (
+ // TODO: make reusable hook. used in list view too
+ createElement(RenderHook, { hookProps: hookProps, classNames: options.allDayClassNames, content: options.allDayContent, defaultContent: renderAllDayInner$1, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: [
+ 'fc-timegrid-axis',
+ 'fc-scrollgrid-shrink',
+ ].concat(classNames).join(' ') },
+ createElement("div", { className: 'fc-timegrid-axis-frame fc-scrollgrid-shrink-frame' + (rowHeight == null ? ' fc-timegrid-axis-frame-liquid' : ''), style: { height: rowHeight } },
+ createElement("span", { className: "fc-timegrid-axis-cushion fc-scrollgrid-shrink-cushion fc-scrollgrid-sync-inner", ref: innerElRef }, innerContent)))); }));
+ };
+ _this.handleSlatCoords = function (slatCoords) {
+ _this.setState({ slatCoords: slatCoords });
+ };
+ return _this;
+ }
+ // rendering
+ // ----------------------------------------------------------------------------------------------------
+ TimeColsView.prototype.renderSimpleLayout = function (headerRowContent, allDayContent, timeContent) {
+ var _a = this, context = _a.context, props = _a.props;
+ var sections = [];
+ var stickyHeaderDates = getStickyHeaderDates(context.options);
+ if (headerRowContent) {
+ sections.push({
+ type: 'header',
+ key: 'header',
+ isSticky: stickyHeaderDates,
+ chunk: {
+ elRef: this.headerElRef,
+ tableClassName: 'fc-col-header',
+ rowContent: headerRowContent,
+ },
+ });
+ }
+ if (allDayContent) {
+ sections.push({
+ type: 'body',
+ key: 'all-day',
+ chunk: { content: allDayContent },
+ });
+ sections.push({
+ type: 'body',
+ key: 'all-day-divider',
+ outerContent: ( // TODO: rename to cellContent so don't need to define ?
+ createElement("tr", { className: "fc-scrollgrid-section" },
+ createElement("td", { className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))),
+ });
+ }
+ sections.push({
+ type: 'body',
+ key: 'body',
+ liquid: true,
+ expandRows: Boolean(context.options.expandRows),
+ chunk: {
+ scrollerElRef: this.scrollerElRef,
+ content: timeContent,
+ },
+ });
+ return (createElement(ViewRoot, { viewSpec: context.viewSpec, elRef: this.rootElRef }, function (rootElRef, classNames) { return (createElement("div", { className: ['fc-timegrid'].concat(classNames).join(' '), ref: rootElRef },
+ createElement(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: props.forPrint, cols: [{ width: 'shrink' }], sections: sections }))); }));
+ };
+ TimeColsView.prototype.renderHScrollLayout = function (headerRowContent, allDayContent, timeContent, colCnt, dayMinWidth, slatMetas, slatCoords) {
+ var _this = this;
+ var ScrollGrid = this.context.pluginHooks.scrollGridImpl;
+ if (!ScrollGrid) {
+ throw new Error('No ScrollGrid implementation');
+ }
+ var _a = this, context = _a.context, props = _a.props;
+ var stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options);
+ var stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options);
+ var sections = [];
+ if (headerRowContent) {
+ sections.push({
+ type: 'header',
+ key: 'header',
+ isSticky: stickyHeaderDates,
+ syncRowHeights: true,
+ chunks: [
+ {
+ key: 'axis',
+ rowContent: function (arg) { return (createElement("tr", null, _this.renderHeadAxis('day', arg.rowSyncHeights[0]))); },
+ },
+ {
+ key: 'cols',
+ elRef: this.headerElRef,
+ tableClassName: 'fc-col-header',
+ rowContent: headerRowContent,
+ },
+ ],
+ });
+ }
+ if (allDayContent) {
+ sections.push({
+ type: 'body',
+ key: 'all-day',
+ syncRowHeights: true,
+ chunks: [
+ {
+ key: 'axis',
+ rowContent: function (contentArg) { return (createElement("tr", null, _this.renderTableRowAxis(contentArg.rowSyncHeights[0]))); },
+ },
+ {
+ key: 'cols',
+ content: allDayContent,
+ },
+ ],
+ });
+ sections.push({
+ key: 'all-day-divider',
+ type: 'body',
+ outerContent: ( // TODO: rename to cellContent so don't need to define ?
+ createElement("tr", { className: "fc-scrollgrid-section" },
+ createElement("td", { colSpan: 2, className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))),
+ });
+ }
+ var isNowIndicator = context.options.nowIndicator;
+ sections.push({
+ type: 'body',
+ key: 'body',
+ liquid: true,
+ expandRows: Boolean(context.options.expandRows),
+ chunks: [
+ {
+ key: 'axis',
+ content: function (arg) { return (
+ // TODO: make this now-indicator arrow more DRY with TimeColsContent
+ createElement("div", { className: "fc-timegrid-axis-chunk" },
+ createElement("table", { style: { height: arg.expandRows ? arg.clientHeight : '' } },
+ arg.tableColGroupNode,
+ createElement("tbody", null,
+ createElement(TimeBodyAxis, { slatMetas: slatMetas }))),
+ createElement("div", { className: "fc-timegrid-now-indicator-container" },
+ createElement(NowTimer, { unit: isNowIndicator ? 'minute' : 'day' /* hacky */ }, function (nowDate) {
+ var nowIndicatorTop = isNowIndicator &&
+ slatCoords &&
+ slatCoords.safeComputeTop(nowDate); // might return void
+ if (typeof nowIndicatorTop === 'number') {
+ return (createElement(NowIndicatorRoot, { isAxis: true, date: nowDate }, function (rootElRef, classNames, innerElRef, innerContent) { return (createElement("div", { ref: rootElRef, className: ['fc-timegrid-now-indicator-arrow'].concat(classNames).join(' '), style: { top: nowIndicatorTop } }, innerContent)); }));
+ }
+ return null;
+ })))); },
+ },
+ {
+ key: 'cols',
+ scrollerElRef: this.scrollerElRef,
+ content: timeContent,
+ },
+ ],
+ });
+ if (stickyFooterScrollbar) {
+ sections.push({
+ key: 'footer',
+ type: 'footer',
+ isSticky: true,
+ chunks: [
+ {
+ key: 'axis',
+ content: renderScrollShim,
+ },
+ {
+ key: 'cols',
+ content: renderScrollShim,
+ },
+ ],
+ });
+ }
+ return (createElement(ViewRoot, { viewSpec: context.viewSpec, elRef: this.rootElRef }, function (rootElRef, classNames) { return (createElement("div", { className: ['fc-timegrid'].concat(classNames).join(' '), ref: rootElRef },
+ createElement(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: false, colGroups: [
+ { width: 'shrink', cols: [{ width: 'shrink' }] },
+ { cols: [{ span: colCnt, minWidth: dayMinWidth }] },
+ ], sections: sections }))); }));
+ };
+ /* Dimensions
+ ------------------------------------------------------------------------------------------------------------------*/
+ TimeColsView.prototype.getAllDayMaxEventProps = function () {
+ var _a = this.context.options, dayMaxEvents = _a.dayMaxEvents, dayMaxEventRows = _a.dayMaxEventRows;
+ if (dayMaxEvents === true || dayMaxEventRows === true) { // is auto?
+ dayMaxEvents = undefined;
+ dayMaxEventRows = AUTO_ALL_DAY_MAX_EVENT_ROWS; // make sure "auto" goes to a real number
+ }
+ return { dayMaxEvents: dayMaxEvents, dayMaxEventRows: dayMaxEventRows };
+ };
+ return TimeColsView;
+ }(DateComponent));
+ function renderAllDayInner$1(hookProps) {
+ return hookProps.text;
+ }
+
+ var TimeColsSlatsCoords = /** @class */ (function () {
+ function TimeColsSlatsCoords(positions, dateProfile, slotDuration) {
+ this.positions = positions;
+ this.dateProfile = dateProfile;
+ this.slotDuration = slotDuration;
+ }
+ TimeColsSlatsCoords.prototype.safeComputeTop = function (date) {
+ var dateProfile = this.dateProfile;
+ if (rangeContainsMarker(dateProfile.currentRange, date)) {
+ var startOfDayDate = startOfDay(date);
+ var timeMs = date.valueOf() - startOfDayDate.valueOf();
+ if (timeMs >= asRoughMs(dateProfile.slotMinTime) &&
+ timeMs < asRoughMs(dateProfile.slotMaxTime)) {
+ return this.computeTimeTop(createDuration(timeMs));
+ }
+ }
+ return null;
+ };
+ // Computes the top coordinate, relative to the bounds of the grid, of the given date.
+ // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
+ TimeColsSlatsCoords.prototype.computeDateTop = function (when, startOfDayDate) {
+ if (!startOfDayDate) {
+ startOfDayDate = startOfDay(when);
+ }
+ return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf()));
+ };
+ // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
+ // This is a makeshify way to compute the time-top. Assumes all slatMetas dates are uniform.
+ // Eventually allow computation with arbirary slat dates.
+ TimeColsSlatsCoords.prototype.computeTimeTop = function (duration) {
+ var _a = this, positions = _a.positions, dateProfile = _a.dateProfile;
+ var len = positions.els.length;
+ // floating-point value of # of slots covered
+ var slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.slotMinTime)) / asRoughMs(this.slotDuration);
+ var slatIndex;
+ var slatRemainder;
+ // compute a floating-point number for how many slats should be progressed through.
+ // from 0 to number of slats (inclusive)
+ // constrained because slotMinTime/slotMaxTime might be customized.
+ slatCoverage = Math.max(0, slatCoverage);
+ slatCoverage = Math.min(len, slatCoverage);
+ // an integer index of the furthest whole slat
+ // from 0 to number slats (*exclusive*, so len-1)
+ slatIndex = Math.floor(slatCoverage);
+ slatIndex = Math.min(slatIndex, len - 1);
+ // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
+ // could be 1.0 if slatCoverage is covering *all* the slots
+ slatRemainder = slatCoverage - slatIndex;
+ return positions.tops[slatIndex] +
+ positions.getHeight(slatIndex) * slatRemainder;
+ };
+ return TimeColsSlatsCoords;
+ }());
+
+ var TimeColsSlatsBody = /** @class */ (function (_super) {
+ __extends(TimeColsSlatsBody, _super);
+ function TimeColsSlatsBody() {
+ return _super !== null && _super.apply(this, arguments) || this;
+ }
+ TimeColsSlatsBody.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ var options = context.options;
+ var slatElRefs = props.slatElRefs;
+ return (createElement("tbody", null, props.slatMetas.map(function (slatMeta, i) {
+ var hookProps = {
+ time: slatMeta.time,
+ date: context.dateEnv.toDate(slatMeta.date),
+ view: context.viewApi,
+ };
+ var classNames = [
+ 'fc-timegrid-slot',
+ 'fc-timegrid-slot-lane',
+ slatMeta.isLabeled ? '' : 'fc-timegrid-slot-minor',
+ ];
+ return (createElement("tr", { key: slatMeta.key, ref: slatElRefs.createRef(slatMeta.key) },
+ props.axis && (createElement(TimeColsAxisCell, __assign({}, slatMeta))),
+ createElement(RenderHook, { hookProps: hookProps, classNames: options.slotLaneClassNames, content: options.slotLaneContent, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount }, function (rootElRef, customClassNames, innerElRef, innerContent) { return (createElement("td", { ref: rootElRef, className: classNames.concat(customClassNames).join(' '), "data-time": slatMeta.isoTimeStr }, innerContent)); })));
+ })));
+ };
+ return TimeColsSlatsBody;
+ }(BaseComponent));
+
+ /*
+ for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
+ */
+ var TimeColsSlats = /** @class */ (function (_super) {
+ __extends(TimeColsSlats, _super);
+ function TimeColsSlats() {
+ var _this = _super !== null && _super.apply(this, arguments) || this;
+ _this.rootElRef = createRef();
+ _this.slatElRefs = new RefMap();
+ return _this;
+ }
+ TimeColsSlats.prototype.render = function () {
+ var _a = this, props = _a.props, context = _a.context;
+ return (createElement("div", { className: "fc-timegrid-slots", ref: this.rootElRef },
+ createElement("table", { className: context.theme.getClass('table'), style: {
+ minWidth: props.tableMinWidth,
+ width: props.clientWidth,
+ height: props.minHeight,
+ } },
+ props.tableColGroupNode /* relies on there only being a single